package dockerfile import ( "context" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/containerd/continuity/fs/fstest" "github.com/moby/buildkit/client" "github.com/moby/buildkit/exporter/containerimage/exptypes" "github.com/moby/buildkit/frontend/dockerfile/builder" gateway "github.com/moby/buildkit/frontend/gateway/client" "github.com/moby/buildkit/solver/pb" binfotypes "github.com/moby/buildkit/util/buildinfo/types" "github.com/moby/buildkit/util/testutil/integration" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var buildinfoTests = integration.TestFuncs( testBuildInfoSources, testBuildInfoAttrs, testBuildInfoMultiPlatform, testBuildInfoDeps, testBuildInfoDepsMultiPlatform, ) func init() { allTests = append(allTests, buildinfoTests...) } // moby/buildkit#2311 func testBuildInfoSources(t *testing.T, sb integration.Sandbox) { f := getFrontend(t, sb) gitDir, err := ioutil.TempDir("", "buildkit") require.NoError(t, err) defer os.RemoveAll(gitDir) dockerfile := ` FROM alpine:latest@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300 AS alpine FROM busybox:latest ADD https://raw.githubusercontent.com/moby/moby/master/README.md / COPY --from=alpine /bin/busybox /alpine-busybox ` err = ioutil.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte(dockerfile), 0600) require.NoError(t, err) err = runShell(gitDir, "git init", "git config --local user.email test", "git config --local user.name test", "git add Dockerfile", "git commit -m initial", "git branch buildinfo", "git update-server-info", ) require.NoError(t, err) server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(gitDir)))) defer server.Close() destDir, err := ioutil.TempDir("", "buildkit") require.NoError(t, err) defer os.RemoveAll(destDir) out := filepath.Join(destDir, "out.tar") outW, err := os.Create(out) require.NoError(t, err) c, err := client.New(sb.Context(), sb.Address()) require.NoError(t, err) defer c.Close() res, err := f.Solve(sb.Context(), c, client.SolveOpt{ Exports: []client.ExportEntry{ { Type: client.ExporterOCI, Output: fixedWriteCloser(outW), }, }, FrontendAttrs: map[string]string{ builder.DefaultLocalNameContext: server.URL + "/.git#buildinfo", }, }, nil) require.NoError(t, err) require.Contains(t, res.ExporterResponse, exptypes.ExporterBuildInfo) dtbi, err := base64.StdEncoding.DecodeString(res.ExporterResponse[exptypes.ExporterBuildInfo]) require.NoError(t, err) var bi binfotypes.BuildInfo err = json.Unmarshal(dtbi, &bi) require.NoError(t, err) sources := bi.Sources require.Equal(t, 3, len(sources)) assert.Equal(t, binfotypes.SourceTypeDockerImage, sources[0].Type) assert.Equal(t, "docker.io/library/alpine:latest@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300", sources[0].Ref) assert.Equal(t, "sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300", sources[0].Pin) assert.Equal(t, binfotypes.SourceTypeDockerImage, sources[1].Type) assert.Equal(t, "docker.io/library/busybox:latest", sources[1].Ref) assert.NotEmpty(t, sources[1].Pin) assert.Equal(t, binfotypes.SourceTypeHTTP, sources[2].Type) assert.Equal(t, "https://raw.githubusercontent.com/moby/moby/master/README.md", sources[2].Ref) assert.Equal(t, "sha256:419455202b0ef97e480d7f8199b26a721a417818bc0e2d106975f74323f25e6c", sources[2].Pin) } // moby/buildkit#2476 func testBuildInfoAttrs(t *testing.T, sb integration.Sandbox) { f := getFrontend(t, sb) f.RequiresBuildctl(t) dockerfile := ` FROM busybox:latest ARG foo RUN echo $foo ` dir, err := tmpdir( fstest.CreateFile("Dockerfile", []byte(dockerfile), 0600), ) require.NoError(t, err) defer os.RemoveAll(dir) c, err := client.New(sb.Context(), sb.Address()) require.NoError(t, err) defer c.Close() destDir, err := ioutil.TempDir("", "buildkit") require.NoError(t, err) defer os.RemoveAll(destDir) out := filepath.Join(destDir, "out.tar") outW, err := os.Create(out) require.NoError(t, err) res, err := f.Solve(sb.Context(), c, client.SolveOpt{ FrontendAttrs: map[string]string{ "build-arg:foo": "bar", }, Exports: []client.ExportEntry{ { Type: client.ExporterOCI, Output: fixedWriteCloser(outW), }, }, LocalDirs: map[string]string{ builder.DefaultLocalNameDockerfile: dir, builder.DefaultLocalNameContext: dir, }, }, nil) require.NoError(t, err) require.Contains(t, res.ExporterResponse, exptypes.ExporterBuildInfo) dtbi, err := base64.StdEncoding.DecodeString(res.ExporterResponse[exptypes.ExporterBuildInfo]) require.NoError(t, err) var bi binfotypes.BuildInfo err = json.Unmarshal(dtbi, &bi) require.NoError(t, err) require.Contains(t, bi.Attrs, "build-arg:foo") require.Equal(t, "bar", *bi.Attrs["build-arg:foo"]) } // moby/buildkit#2476 func testBuildInfoMultiPlatform(t *testing.T, sb integration.Sandbox) { f := getFrontend(t, sb) f.RequiresBuildctl(t) dockerfile := ` FROM busybox:latest ARG foo RUN echo $foo ADD https://raw.githubusercontent.com/moby/moby/master/README.md / ` dir, err := tmpdir( fstest.CreateFile("Dockerfile", []byte(dockerfile), 0600), ) require.NoError(t, err) defer os.RemoveAll(dir) c, err := client.New(sb.Context(), sb.Address()) require.NoError(t, err) defer c.Close() destDir, err := ioutil.TempDir("", "buildkit") require.NoError(t, err) defer os.RemoveAll(destDir) out := filepath.Join(destDir, "out.tar") outW, err := os.Create(out) require.NoError(t, err) platforms := []string{"linux/amd64", "linux/arm64"} res, err := f.Solve(sb.Context(), c, client.SolveOpt{ FrontendAttrs: map[string]string{ "build-arg:foo": "bar", "platform": strings.Join(platforms, ","), }, Exports: []client.ExportEntry{ { Type: client.ExporterOCI, Output: fixedWriteCloser(outW), }, }, LocalDirs: map[string]string{ builder.DefaultLocalNameDockerfile: dir, builder.DefaultLocalNameContext: dir, }, }, nil) require.NoError(t, err) for _, platform := range platforms { require.Contains(t, res.ExporterResponse, fmt.Sprintf("%s/%s", exptypes.ExporterBuildInfo, platform)) dtbi, err := base64.StdEncoding.DecodeString(res.ExporterResponse[fmt.Sprintf("%s/%s", exptypes.ExporterBuildInfo, platform)]) require.NoError(t, err) var bi binfotypes.BuildInfo err = json.Unmarshal(dtbi, &bi) require.NoError(t, err) require.Contains(t, bi.Attrs, "build-arg:foo") require.Equal(t, "bar", *bi.Attrs["build-arg:foo"]) sources := bi.Sources require.Equal(t, 2, len(sources)) assert.Equal(t, binfotypes.SourceTypeDockerImage, sources[0].Type) assert.Equal(t, "docker.io/library/busybox:latest", sources[0].Ref) assert.NotEmpty(t, sources[0].Pin) assert.Equal(t, binfotypes.SourceTypeHTTP, sources[1].Type) assert.Equal(t, "https://raw.githubusercontent.com/moby/moby/master/README.md", sources[1].Ref) assert.Equal(t, "sha256:419455202b0ef97e480d7f8199b26a721a417818bc0e2d106975f74323f25e6c", sources[1].Pin) } } func testBuildInfoDeps(t *testing.T, sb integration.Sandbox) { ctx := sb.Context() f := getFrontend(t, sb) f.RequiresBuildctl(t) c, err := client.New(ctx, sb.Address()) require.NoError(t, err) defer c.Close() dockerfile := []byte(` FROM alpine ENV FOO=bar RUN echo first > /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 busybox COPY --from=build /foo /out / `) dir2, err := tmpdir( fstest.CreateFile("Dockerfile", dockerfile2, 0600), ) require.NoError(t, err) defer os.RemoveAll(dir) b := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { res, err := f.SolveGateway(ctx, c, gateway.SolveRequest{}) if err != nil { return nil, err } ref, err := res.SingleRef() if err != nil { return nil, err } st, err := ref.ToState() if err != nil { return nil, err } def, err := st.Marshal(ctx) if err != nil { return nil, err } dtic, ok := res.Metadata[exptypes.ExporterImageConfigKey] if !ok { return nil, errors.Errorf("no containerimage.config in metadata") } dtbi, ok := res.Metadata[exptypes.ExporterBuildInfo] if !ok { return nil, errors.Errorf("no containerimage.buildinfo in metadata") } dt, err := json.Marshal(map[string][]byte{ exptypes.ExporterImageConfigKey: dtic, exptypes.ExporterBuildInfo: dtbi, }) if err != nil { return nil, err } res, err = f.SolveGateway(ctx, c, gateway.SolveRequest{ FrontendOpt: map[string]string{ "dockerfilekey": builder.DefaultLocalNameDockerfile + "2", "context:base": "input:base", "input-metadata:base": string(dt), }, FrontendInputs: map[string]*pb.Definition{ "base": def.ToPB(), }, }) if err != nil { return nil, err } return res, nil } destDir, err := ioutil.TempDir("", "buildkit") require.NoError(t, err) defer os.RemoveAll(destDir) res, 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, }, }, }, "", b, nil) require.NoError(t, err) require.Contains(t, res.ExporterResponse, exptypes.ExporterBuildInfo) dtbi, err := base64.StdEncoding.DecodeString(res.ExporterResponse[exptypes.ExporterBuildInfo]) require.NoError(t, err) var bi binfotypes.BuildInfo err = json.Unmarshal(dtbi, &bi) require.NoError(t, err) require.Equal(t, 2, len(bi.Sources)) assert.Equal(t, binfotypes.SourceTypeDockerImage, bi.Sources[0].Type) assert.True(t, strings.HasPrefix(bi.Sources[0].Ref, "docker.io/library/alpine")) assert.NotEmpty(t, bi.Sources[0].Pin) assert.Equal(t, binfotypes.SourceTypeDockerImage, bi.Sources[1].Type) assert.Equal(t, "docker.io/library/busybox:latest", bi.Sources[1].Ref) assert.NotEmpty(t, bi.Sources[1].Pin) require.Contains(t, bi.Deps, "base") depsrc := bi.Deps["base"].Sources require.Equal(t, 1, len(depsrc)) assert.Equal(t, binfotypes.SourceTypeDockerImage, depsrc[0].Type) assert.Equal(t, "alpine", depsrc[0].Ref) assert.NotEmpty(t, depsrc[0].Pin) } func testBuildInfoDepsMultiPlatform(t *testing.T, sb integration.Sandbox) { ctx := sb.Context() f := getFrontend(t, sb) f.RequiresBuildctl(t) platforms := []string{"linux/amd64", "linux/arm64"} 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 busybox COPY --from=build /foo /out / `) dir2, err := tmpdir( fstest.CreateFile("Dockerfile", dockerfile2, 0600), ) require.NoError(t, err) defer os.RemoveAll(dir) b := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { res, err := f.SolveGateway(ctx, c, gateway.SolveRequest{ FrontendOpt: map[string]string{ "platform": strings.Join(platforms, ","), }, }) if err != nil { return nil, err } if len(res.Refs) != 2 { return nil, errors.Errorf("expected 2 refs, got %d", len(res.Refs)) } frontendOpt := map[string]string{ "dockerfilekey": builder.DefaultLocalNameDockerfile + "2", "platform": strings.Join(platforms, ","), } inputs := map[string]*pb.Definition{} for _, platform := range platforms { frontendOpt["context:base::"+platform] = "input:base::" + platform st, err := res.Refs[platform].ToState() if err != nil { return nil, err } def, err := st.Marshal(ctx) if err != nil { return nil, err } inputs["base::"+platform] = def.ToPB() dtic, ok := res.Metadata[exptypes.ExporterImageConfigKey+"/"+platform] if !ok { return nil, errors.Errorf("no containerimage.config/" + platform + " in metadata") } dtbi, ok := res.Metadata[exptypes.ExporterBuildInfo+"/"+platform] if !ok { return nil, errors.Errorf("no containerimage.buildinfo/" + platform + " in metadata") } dt, err := json.Marshal(map[string][]byte{ exptypes.ExporterImageConfigKey: dtic, exptypes.ExporterBuildInfo: dtbi, }) if err != nil { return nil, err } frontendOpt["input-metadata:base::"+platform] = string(dt) } res, err = f.SolveGateway(ctx, c, gateway.SolveRequest{ FrontendOpt: frontendOpt, FrontendInputs: inputs, }) if err != nil { return nil, err } return res, nil } destDir, err := ioutil.TempDir("", "buildkit") require.NoError(t, err) defer os.RemoveAll(destDir) res, 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, }, }, }, "", b, nil) require.NoError(t, err) for _, platform := range platforms { require.Contains(t, res.ExporterResponse, fmt.Sprintf("%s/%s", exptypes.ExporterBuildInfo, platform)) dtbi, err := base64.StdEncoding.DecodeString(res.ExporterResponse[fmt.Sprintf("%s/%s", exptypes.ExporterBuildInfo, platform)]) require.NoError(t, err) var bi binfotypes.BuildInfo err = json.Unmarshal(dtbi, &bi) require.NoError(t, err) require.Equal(t, 2, len(bi.Sources)) assert.Equal(t, binfotypes.SourceTypeDockerImage, bi.Sources[0].Type) assert.True(t, strings.HasPrefix(bi.Sources[0].Ref, "docker.io/library/alpine")) assert.NotEmpty(t, bi.Sources[0].Pin) assert.Equal(t, binfotypes.SourceTypeDockerImage, bi.Sources[1].Type) assert.Equal(t, "docker.io/library/busybox:latest", bi.Sources[1].Ref) assert.NotEmpty(t, bi.Sources[1].Pin) require.Contains(t, bi.Deps, "base") depsrc := bi.Deps["base"].Sources require.Equal(t, 1, len(depsrc)) assert.Equal(t, binfotypes.SourceTypeDockerImage, depsrc[0].Type) assert.Equal(t, "alpine", depsrc[0].Ref) assert.NotEmpty(t, depsrc[0].Pin) } }