diff --git a/bake/bake.go b/bake/bake.go index d98cd7be1fbc..71e4d532be36 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -1,6 +1,7 @@ package bake import ( + "bytes" "context" "encoding" "io" @@ -638,10 +639,6 @@ func (c Config) ResolveTarget(name string, overrides map[string]map[string]Overr return nil, err } t.Inherits = nil - if t.Context == nil { - s := "." - t.Context = &s - } if t.Dockerfile == nil { s := "Dockerfile" t.Dockerfile = &s @@ -702,7 +699,7 @@ type Target struct { Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"` Attest buildflags.Attests `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"` - Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"` + Context **string `json:"context" hcl:"context,optional" cty:"context"` Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"` Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"` DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"` @@ -761,11 +758,23 @@ func (t *Target) normalize() { delete(t.Contexts, k) } } + if t.Context == nil { + s := t.ContextPath() + t.Context = &s + } if len(t.Contexts) == 0 { t.Contexts = nil } } +func (t *Target) ContextPath() *string { + if t.Context == nil { + p := "." + return &p + } + return (*string)(*t.Context) +} + func (t *Target) Merge(t2 *Target) { if t2.Context != nil { t.Context = t2.Context @@ -866,7 +875,8 @@ func (t *Target) AddOverrides(overrides map[string]Override, ent *EntitlementCon keys := strings.SplitN(key, ".", 2) switch keys[0] { case "context": - t.Context = &value + ptr := &value + t.Context = &ptr case "dockerfile": t.Dockerfile = &value case "args": @@ -1210,37 +1220,18 @@ func isLocalPath(p string) (string, bool) { } func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { - if v := t.Context; v != nil && *v == "-" { + if v := t.ContextPath(); v != nil && *v == "-" { return nil, errors.Errorf("context from stdin not allowed in bake") } if v := t.Dockerfile; v != nil && *v == "-" { return nil, errors.Errorf("dockerfile from stdin not allowed in bake") } - contextPath := "." - if t.Context != nil { - contextPath = *t.Context - } - if !strings.HasPrefix(contextPath, "cwd://") && !build.IsRemoteURL(contextPath) { - contextPath = path.Clean(contextPath) - } - dockerfilePath := "Dockerfile" - if t.Dockerfile != nil { - dockerfilePath = *t.Dockerfile - } - if !strings.HasPrefix(dockerfilePath, "cwd://") { - dockerfilePath = path.Clean(dockerfilePath) - } + bi := build.Inputs{} - bi := build.Inputs{ - ContextPath: contextPath, - DockerfilePath: dockerfilePath, - NamedContexts: toNamedContexts(t.Contexts), - } - if t.DockerfileInline != nil { - bi.DockerfileInline = *t.DockerfileInline - } + setBuildOptContexts(&bi, t) updateContext(&bi, inp) + if strings.HasPrefix(bi.DockerfilePath, "cwd://") { // If Dockerfile is local for a remote invocation, we first check if // it's not outside the working directory and then resolve it to an @@ -1273,7 +1264,7 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { if strings.HasPrefix(bi.ContextPath, "cwd://") { bi.ContextPath = path.Clean(strings.TrimPrefix(bi.ContextPath, "cwd://")) } - if !build.IsRemoteURL(bi.ContextPath) && bi.ContextState == nil && !path.IsAbs(bi.DockerfilePath) { + if !build.IsRemoteURL(bi.ContextPath) && bi.ContextState == nil && bi.DockerfilePath != "" && !path.IsAbs(bi.DockerfilePath) { bi.DockerfilePath = path.Join(bi.ContextPath, bi.DockerfilePath) } for k, v := range bi.NamedContexts { @@ -1282,8 +1273,6 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { } } - t.Context = &bi.ContextPath - args := map[string]string{} for k, v := range t.Args { if v == nil { @@ -1410,6 +1399,43 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { return bo, nil } +func setBuildOptContexts(bi *build.Inputs, t *Target) error { + dockerfilePath := "Dockerfile" + if t.Dockerfile != nil { + dockerfilePath = *t.Dockerfile + } + + if !strings.HasPrefix(dockerfilePath, "cwd://") { + dockerfilePath = path.Clean(dockerfilePath) + } + bi.DockerfilePath = dockerfilePath + + if contextPath := t.ContextPath(); contextPath != nil { + bi.ContextPath = *contextPath + if !strings.HasPrefix(bi.ContextPath, "cwd://") && !build.IsRemoteURL(bi.ContextPath) { + bi.ContextPath = path.Clean(bi.ContextPath) + } + + if t.DockerfileInline != nil { + bi.DockerfileInline = *t.DockerfileInline + } + } else { + bi.ContextPath = "-" + if t.DockerfileInline != nil { + bi.InStream = build.NewSyncMultiReader(strings.NewReader(*t.DockerfileInline)) + } else { + dockerfile, err := os.ReadFile(bi.DockerfilePath) + if err != nil { + return err + } + bi.DockerfilePath = "" + bi.InStream = build.NewSyncMultiReader(bytes.NewReader(dockerfile)) + } + } + bi.NamedContexts = toNamedContexts(t.Contexts) + return nil +} + func defaultTarget() *Target { return &Target{} } diff --git a/bake/bake_test.go b/bake/bake_test.go index 15a3333d2a0d..c09c4b8dad20 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -46,7 +46,7 @@ target "webapp" { require.Equal(t, 1, len(m)) require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile) - require.Equal(t, ".", *m["webapp"].Context) + require.Equal(t, ".", *m["webapp"].ContextPath()) require.Equal(t, ptrstr("webDEP"), m["webapp"].Args["VAR_INHERITED"]) require.Equal(t, true, *m["webapp"].NoCache) require.Equal(t, "128m", *m["webapp"].ShmSize) @@ -79,7 +79,7 @@ target "webapp" { require.NoError(t, err) require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile) - require.Equal(t, ".", *m["webapp"].Context) + require.Equal(t, ".", *m["webapp"].ContextPath()) _, isSet := m["webapp"].Args["VAR_UNSET"] require.False(t, isSet, m["webapp"].Args["VAR_UNSET"]) @@ -121,7 +121,7 @@ target "webapp" { m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.context=foo"}, nil, &EntitlementConf{}) require.NoError(t, err) - require.Equal(t, "foo", *m["webapp"].Context) + require.Equal(t, "foo", *m["webapp"].ContextPath()) require.Equal(t, 1, len(g)) require.Equal(t, []string{"webapp"}, g["default"].Targets) }) @@ -518,7 +518,7 @@ services: require.True(t, ok) require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile) - require.Equal(t, ".", *m["webapp"].Context) + require.Equal(t, ".", *m["webapp"].ContextPath()) require.Equal(t, ptrstr("1"), m["webapp"].Args["buildno"]) require.Equal(t, ptrstr("12"), m["webapp"].Args["buildno2"]) @@ -579,7 +579,7 @@ services: _, 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, ".", *m["web_app"].ContextPath()) require.Equal(t, ptrstr("1"), m["web_app"].Args["buildno"]) require.Equal(t, ptrstr("12"), m["web_app"].Args["buildno2"]) @@ -610,7 +610,7 @@ func TestHCLContextCwdPrefix(t *testing.T) { 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, "cwd://foo", *m["app"].ContextPath()) assert.Equal(t, "foo/test", bo["app"].Inputs.DockerfilePath) assert.Equal(t, "foo", bo["app"].Inputs.ContextPath) } @@ -641,7 +641,7 @@ func TestHCLDockerfileCwdPrefix(t *testing.T) { 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, ".", *m["app"].ContextPath()) assert.Equal(t, filepath.Join(cwd, "Dockerfile.app"), bo["app"].Inputs.DockerfilePath) assert.Equal(t, ".", bo["app"].Inputs.ContextPath) } @@ -798,9 +798,9 @@ services: require.True(t, ok) require.Equal(t, "Dockerfile", *m["app1"].Dockerfile) - require.Equal(t, ".", *m["app1"].Context) + require.Equal(t, ".", *m["app1"].ContextPath()) require.Equal(t, "Dockerfile", *m["app2"].Dockerfile) - require.Equal(t, ".", *m["app2"].Context) + require.Equal(t, ".", *m["app2"].ContextPath()) } func TestReadContextFromTargetChain(t *testing.T) { @@ -1130,7 +1130,7 @@ services: require.Equal(t, 1, len(g)) require.Equal(t, []string{"image", "image-release"}, g["default"].Targets) require.Equal(t, 2, len(m)) - require.Equal(t, ".", *m["image"].Context) + require.Equal(t, ".", *m["image"].ContextPath()) require.Equal(t, 1, len(m["image-release"].Outputs)) require.Equal(t, "type=image,push=true", m["image-release"].Outputs[0].String()) @@ -1139,14 +1139,14 @@ services: require.Equal(t, 1, len(g)) require.Equal(t, []string{"image"}, g["default"].Targets) require.Equal(t, 1, len(m)) - require.Equal(t, ".", *m["image"].Context) + require.Equal(t, ".", *m["image"].ContextPath()) m, g, err = ReadTargets(ctx, []File{fjson}, []string{"default"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) require.Equal(t, 1, len(g)) require.Equal(t, []string{"image"}, g["default"].Targets) require.Equal(t, 1, len(m)) - require.Equal(t, ".", *m["image"].Context) + require.Equal(t, ".", *m["image"].ContextPath()) m, g, err = ReadTargets(ctx, []File{fyml}, []string{"default"}, nil, nil, &EntitlementConf{}) require.NoError(t, err) @@ -1172,7 +1172,7 @@ services: sort.Strings(g["default"].Targets) require.Equal(t, []string{"addon", "aws", "image"}, g["default"].Targets) require.Equal(t, 3, len(m)) - require.Equal(t, ".", *m["image"].Context) + require.Equal(t, ".", *m["image"].ContextPath()) require.Equal(t, "./Dockerfile", *m["addon"].Dockerfile) require.Equal(t, "./aws.Dockerfile", *m["aws"].Dockerfile) } @@ -1537,7 +1537,7 @@ target "f" { require.Equal(t, tt.targets, g["default"].Targets) require.Equal(t, tt.count, len(m)) - require.Equal(t, ".", *m["d"].Context) + require.Equal(t, ".", *m["d"].ContextPath()) require.Equal(t, "./testdockerfile", *m["d"].Dockerfile) }) } @@ -1571,7 +1571,7 @@ services: 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, "dir", *c.Targets[0].ContextPath()) require.Equal(t, "Dockerfile-alternate", *c.Targets[0].Dockerfile) } diff --git a/bake/compose.go b/bake/compose.go index 58cfc80c953d..07e66f43daa2 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -72,10 +72,10 @@ func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Conf return nil, errors.Wrapf(err, "invalid service name %q", targetName) } - var contextPathP *string + var contextPathP **string if s.Build.Context != "" { - contextPath := s.Build.Context - contextPathP = &contextPath + ptr := &s.Build.Context + contextPathP = &ptr } var dockerfilePathP *string if s.Build.Dockerfile != "" { diff --git a/bake/compose_test.go b/bake/compose_test.go index ebeb880e8b51..6983275b446f 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -65,11 +65,11 @@ secrets: return c.Targets[i].Name < c.Targets[j].Name }) require.Equal(t, "db", c.Targets[0].Name) - require.Equal(t, "db", *c.Targets[0].Context) + require.Equal(t, "db", *c.Targets[0].ContextPath()) require.Equal(t, []string{"docker.io/tonistiigi/db"}, c.Targets[0].Tags) require.Equal(t, "webapp", c.Targets[1].Name) - require.Equal(t, "dir", *c.Targets[1].Context) + require.Equal(t, "dir", *c.Targets[1].ContextPath()) require.Equal(t, map[string]string{"foo": "bar"}, c.Targets[1].Contexts) require.Equal(t, "Dockerfile-alternate", *c.Targets[1].Dockerfile) require.Equal(t, 1, len(c.Targets[1].Args)) @@ -84,7 +84,7 @@ secrets: }, stringify(c.Targets[1].Secrets)) require.Equal(t, "webapp2", c.Targets[2].Name) - require.Equal(t, "dir", *c.Targets[2].Context) + require.Equal(t, "dir", *c.Targets[2].ContextPath()) require.Equal(t, "FROM alpine\n", *c.Targets[2].DockerfileInline) } diff --git a/bake/hcl_test.go b/bake/hcl_test.go index fd58b4460a98..3d522f6de4bf 100644 --- a/bake/hcl_test.go +++ b/bake/hcl_test.go @@ -54,7 +54,7 @@ func TestHCLBasic(t *testing.T) { 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, "./db", *c.Targets[0].ContextPath()) require.Equal(t, "webapp", c.Targets[1].Name) require.Equal(t, 1, len(c.Targets[1].Args)) @@ -114,7 +114,7 @@ func TestHCLBasicInJSON(t *testing.T) { 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, "./db", *c.Targets[0].ContextPath()) require.Equal(t, "webapp", c.Targets[1].Name) require.Equal(t, 1, len(c.Targets[1].Args)) @@ -516,11 +516,11 @@ func TestHCLTargetAttrs(t *testing.T) { 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, "yyy", *c.Targets[0].ContextPath()) 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].ContextPath()) require.Equal(t, "yyy", *c.Targets[1].Target) } @@ -576,8 +576,7 @@ func TestHCLTargetAttrEmptyChain(t *testing.T) { 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].ContextPath()) require.Nil(t, c.Targets[0].Target) } @@ -963,7 +962,7 @@ func TestHCLRenameMultiFile(t *testing.T) { require.Equal(t, "z", *c.Targets[0].Target) require.Equal(t, "foo", c.Targets[1].Name) - require.Equal(t, "y", *c.Targets[1].Context) + require.Equal(t, "y", *c.Targets[1].ContextPath()) } func TestHCLMatrixBasic(t *testing.T) { @@ -1365,7 +1364,7 @@ services: 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, "dir", *c.Targets[0].ContextPath()) require.Equal(t, "Dockerfile-alternate", *c.Targets[0].Dockerfile) } @@ -1386,7 +1385,7 @@ func TestHCLBuiltinVars(t *testing.T) { 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, "foo", *c.Targets[0].ContextPath()) require.Equal(t, "test", *c.Targets[0].Dockerfile) } @@ -1458,11 +1457,11 @@ target "b" { 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, ".", *c.Targets[2].ContextPath()) 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, ".", *c.Targets[3].ContextPath()) require.Equal(t, "b", *c.Targets[3].Target) } diff --git a/tests/bake.go b/tests/bake.go index a4b0b3a578d9..4eeb607e793c 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -38,6 +38,7 @@ func bakeCmd(sb integration.Sandbox, opts ...cmdOpt) (string, error) { var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakePrint, testBakePrintSensitive, + testBakePrintNullContext, testBakeLocal, testBakeLocalMulti, testBakeRemote, @@ -140,7 +141,7 @@ RUN echo "Hello ${HELLO}" require.Equal(t, []string{"build"}, def.Group["default"].Targets) require.Len(t, def.Target, 1) require.Contains(t, def.Target, "build") - require.Equal(t, ".", *def.Target["build"].Context) + require.Equal(t, ".", *def.Target["build"].ContextPath()) require.Equal(t, "Dockerfile", *def.Target["build"].Dockerfile) require.Equal(t, map[string]*string{"HELLO": ptrstr("foo")}, def.Target["build"].Args) @@ -245,7 +246,7 @@ RUN echo "Hello ${HELLO}" require.Equal(t, []string{"build"}, def.Group["default"].Targets) require.Len(t, def.Target, 1) require.Contains(t, def.Target, "build") - require.Equal(t, ".", *def.Target["build"].Context) + require.Equal(t, ".", *def.Target["build"].ContextPath()) require.Equal(t, "Dockerfile", *def.Target["build"].Dockerfile) require.Equal(t, map[string]*string{"HELLO": ptrstr("foo")}, def.Target["build"].Args) require.NotNil(t, def.Target["build"].CacheFrom) @@ -286,6 +287,90 @@ RUN echo "Hello ${HELLO}" } } +func testBakePrintNullContext(t *testing.T, sb integration.Sandbox) { + testCases := []struct { + name string + f string + dt []byte + }{ + { + "HCL", + "docker-bake.hcl", + []byte(` +target "build" { + context = null +} +`), + }, + { + "JSON", + "docker-bake.json", + []byte(` +{ + "target": { + "build": { + "context": null + } + } +} +`), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dir := tmpdir( + t, + fstest.CreateFile(tc.f, tc.dt, 0600), + fstest.CreateFile("Dockerfile", []byte(` +FROM busybox +ARG HELLO +RUN echo "Hello ${HELLO}" + `), 0600), + ) + + cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--print", "build")) + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + cmd.Stdout = &stdout + cmd.Stderr = &stderr + require.NoError(t, cmd.Run(), stdout.String(), stderr.String()) + + var def struct { + Group map[string]*bake.Group `json:"group,omitempty"` + Target map[string]*bake.Target `json:"target"` + } + require.NoError(t, json.Unmarshal(stdout.Bytes(), &def)) + + require.Len(t, def.Group, 1) + require.Contains(t, def.Group, "default") + + require.Equal(t, []string{"build"}, def.Group["default"].Targets) + require.Len(t, def.Target, 1) + require.Contains(t, def.Target, "build") + require.Nil(t, def.Target["build"].Context) + require.Equal(t, "Dockerfile", *def.Target["build"].Dockerfile) + + require.JSONEq(t, `{ + "group": { + "default": { + "targets": [ + "build" + ] + } + }, + "target": { + "build": { + "context": null, + "dockerfile": "Dockerfile" + } + } +} +`, stdout.String()) + }) + } +} + func testBakeLocal(t *testing.T, sb integration.Sandbox) { dockerfile := []byte(` FROM scratch @@ -871,6 +956,7 @@ target "default" { }) } } + func testBakeSetNonExistingOutsideNoParallel(t *testing.T, sb integration.Sandbox) { for _, ent := range []bool{true, false} { t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {