diff --git a/build/build.go b/build/build.go index 7e354ec8a316..daf690c04b37 100644 --- a/build/build.go +++ b/build/build.go @@ -607,16 +607,45 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[ // Capture the error from this build function. defer catchFrontendError(&retErr, &frontendErr) + rKey := resultKey(dp, k) + children := childTargets[rKey] + + // A call method replies with the metadata of the subrequest + // instead of a build result, so there would be nothing to link + // into the targets using this one as a named context. Solve the + // build request on its own in that case and register its result + // for them. It is never evaluated, so this only resolves the + // definition and does not build anything. + linked := false + if opt.CallFunc != nil && len(children) > 0 { + linkReq := req + linkReq.FrontendOpt = maps.Clone(req.FrontendOpt) + // checks are reported by the call method below, skip them + // here so that this request is not rejected by a Dockerfile + // that asks for check violations to be errors + linkReq.FrontendOpt["build-arg:BUILDKIT_DOCKERFILE_CHECK"] = "skip=all;error=false" + res, err := solve(ctx, c, linkReq) + if err != nil { + return nil, err + } + results.Set(rKey, res) + linked = true + } + + solveReq := req if opt.CallFunc != nil { - if _, ok := req.FrontendOpt["frontend.caps"]; !ok { - req.FrontendOpt["frontend.caps"] = "moby.buildkit.frontend.subrequests+forward" + // keep the initial request untouched, it may have already + // been sent for the linked result above + solveReq.FrontendOpt = maps.Clone(req.FrontendOpt) + if _, ok := solveReq.FrontendOpt["frontend.caps"]; !ok { + solveReq.FrontendOpt["frontend.caps"] = "moby.buildkit.frontend.subrequests+forward" } else { - req.FrontendOpt["frontend.caps"] += ",moby.buildkit.frontend.subrequests+forward" + solveReq.FrontendOpt["frontend.caps"] += ",moby.buildkit.frontend.subrequests+forward" } - req.FrontendOpt["requestid"] = "frontend." + opt.CallFunc.Name + solveReq.FrontendOpt["requestid"] = "frontend." + opt.CallFunc.Name } - res, err := solve(ctx, c, req) + res, err := solve(ctx, c, solveReq) if err != nil { return nil, err } @@ -625,11 +654,12 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[ callRes = res.Metadata } - rKey := resultKey(dp, k) - results.Set(rKey, res) + if !linked { + results.Set(rKey, res) + } forceEval := false - if children := childTargets[rKey]; len(children) > 0 { + if len(children) > 0 { // wait for the child targets to register their LLB before evaluating _, err := results.Get(ctx, children...) if err != nil { diff --git a/tests/bake.go b/tests/bake.go index ae26e64bf4b2..b850b0fca2c3 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -83,6 +83,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakeListVariables, testBakeListTypedVariables, testBakeCallCheck, + testBakeCallCheckLinkedTargets, testBakeCallCheckFlag, testBakeCallMetadata, testBakeMultiPlatform, @@ -2450,6 +2451,63 @@ target "validate" { require.Contains(t, out, "ConsistentInstructionCasing") } +func testBakeCallCheckLinkedTargets(t *testing.T, sb integration.Sandbox) { + dockerfileBuilder := []byte(` +FROM scratch +COPY foo /foo + `) + dockerfileBase := []byte(` +FROM builder +COPY foo /bar + `) + dockerfileApp := []byte(` +FROM base +COPy foo /baz + `) + bakefile := []byte(` +target "builder" { + dockerfile = "builder.Dockerfile" +} + +target "base" { + dockerfile = "base.Dockerfile" + contexts = { + builder = "target:builder" + } +} + +target "app" { + dockerfile = "app.Dockerfile" + contexts = { + base = "target:base" + } +} +`) + dir := tmpdir( + t, + fstest.CreateFile("docker-bake.hcl", bakefile, 0600), + fstest.CreateFile("builder.Dockerfile", dockerfileBuilder, 0600), + fstest.CreateFile("base.Dockerfile", dockerfileBase, 0600), + fstest.CreateFile("app.Dockerfile", dockerfileApp, 0600), + fstest.CreateFile("foo", []byte("foo"), 0600), + ) + + out, err := bakeCmd( + sb, + withDir(dir), + withArgs("app", "--check"), + ) + require.Error(t, err, out) + + // the "target:" contexts must be resolved for the call method instead of + // being forwarded to the frontend + require.NotContains(t, out, "unsupported context source target") + + // the requested target and the ones it links to are all checked + require.Equal(t, 3, strings.Count(out, "Check complete"), out) + require.Contains(t, out, "ConsistentInstructionCasing") +} + func testBakeCallCheckFlag(t *testing.T, sb integration.Sandbox) { dockerfile := []byte(` FROM scratch