diff --git a/frontend/dockerfile/builder/build.go b/frontend/dockerfile/builder/build.go index 7f7e8b42..18d175ce 100644 --- a/frontend/dockerfile/builder/build.go +++ b/frontend/dockerfile/builder/build.go @@ -462,7 +462,7 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) { } c.Warn(ctx, defVtx, msg, warnOpts(sourceMap, location, detail, url)) }, - ContextByName: contextByName(c, tp), + ContextByName: contextByNameFunc(c, tp), }) if err != nil { @@ -804,7 +804,7 @@ func warnOpts(sm *llb.SourceMap, r *parser.Range, detail [][]byte, url string) c return opts } -func contextByName(c client.Client, p *ocispecs.Platform) func(context.Context, string) (*llb.State, *dockerfile2llb.Image, error) { +func contextByNameFunc(c client.Client, p *ocispecs.Platform) func(context.Context, string) (*llb.State, *dockerfile2llb.Image, error) { return func(ctx context.Context, name string) (*llb.State, *dockerfile2llb.Image, error) { named, err := reference.ParseNormalizedNamed(name) if err != nil { @@ -812,68 +812,82 @@ func contextByName(c client.Client, p *ocispecs.Platform) func(context.Context, } name = strings.TrimSuffix(reference.FamiliarString(named), ":latest") - opts := c.BuildOpts().Opts - v, ok := opts["context:"+name] - if !ok { - return nil, nil, nil - } - - vv := strings.SplitN(v, ":", 2) - if len(vv) != 2 { - return nil, nil, errors.Errorf("invalid context specifier %s for %s", v, name) - } - switch vv[0] { - case "docker-image": - st := llb.Image(strings.TrimPrefix(vv[1], "//"), llb.WithCustomName("[context "+name+"] "+vv[1]), llb.WithMetaResolver(c)) - return &st, nil, nil - case "git": - st, ok := detectGitContext(v, "") - if !ok { - return nil, nil, errors.Errorf("invalid git context %s", v) - } - return st, nil, nil - case "http", "https": - st, ok := detectGitContext(v, "") - if !ok { - httpst := llb.HTTP(v, llb.WithCustomName("[context "+name+"] "+v)) - st = &httpst - } - return st, nil, nil - case "local": - st := llb.Local(vv[1], llb.WithCustomName("[context "+name+"] load from client"), llb.SessionID(c.BuildOpts().SessionID), llb.SharedKeyHint("context:"+name)) - return &st, nil, nil - case "input": - inputs, err := c.Inputs(ctx) + if p != nil { + name := name + "::" + platforms.Format(platforms.Normalize(*p)) + st, img, err := contextByName(ctx, c, name) if err != nil { return nil, nil, err } - st, ok := inputs[vv[1]] - if !ok { - return nil, nil, errors.Errorf("invalid input %s for %s", vv[1], name) + if st != nil { + return st, img, nil } - md, ok := opts["input-metadata:"+vv[1]] - if ok { - m := make(map[string][]byte) - if err := json.Unmarshal([]byte(md), &m); err != nil { - return nil, nil, errors.Wrapf(err, "failed to parse input metadata %s", md) - } - dt, ok := m["containerimage.config"] - if ok { - st, err = st.WithImageConfig([]byte(dt)) - if err != nil { - return nil, nil, err - } - var img dockerfile2llb.Image - if err := json.Unmarshal(dt, &img); err != nil { - return nil, nil, errors.Wrapf(err, "failed to parse image config for %s", name) - } - return &st, &img, nil - } - } - return &st, nil, nil - default: - return nil, nil, errors.Errorf("unsupported context source %s for %s", vv[0], name) } + return contextByName(ctx, c, name) + } +} + +func contextByName(ctx context.Context, c client.Client, name string) (*llb.State, *dockerfile2llb.Image, error) { + opts := c.BuildOpts().Opts + v, ok := opts["context:"+name] + if !ok { + return nil, nil, nil + } + + vv := strings.SplitN(v, ":", 2) + if len(vv) != 2 { + return nil, nil, errors.Errorf("invalid context specifier %s for %s", v, name) + } + switch vv[0] { + case "docker-image": + st := llb.Image(strings.TrimPrefix(vv[1], "//"), llb.WithCustomName("[context "+name+"] "+vv[1]), llb.WithMetaResolver(c)) + return &st, nil, nil + case "git": + st, ok := detectGitContext(v, "1") + if !ok { + return nil, nil, errors.Errorf("invalid git context %s", v) + } + return st, nil, nil + case "http", "https": + st, ok := detectGitContext(v, "1") + if !ok { + httpst := llb.HTTP(v, llb.WithCustomName("[context "+name+"] "+v)) + st = &httpst + } + return st, nil, nil + case "local": + st := llb.Local(vv[1], llb.WithCustomName("[context "+name+"] load from client"), llb.SessionID(c.BuildOpts().SessionID), llb.SharedKeyHint("context:"+name)) + return &st, nil, nil + case "input": + inputs, err := c.Inputs(ctx) + if err != nil { + return nil, nil, err + } + st, ok := inputs[vv[1]] + if !ok { + return nil, nil, errors.Errorf("invalid input %s for %s", vv[1], name) + } + md, ok := opts["input-metadata:"+vv[1]] + if ok { + m := make(map[string][]byte) + if err := json.Unmarshal([]byte(md), &m); err != nil { + return nil, nil, errors.Wrapf(err, "failed to parse input metadata %s", md) + } + dt, ok := m["containerimage.config"] + if ok { + st, err = st.WithImageConfig([]byte(dt)) + if err != nil { + return nil, nil, err + } + var img dockerfile2llb.Image + if err := json.Unmarshal(dt, &img); err != nil { + return nil, nil, errors.Wrapf(err, "failed to parse image config for %s", name) + } + return &st, &img, nil + } + } + return &st, nil, nil + default: + return nil, nil, errors.Errorf("unsupported context source %s for %s", vv[0], name) } } diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go index fa67fa5e..3aaa9f0e 100644 --- a/frontend/dockerfile/dockerfile_test.go +++ b/frontend/dockerfile/dockerfile_test.go @@ -122,6 +122,7 @@ var allTests = integration.TestFuncs( testNamedImageContext, testNamedLocalContext, testNamedInputContext, + testNamedMultiplatformInputContext, ) var fileOpTests = integration.TestFuncs( @@ -5627,6 +5628,155 @@ COPY --from=build /foo /out / require.Equal(t, "foo is bar\n", string(dt)) } +func testNamedMultiplatformInputContext(t *testing.T, sb integration.Sandbox) { + ctx := sb.Context() + + c, err := client.New(ctx, sb.Address()) + require.NoError(t, err) + defer c.Close() + + dockerfile := []byte(` +FROM --platform=$BUILDPLATFORM alpine +ARG TARGETARCH +ENV FOO=bar-$TARGETARCH +RUN echo "foo $TARGETARCH" > /out +`) + + dir, err := tmpdir( + fstest.CreateFile("Dockerfile", dockerfile, 0600), + ) + require.NoError(t, err) + defer os.RemoveAll(dir) + + dockerfile2 := []byte(` +FROM base AS build +RUN echo "foo is $FOO" > /foo +FROM scratch +COPY --from=build /foo /out / +`) + + dir2, err := tmpdir( + fstest.CreateFile("Dockerfile", dockerfile2, 0600), + ) + require.NoError(t, err) + defer os.RemoveAll(dir) + + f := getFrontend(t, sb) + + b := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res, err := f.SolveGateway(ctx, c, gateway.SolveRequest{ + FrontendOpt: map[string]string{ + "platform": "linux/amd64,linux/arm64", + }, + }) + if err != nil { + return nil, err + } + + if len(res.Refs) != 2 { + return nil, errors.Errorf("expected 2 refs, got %d", len(res.Refs)) + } + + inputs := map[string]*pb.Definition{} + st, err := res.Refs["linux/amd64"].ToState() + if err != nil { + return nil, err + } + def, err := st.Marshal(ctx) + if err != nil { + return nil, err + } + inputs["base::linux/amd64"] = def.ToPB() + + st, err = res.Refs["linux/arm64"].ToState() + if err != nil { + return nil, err + } + def, err = st.Marshal(ctx) + if err != nil { + return nil, err + } + inputs["base::linux/arm64"] = def.ToPB() + + frontendOpt := map[string]string{ + "dockerfilekey": builder.DefaultLocalNameDockerfile + "2", + "context:base::linux/amd64": "input:base::linux/amd64", + "context:base::linux/arm64": "input:base::linux/arm64", + "platform": "linux/amd64,linux/arm64", + } + + dt, ok := res.Metadata["containerimage.config/linux/amd64"] + if !ok { + return nil, errors.Errorf("no containerimage.config in metadata") + } + dt, err = json.Marshal(map[string][]byte{ + "containerimage.config": dt, + }) + if err != nil { + return nil, err + } + frontendOpt["input-metadata:base::linux/amd64"] = string(dt) + + dt, ok = res.Metadata["containerimage.config/linux/arm64"] + if !ok { + return nil, errors.Errorf("no containerimage.config in metadata") + } + dt, err = json.Marshal(map[string][]byte{ + "containerimage.config": dt, + }) + if err != nil { + return nil, err + } + frontendOpt["input-metadata:base::linux/arm64"] = string(dt) + + res, err = f.SolveGateway(ctx, c, gateway.SolveRequest{ + FrontendOpt: frontendOpt, + FrontendInputs: inputs, + }) + if err != nil { + return nil, err + } + return res, nil + } + + product := "buildkit_test" + + destDir, err := ioutil.TempDir("", "buildkit") + require.NoError(t, err) + defer os.RemoveAll(destDir) + + _, err = c.Build(ctx, client.SolveOpt{ + LocalDirs: map[string]string{ + builder.DefaultLocalNameDockerfile: dir, + builder.DefaultLocalNameContext: dir, + builder.DefaultLocalNameDockerfile + "2": dir2, + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: destDir, + }, + }, + }, product, b, nil) + require.NoError(t, err) + + dt, err := ioutil.ReadFile(filepath.Join(destDir, "linux_amd64/out")) + require.NoError(t, err) + require.Equal(t, "foo amd64\n", string(dt)) + + dt, err = ioutil.ReadFile(filepath.Join(destDir, "linux_amd64/foo")) + require.NoError(t, err) + require.Equal(t, "foo is bar-amd64\n", string(dt)) + + dt, err = ioutil.ReadFile(filepath.Join(destDir, "linux_arm64/out")) + require.NoError(t, err) + require.Equal(t, "foo arm64\n", string(dt)) + + dt, err = ioutil.ReadFile(filepath.Join(destDir, "linux_arm64/foo")) + require.NoError(t, err) + require.Equal(t, "foo is bar-arm64\n", string(dt)) +} + func tmpdir(appliers ...fstest.Applier) (string, error) { tmpdir, err := ioutil.TempDir("", "buildkit-dockerfile") if err != nil {