package bake import ( "context" "fmt" "os" "path/filepath" "sort" "strings" "testing" "github.com/docker/buildx/util/buildflags" "github.com/moby/buildkit/util/entitlements" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestReadTargets(t *testing.T) { fp := File{ Name: "config.hcl", Data: []byte(` target "webDEP" { args = { VAR_INHERITED = "webDEP" VAR_BOTH = "webDEP" } no-cache = true shm-size = "128m" ulimits = ["nofile=1024:1024"] resources = { memory = "2g" memory-swap = "4g" cpu-shares = 1024 cpu-period = 100000 cpu-quota = 50000 cpuset-cpus = "0-3" cpuset-mems = "0,1" } extra-hosts = { my_hostname = "8.8.8.8" } } target "webapp" { dockerfile = "Dockerfile.webapp" args = { VAR_BOTH = "webapp" } annotations = [ "index,manifest:org.opencontainers.image.authors=dvdksn" ] attest = [ "type=provenance,mode=max" ] platforms = [ "linux/amd64" ] secret = [ "id=FOO,env=FOO" ] inherits = ["webDEP"] }`), } ctx := context.TODO() t.Run("NoOverrides", func(t *testing.T) { t.Parallel() m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m)) require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile) require.Equal(t, ".", *m["webapp"].Context) require.Equal(t, ptrstr("webDEP"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, true, *m["webapp"].NoCache) require.Equal(t, "128m", *m["webapp"].ShmSize) require.Equal(t, []string{"nofile=1024:1024"}, m["webapp"].Ulimits) require.NotNil(t, m["webapp"].Resources) require.Equal(t, ptrstr("2g"), m["webapp"].Resources.Memory) require.Equal(t, ptrstr("4g"), m["webapp"].Resources.MemorySwap) require.Equal(t, int64(1024), *m["webapp"].Resources.CPUShares) require.Equal(t, int64(100000), *m["webapp"].Resources.CPUPeriod) require.Equal(t, int64(50000), *m["webapp"].Resources.CPUQuota) require.Equal(t, ptrstr("0-3"), m["webapp"].Resources.CPUSetCPUs) require.Equal(t, ptrstr("0,1"), m["webapp"].Resources.CPUSetMems) require.Equal(t, map[string]*string{"my_hostname": ptrstr("8.8.8.8")}, m["webapp"].ExtraHosts) require.Nil(t, m["webapp"].Pull) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }) t.Run("InvalidTargetOverrides", func(t *testing.T) { t.Parallel() _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"nosuchtarget.context=foo"}, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Equal(t, "could not find any target matching 'nosuchtarget'", err.Error()) }) t.Run("ArgsOverrides", func(t *testing.T) { t.Run("leaf", func(t *testing.T) { t.Setenv("VAR_FROMENV"+t.Name(), "fromEnv") m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{ "webapp.args.VAR_UNSET", "webapp.args.VAR_EMPTY=", "webapp.args.VAR_SET=bananas", "webapp.args.VAR_FROMENV" + t.Name(), "webapp.args.VAR_INHERITED=override", // not overriding VAR_BOTH on purpose }, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile) require.Equal(t, ".", *m["webapp"].Context) _, isSet := m["webapp"].Args["VAR_UNSET"] require.False(t, isSet, m["webapp"].Args["VAR_UNSET"]) _, isSet = m["webapp"].Args["VAR_EMPTY"] require.True(t, isSet, m["webapp"].Args["VAR_EMPTY"]) require.Equal(t, ptrstr("bananas"), m["webapp"].Args["VAR_SET"]) require.Equal(t, ptrstr("fromEnv"), m["webapp"].Args["VAR_FROMENV"+t.Name()]) require.Equal(t, ptrstr("webapp"), m["webapp"].Args["VAR_BOTH"]) require.Equal(t, ptrstr("override"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }) // building leaf but overriding parent fields t.Run("parent", func(t *testing.T) { t.Parallel() m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{ "webDEP.args.VAR_INHERITED=override", "webDEP.args.VAR_BOTH=override", }, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, ptrstr("override"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, ptrstr("webapp"), m["webapp"].Args["VAR_BOTH"]) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }) }) t.Run("AnnotationsOverrides", func(t *testing.T) { t.Parallel() m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.annotations=index,manifest:org.opencontainers.image.vendor=docker"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, []string{"index,manifest:org.opencontainers.image.authors=dvdksn", "index,manifest:org.opencontainers.image.vendor=docker"}, m["webapp"].Annotations) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }) t.Run("AttestOverride", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.attest=type=sbom"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Len(t, m["webapp"].Attest, 2) require.Equal(t, "provenance", m["webapp"].Attest[0].Type) require.Equal(t, "sbom", m["webapp"].Attest[1].Type) }) t.Run("AttestAppend", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.attest+=type=sbom"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Len(t, m["webapp"].Attest, 2) require.Equal(t, "provenance", m["webapp"].Attest[0].Type) require.Equal(t, "sbom", m["webapp"].Attest[1].Type) }) t.Run("ContextOverride", func(t *testing.T) { t.Parallel() _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.context"}, nil, nil, &EntitlementConf{}) require.Error(t, err) m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.context=foo"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, "foo", *m["webapp"].Context) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }) t.Run("NoCacheOverride", func(t *testing.T) { t.Parallel() m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.no-cache=false"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, false, *m["webapp"].NoCache) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }) t.Run("PlatformOverride", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.platform=linux/arm64"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, []string{"linux/arm64"}, m["webapp"].Platforms) }) t.Run("PlatformAppend", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.platform+=linux/arm64"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, []string{"linux/amd64", "linux/arm64"}, m["webapp"].Platforms) }) t.Run("PlatformAppendMulti", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.platform+=linux/arm64", "webapp.platform+=linux/riscv64"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, []string{"linux/amd64", "linux/arm64", "linux/riscv64"}, m["webapp"].Platforms) }) t.Run("PlatformAppendMultiLastOverride", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.platform+=linux/arm64", "webapp.platform=linux/riscv64"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, []string{"linux/arm64", "linux/riscv64"}, m["webapp"].Platforms) }) t.Run("SecretsOverride", func(t *testing.T) { t.Setenv("FOO", "foo") t.Setenv("BAR", "bar") m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secrets=id=BAR,env=BAR"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Len(t, m["webapp"].Secrets, 1) require.Equal(t, "BAR", m["webapp"].Secrets[0].ID) }) t.Run("SecretsAppend", func(t *testing.T) { t.Setenv("FOO", "foo") t.Setenv("BAR", "bar") m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secrets+=id=BAR,env=BAR"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Len(t, m["webapp"].Secrets, 2) require.Equal(t, "FOO", m["webapp"].Secrets[0].ID) require.Equal(t, "BAR", m["webapp"].Secrets[1].ID) }) t.Run("SecretSourceOverrideEnv", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret.FOO=env=BAR"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Len(t, m["webapp"].Secrets, 1) require.Equal(t, "FOO", m["webapp"].Secrets[0].ID) require.Equal(t, "BAR", m["webapp"].Secrets[0].Env) require.Empty(t, m["webapp"].Secrets[0].FilePath) }) t.Run("SecretSourceOverrideFile", func(t *testing.T) { ent := &EntitlementConf{} m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret.FOO=src=/tmp/foo"}, nil, nil, ent) require.NoError(t, err) require.Len(t, m["webapp"].Secrets, 1) require.Equal(t, "FOO", m["webapp"].Secrets[0].ID) require.Equal(t, "/tmp/foo", m["webapp"].Secrets[0].FilePath) require.Empty(t, m["webapp"].Secrets[0].Env) require.Equal(t, []string{"/tmp/foo"}, ent.FSRead) }) t.Run("SecretSourceOverrideUsesFinalSourceForEntitlements", func(t *testing.T) { ent := &EntitlementConf{} m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secrets=id=FOO,src=/tmp/foo", "webapp.secret.FOO=env=BAR"}, nil, nil, ent) require.NoError(t, err) require.Len(t, m["webapp"].Secrets, 1) require.Equal(t, "FOO", m["webapp"].Secrets[0].ID) require.Equal(t, "BAR", m["webapp"].Secrets[0].Env) require.Empty(t, m["webapp"].Secrets[0].FilePath) require.Empty(t, ent.FSRead) }) t.Run("SecretSourceOverrideUndeclared", func(t *testing.T) { _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret.BAR=env=BAR"}, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Equal(t, `secret "BAR" must be declared before it can be overridden`, err.Error()) }) t.Run("SecretSourceOverrideMismatchedID", func(t *testing.T) { _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.secret.FOO=id=BAR,env=BAR"}, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Equal(t, `secret override id "BAR" does not match declared secret "FOO"`, err.Error()) }) t.Run("ShmSizeOverride", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.shm-size=256m"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, "256m", *m["webapp"].ShmSize) }) t.Run("ResourceLimitsOverride", func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{ "webapp.resources.memory=512m", "webapp.resources.cpu-quota=25000", }, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, ptrstr("512m"), m["webapp"].Resources.Memory) require.Equal(t, int64(25000), *m["webapp"].Resources.CPUQuota) }) t.Run("ResourceLimitsInvalidOverride", func(t *testing.T) { _, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.resources.cpu-quota=notanumber"}, nil, nil, &EntitlementConf{}) require.Error(t, err) }) t.Run("PullOverride", func(t *testing.T) { t.Parallel() m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.pull=false"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, false, *m["webapp"].Pull) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }) t.Run("PatternOverride", func(t *testing.T) { t.Parallel() // same check for two cases multiTargetCheck := func(t *testing.T, m map[string]*Target, g map[string]*Group, err error) { require.NoError(t, err) require.Equal(t, 2, len(m)) require.Equal(t, "foo", *m["webapp"].Dockerfile) require.Equal(t, ptrstr("webDEP"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, "foo", *m["webDEP"].Dockerfile) require.Equal(t, ptrstr("webDEP"), m["webDEP"].Args["VAR_INHERITED"]) require.Equal(t, 1, len(g)) sort.Strings(g["default"].Targets) require.Equal(t, []string{"webDEP", "webapp"}, g["default"].Targets) } cases := []struct { name string targets []string overrides []string check func(*testing.T, map[string]*Target, map[string]*Group, error) }{ { name: "multi target single pattern", targets: []string{"webapp", "webDEP"}, overrides: []string{"web*.dockerfile=foo"}, check: multiTargetCheck, }, { name: "multi target multi pattern", targets: []string{"webapp", "webDEP"}, overrides: []string{"web*.dockerfile=foo", "*.args.VAR_BOTH=bar"}, check: multiTargetCheck, }, { name: "single target", targets: []string{"webapp"}, overrides: []string{"web*.dockerfile=foo"}, check: func(t *testing.T, m map[string]*Target, g map[string]*Group, err error) { require.NoError(t, err) require.Equal(t, 1, len(m)) require.Equal(t, "foo", *m["webapp"].Dockerfile) require.Equal(t, ptrstr("webDEP"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }, }, { name: "nomatch", targets: []string{"webapp"}, overrides: []string{"nomatch*.dockerfile=foo"}, check: func(t *testing.T, m map[string]*Target, g map[string]*Group, err error) { // NOTE: I am unsure whether failing to match should always error out // instead of simply skipping that override. // Let's enforce the error and we can relax it later if users complain. require.Error(t, err) require.Equal(t, "could not find any target matching 'nomatch*'", err.Error()) }, }, } for _, test := range cases { t.Run(test.name, func(t *testing.T) { m, g, err := ReadTargets(ctx, []File{fp}, test.targets, test.overrides, nil, nil, &EntitlementConf{}) test.check(t, m, g, err) }) } }) } func TestPushOverride(t *testing.T) { t.Run("empty output", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m["app"].Outputs)) require.Equal(t, "type=image,push=true", m["app"].Outputs[0].String()) }) t.Run("type image", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=image,compression=zstd"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m["app"].Outputs)) require.Equal(t, "type=image,compression=zstd,push=true", m["app"].Outputs[0].String()) }) t.Run("type image push false", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=image,compression=zstd"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=false"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m["app"].Outputs)) require.Equal(t, "type=image,compression=zstd,push=false", m["app"].Outputs[0].String()) }) t.Run("type registry", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=registry"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m["app"].Outputs)) require.Equal(t, "type=registry", m["app"].Outputs[0].String()) }) t.Run("type registry push false", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=registry"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=false"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 0, len(m["app"].Outputs)) }) t.Run("type local and empty target", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "foo" { output = [ "type=local,dest=out" ] } target "bar" { }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"foo", "bar"}, []string{"*.push=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 2, len(m)) require.Equal(t, 1, len(m["foo"].Outputs)) require.Equal(t, []string{"type=local,dest=out"}, stringify(m["foo"].Outputs)) require.Equal(t, 1, len(m["bar"].Outputs)) require.Equal(t, []string{"type=image,push=true"}, stringify(m["bar"].Outputs)) }) } func TestLoadOverride(t *testing.T) { t.Run("empty output", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m["app"].Outputs)) require.Equal(t, "type=docker", m["app"].Outputs[0].String()) }) t.Run("type docker", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=docker"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m["app"].Outputs)) require.Equal(t, []string{"type=docker"}, stringify(m["app"].Outputs)) }) t.Run("type image", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=image"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 2, len(m["app"].Outputs)) require.Equal(t, []string{"type=docker", "type=image"}, stringify(m["app"].Outputs)) }) t.Run("type image load false", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=image"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=false"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m["app"].Outputs)) require.Equal(t, []string{"type=image"}, stringify(m["app"].Outputs)) }) t.Run("type registry", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=registry"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 2, len(m["app"].Outputs)) require.Equal(t, []string{"type=docker", "type=registry"}, stringify(m["app"].Outputs)) }) t.Run("type oci", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=oci,dest=out"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 2, len(m["app"].Outputs)) require.Equal(t, []string{"type=docker", "type=oci,dest=out"}, stringify(m["app"].Outputs)) }) t.Run("type docker with dest", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { output = ["type=docker,dest=out"] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 2, len(m["app"].Outputs)) require.Equal(t, []string{"type=docker", "type=docker,dest=out"}, stringify(m["app"].Outputs)) }) t.Run("type local and empty target", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "foo" { output = [ "type=local,dest=out" ] } target "bar" { }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"foo", "bar"}, []string{"*.load=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 2, len(m)) require.Equal(t, 1, len(m["foo"].Outputs)) require.Equal(t, []string{"type=local,dest=out"}, stringify(m["foo"].Outputs)) require.Equal(t, 1, len(m["bar"].Outputs)) require.Equal(t, []string{"type=docker"}, stringify(m["bar"].Outputs)) }) } func TestLoadAndPushOverride(t *testing.T) { t.Run("type local and empty target", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "foo" { output = [ "type=local,dest=out" ] } target "bar" { }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"foo", "bar"}, []string{"*.load=true", "*.push=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 2, len(m)) require.Equal(t, 1, len(m["foo"].Outputs)) require.Equal(t, []string{"type=local,dest=out"}, stringify(m["foo"].Outputs)) require.Equal(t, 2, len(m["bar"].Outputs)) require.Equal(t, []string{"type=docker", "type=image,push=true"}, stringify(m["bar"].Outputs)) }) t.Run("type registry", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "foo" { output = [ "type=registry" ] }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"foo"}, []string{"*.load=true", "*.push=true"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m)) require.Equal(t, 2, len(m["foo"].Outputs)) require.Equal(t, []string{"type=docker", "type=registry"}, stringify(m["foo"].Outputs)) }) } func TestReadTargetsCompose(t *testing.T) { t.Parallel() fp := File{ Name: "docker-compose.yml", Data: []byte( `version: "3" services: db: build: . command: ./entrypoint.sh image: docker.io/tonistiigi/db webapp: build: dockerfile: Dockerfile.webapp args: buildno: 1 `), } fp2 := File{ Name: "docker-compose2.yml", Data: []byte( `version: "3" services: newservice: build: . webapp: build: args: buildno2: 12 `), } fp3 := File{ Name: "docker-compose3.yml", Data: []byte( `version: "3" services: webapp: entrypoint: echo 1 `), } ctx := context.TODO() m, g, err := ReadTargets(ctx, []File{fp, fp2, fp3}, []string{"default"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 3, len(m)) _, ok := m["newservice"] require.True(t, ok) require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile) require.Equal(t, ".", *m["webapp"].Context) require.Equal(t, ptrstr("1"), m["webapp"].Args["buildno"]) require.Equal(t, ptrstr("12"), m["webapp"].Args["buildno2"]) require.Equal(t, 1, len(g)) sort.Strings(g["default"].Targets) require.Equal(t, []string{"db", "newservice", "webapp"}, g["default"].Targets) } func TestReadTargetsWithDotCompose(t *testing.T) { t.Parallel() fp := File{ Name: "docker-compose.yml", Data: []byte( `version: "3" services: web.app: build: dockerfile: Dockerfile.webapp args: buildno: 1 `), } fp2 := File{ Name: "docker-compose2.yml", Data: []byte( `version: "3" services: web_app: build: args: buildno2: 12 `), } ctx := context.TODO() m, _, err := ReadTargets(ctx, []File{fp}, []string{"web.app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m)) _, ok := m["web_app"] require.True(t, ok) require.Equal(t, "Dockerfile.webapp", *m["web_app"].Dockerfile) require.Equal(t, ptrstr("1"), m["web_app"].Args["buildno"]) m, _, err = ReadTargets(ctx, []File{fp2}, []string{"web_app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m)) _, ok = m["web_app"] require.True(t, ok) require.Equal(t, "Dockerfile", *m["web_app"].Dockerfile) require.Equal(t, ptrstr("12"), m["web_app"].Args["buildno2"]) m, g, err := ReadTargets(ctx, []File{fp, fp2}, []string{"default"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(m)) _, ok = m["web_app"] require.True(t, ok) require.Equal(t, "Dockerfile.webapp", *m["web_app"].Dockerfile) require.Equal(t, ".", *m["web_app"].Context) require.Equal(t, ptrstr("1"), m["web_app"].Args["buildno"]) require.Equal(t, ptrstr("12"), m["web_app"].Args["buildno2"]) require.Equal(t, 1, len(g)) sort.Strings(g["default"].Targets) require.Equal(t, []string{"web_app"}, g["default"].Targets) } func TestHCLContextCwdPrefix(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { context = "cwd://foo" dockerfile = "test" }`), } ctx := context.TODO() m, g, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) bo, err := TargetsToBuildOpt(m, &Input{}) require.NoError(t, err) require.Equal(t, 1, len(g)) require.Equal(t, []string{"app"}, g["default"].Targets) require.Equal(t, 1, len(m)) require.Contains(t, m, "app") assert.Equal(t, "test", *m["app"].Dockerfile) assert.Equal(t, "foo", *m["app"].Context) assert.Equal(t, filepath.Clean("foo/test"), bo["app"].Inputs.DockerfilePath) assert.Equal(t, "foo", bo["app"].Inputs.ContextPath) } func TestHCLDockerfileCwdPrefix(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "app" { context = "." dockerfile = "cwd://Dockerfile.app" }`), } ctx := context.TODO() cwd, err := os.Getwd() require.NoError(t, err) m, g, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) bo, err := TargetsToBuildOpt(m, &Input{}) require.NoError(t, err) require.Equal(t, 1, len(g)) require.Equal(t, []string{"app"}, g["default"].Targets) require.Equal(t, 1, len(m)) require.Contains(t, m, "app") assert.Equal(t, "cwd://Dockerfile.app", *m["app"].Dockerfile) assert.Equal(t, ".", *m["app"].Context) assert.Equal(t, filepath.Join(cwd, "Dockerfile.app"), bo["app"].Inputs.DockerfilePath) assert.Equal(t, ".", bo["app"].Inputs.ContextPath) } func TestContextPathRebase(t *testing.T) { fp := File{ Name: filepath.Join("subdir", "docker-bake.hcl"), Data: []byte(` target "base" { context = "base" } target "app" { context = "." dockerfile = "Dockerfile.app" contexts = { shared = "../shared" cwd = "cwd://local" linked = "target:base" image = "docker-image://alpine:latest" layout = "oci-layout://layout" } }`), } m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{ FileRelativePaths: true, }) require.NoError(t, err) require.Equal(t, filepath.ToSlash(filepath.Clean("subdir")), *m["app"].Context) require.Equal(t, "Dockerfile.app", *m["app"].Dockerfile) require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), m["app"].Contexts["shared"]) require.Equal(t, "cwd://local", m["app"].Contexts["cwd"]) require.Equal(t, "target:base", m["app"].Contexts["linked"]) require.Equal(t, "docker-image://alpine:latest", m["app"].Contexts["image"]) require.Equal(t, "oci-layout://layout", m["app"].Contexts["layout"]) require.Equal(t, filepath.ToSlash(filepath.Clean("subdir/base")), *m["base"].Context) bo, err := TargetsToBuildOpt(m, &Input{}) require.NoError(t, err) require.Equal(t, filepath.ToSlash(filepath.Clean("subdir")), bo["app"].Inputs.ContextPath) require.Equal(t, filepath.Join("subdir", "Dockerfile.app"), bo["app"].Inputs.DockerfilePath) require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), bo["app"].Inputs.NamedContexts["shared"].Path) } func TestDefaultContextRebase(t *testing.T) { fp := File{ Name: filepath.Join("definitions", "docker-bake.hcl"), Data: []byte(` target "app" { dockerfile-inline = < 4 error_message = "FOO must be longer than 4 characters." } } target "app" { args = { FOO = FOO } } `), } ctx := context.TODO() t.Run("Valid", func(t *testing.T) { t.Setenv("FOO", "barbar") _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) }) t.Run("InvalidLength", func(t *testing.T) { t.Setenv("FOO", "bar") _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Contains(t, err.Error(), "FOO must be longer than 4 characters.") }) t.Run("InvalidEmpty", func(t *testing.T) { _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Contains(t, err.Error(), "FOO is required.") }) } func TestVariableValidationWithDeps(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte(` variable "FOO" {} variable "BAR" { validation { condition = FOO != "" error_message = "BAR requires FOO to be set." } } target "app" { args = { BAR = BAR } } `), } ctx := context.TODO() t.Run("Valid", func(t *testing.T) { t.Setenv("FOO", "bar") _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) }) t.Run("SetBar", func(t *testing.T) { t.Setenv("FOO", "bar") t.Setenv("BAR", "baz") _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) }) t.Run("Invalid", func(t *testing.T) { _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Contains(t, err.Error(), "BAR requires FOO to be set.") }) } func TestVariableValidationTyped(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte(` variable "FOO" { default = 0 validation { condition = FOO > 5 error_message = "FOO must be greater than 5." } } target "app" { args = { FOO = FOO } } `), } ctx := context.TODO() t.Run("Valid", func(t *testing.T) { t.Setenv("FOO", "10") _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) }) t.Run("Invalid", func(t *testing.T) { _, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Contains(t, err.Error(), "FOO must be greater than 5.") }) } func TestVariableValidationConditionNull(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte(` variable "PORT" { default = 3000 validation {} } target "app" { args = { PORT = PORT } } `), } _, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Contains(t, err.Error(), "Condition expression must return either true or false, not null") } func TestVariableValidationConditionUnknownValue(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte(` variable "PORT" { default = 3000 validation { condition = "foo" } } target "app" { args = { PORT = PORT } } `), } _, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Contains(t, err.Error(), "Invalid condition result value: a bool is required") } func TestVariableValidationInvalidErrorMessage(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte(` variable "FOO" { default = 0 validation { condition = FOO > 5 } } target "app" { args = { FOO = FOO } } `), } _, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.Error(t, err) require.Contains(t, err.Error(), "This check failed, but has an invalid error message") } // https://github.com/docker/buildx/issues/2822 func TestVariableEmpty(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte(` variable "FOO" { default = "" } target "app" { output = [FOO] } `), } ctx := context.TODO() m, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Contains(t, m, "app") require.Len(t, m["app"].Outputs, 0) } // https://github.com/docker/buildx/issues/2858 func TestOverrideEmpty(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte(` target "app" { output = ["./bin"] } `), } ctx := context.TODO() m, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, []string{"app.output="}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Contains(t, m, "app") require.Len(t, m["app"].Outputs, 0) } func TestEmptyDockerfile(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte(` target "app" { dockerfile = "" } `), } ctx := context.TODO() m, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Contains(t, m, "app") require.Equal(t, "Dockerfile", *m["app"].Dockerfile) } // https://github.com/docker/buildx/issues/2859 func TestGroupTargetsWithDefault(t *testing.T) { t.Run("OnTarget", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `target "default" { dockerfile = "Dockerfile" platforms = ["linux/amd64"] } target "multiarch" { dockerfile = "Dockerfile" platforms = ["linux/amd64","linux/arm64","linux/arm/v7","linux/arm/v6"] }`), } ctx := context.TODO() _, g, err := ReadTargets(ctx, []File{fp}, []string{"default", "multiarch"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(g)) require.Equal(t, 2, len(g["default"].Targets)) require.Equal(t, []string{"default", "multiarch"}, g["default"].Targets) }) t.Run("OnGroup", func(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( `group "default" { targets = ["app", "multiarch"] } target "app" { dockerfile = "app.Dockerfile" } target "foo" { dockerfile = "foo.Dockerfile" } target "multiarch" { dockerfile = "Dockerfile" platforms = ["linux/amd64","linux/arm64","linux/arm/v7","linux/arm/v6"] }`), } ctx := context.TODO() _, g, err := ReadTargets(ctx, []File{fp}, []string{"default", "foo"}, nil, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(g)) require.Equal(t, 3, len(g["default"].Targets)) require.Equal(t, []string{"app", "foo", "multiarch"}, g["default"].Targets) }) } func TestMatchNames(t *testing.T) { ctx := context.TODO() f := File{ Name: "docker-bake.hcl", Data: []byte(` group "default" { targets = ["foo", "foo-bar"] } group "baz" { targets = ["baz-foo", "baz-bar", "baz-fuu"] } target "foo" {} target "foo-bar" {} target "foo-baz" {} target "foo-fuu" {} target "bar-foo" {} target "bar-baz" {} target "bar-fuu" {} target "baz-foo" {} target "baz-bar" {} target "baz-fuu" {} target "mtx" { name = "mtx-${foo}-${bar}-${baz}" matrix = { foo = ["a"] bar = ["b", "c"] baz = ["d", "e", "f"] } } `), } cases := []struct { name string pattern []string expected []string wantErr bool }{ { name: "single", pattern: []string{"foo"}, expected: []string{"foo"}, }, { name: "starts with", pattern: []string{"foo-*"}, expected: []string{"foo-bar", "foo-baz", "foo-fuu"}, }, { name: "starts with group", pattern: []string{"baz*"}, expected: []string{"baz-bar", "baz-foo", "baz-fuu"}, }, { name: "matrix", pattern: []string{"mtx"}, expected: []string{"mtx-a-b-d", "mtx-a-b-e", "mtx-a-b-f", "mtx-a-c-d", "mtx-a-c-e", "mtx-a-c-f"}, }, { name: "matrix starts with", pattern: []string{"mtx-a-b-*"}, expected: []string{"mtx-a-b-d", "mtx-a-b-e", "mtx-a-b-f"}, }, { name: "any", pattern: []string{"*"}, expected: []string{"bar-baz", "bar-foo", "bar-fuu", "baz-bar", "baz-foo", "baz-fuu", "foo", "foo-bar", "foo-baz", "foo-fuu", "mtx-a-b-d", "mtx-a-b-e", "mtx-a-b-f", "mtx-a-c-d", "mtx-a-c-e", "mtx-a-c-f"}, }, { name: "any with group", pattern: []string{"*", "default"}, expected: []string{"bar-baz", "bar-foo", "bar-fuu", "baz-bar", "baz-foo", "baz-fuu", "foo", "foo-bar", "foo-baz", "foo-fuu", "mtx-a-b-d", "mtx-a-b-e", "mtx-a-b-f", "mtx-a-c-d", "mtx-a-c-e", "mtx-a-c-f"}, }, { name: "not found", pattern: []string{"aaa-*"}, wantErr: true, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { m, _, err := ReadTargets(ctx, []File{f}, tt.pattern, nil, nil, nil, &EntitlementConf{}) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) require.Equal(t, tt.expected, keys) } }) } } func TestRemoteURLWithSubdir(t *testing.T) { tests := []struct { name string remote string subdir string want string }{ { name: "git no ref", remote: "https://github.com/docker/buildx.git", subdir: "components/interface", want: "https://github.com/docker/buildx.git#:components/interface", }, { name: "git with ref", remote: "https://github.com/docker/buildx.git#main", subdir: "components/interface", want: "https://github.com/docker/buildx.git#main:components/interface", }, { name: "git with existing subdir", remote: "https://github.com/docker/buildx.git#main:base", subdir: "components/interface", want: "https://github.com/docker/buildx.git#main:base/components/interface", }, { name: "git query ref", remote: "https://github.com/docker/buildx.git?branch=main", subdir: "components/interface", want: "https://github.com/docker/buildx.git?branch=main#:components/interface", }, { name: "non git", remote: "https://example.com/context.tar.gz", subdir: "components/interface", want: "https://example.com/context.tar.gz", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := remoteURLWithSubdir(tt.remote, tt.subdir) assert.Equal(t, tt.want, got) }) } } func stringify[V fmt.Stringer](values []V) []string { s := make([]string, len(values)) for i, v := range values { s[i] = v.String() } sort.Strings(s) return s }