From dbd8208b1b91cb157120a353287e8a2667f4d4fd Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 13 May 2024 10:32:03 -0700 Subject: [PATCH] dockerfile: detect base image with wrong platform being used Signed-off-by: Tonis Tiigi --- frontend/dockerfile/dockerfile2llb/convert.go | 10 +++ frontend/dockerfile/dockerfile_lint_test.go | 13 +++- frontend/dockerfile/dockerfile_test.go | 74 +++++++++++++++++++ frontend/dockerfile/linter/ruleset.go | 7 ++ 4 files changed, 101 insertions(+), 3 deletions(-) diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 3f0282c21a3fe..c05e7244f1d9a 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -529,6 +529,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS llb.WithCustomName(prefixCommand(d, "FROM "+d.stage.BaseName, opt.MultiPlatformRequested, platform, nil)), location(opt.SourceMap, d.stage.Location), ) + validateBaseImagePlatform(origName, *platform, d.image.Platform, d.stage.Location, opt.Warn) } d.platform = platform return nil @@ -2178,3 +2179,12 @@ func validateUsedOnce(c instructions.Command, loc *instructionTracker, warn lint } loc.MarkUsed(c.Location()) } + +func validateBaseImagePlatform(name string, expected, actual ocispecs.Platform, location []parser.Range, warn linter.LintWarnFunc) { + if expected.OS != actual.OS || expected.Architecture != actual.Architecture { + expectedStr := platforms.Format(platforms.Normalize(expected)) + actualStr := platforms.Format(platforms.Normalize(actual)) + msg := linter.RuleInvalidBaseImagePlatform.Format(name, expectedStr, actualStr) + linter.RuleInvalidBaseImagePlatform.Run(warn, location, msg) + } +} diff --git a/frontend/dockerfile/dockerfile_lint_test.go b/frontend/dockerfile/dockerfile_lint_test.go index 186b63ad88e18..6a5df9f55e95d 100644 --- a/frontend/dockerfile/dockerfile_lint_test.go +++ b/frontend/dockerfile/dockerfile_lint_test.go @@ -36,6 +36,7 @@ var lintTests = integration.TestFuncs( testWorkdirRelativePath, testUnmatchedVars, testMultipleInstructionsDisallowed, + testBaseImagePlatformMismatch, ) func testStageName(t *testing.T, sb integration.Sandbox) { @@ -706,10 +707,15 @@ func checkProgressStream(t *testing.T, sb integration.Sandbox, lintTest *lintTes f := getFrontend(t, sb) - _, err := f.Solve(sb.Context(), lintTest.Client, client.SolveOpt{ - FrontendAttrs: map[string]string{ + attrs := lintTest.FrontendAttrs + if attrs == nil { + attrs = map[string]string{ "platform": "linux/amd64,linux/arm64", - }, + } + } + + _, err := f.Solve(sb.Context(), lintTest.Client, client.SolveOpt{ + FrontendAttrs: attrs, LocalMounts: map[string]fsutil.FS{ dockerui.DefaultLocalNameDockerfile: lintTest.TmpDir, dockerui.DefaultLocalNameContext: lintTest.TmpDir, @@ -821,4 +827,5 @@ type lintTestParams struct { StreamBuildErr string UnmarshalBuildErr string BuildErrLocation int32 + FrontendAttrs map[string]string } diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go index 1e90e1a9ee72f..d16cf22089858 100644 --- a/frontend/dockerfile/dockerfile_test.go +++ b/frontend/dockerfile/dockerfile_test.go @@ -7073,6 +7073,80 @@ func testSourcePolicyWithNamedContext(t *testing.T, sb integration.Sandbox) { require.Equal(t, "foo", string(dt)) } +func testBaseImagePlatformMismatch(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush) + ctx := sb.Context() + + c, err := client.New(ctx, sb.Address()) + require.NoError(t, err) + defer c.Close() + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + f := getFrontend(t, sb) + + dockerfile := []byte(` +FROM scratch +COPY foo /foo +`) + dir := integration.Tmpdir( + t, + fstest.CreateFile("Dockerfile", dockerfile, 0600), + fstest.CreateFile("foo", []byte("test"), 0644), + ) + + // choose target platform that is different from the current platform + targetPlatform := runtime.GOOS + "/arm64" + if runtime.GOARCH == "arm64" { + targetPlatform = runtime.GOOS + "/amd64" + } + + target := registry + "/buildkit/testbaseimageplatform:latest" + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir, + dockerui.DefaultLocalNameContext: dir, + }, + FrontendAttrs: map[string]string{ + "platform": targetPlatform, + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dockerfile = []byte(fmt.Sprintf(` +FROM %s +ENV foo=bar + `, target)) + + checkLinterWarnings(t, sb, &lintTestParams{ + Dockerfile: dockerfile, + Warnings: []expectedLintWarning{ + { + RuleName: "InvalidBaseImagePlatform", + Description: "Base image platform does not match expected target platform", + Detail: fmt.Sprintf("Base image %s was pulled with platform %q, expected %q for current build", target, targetPlatform, runtime.GOOS+"/"+runtime.GOARCH), + Level: 1, + Line: 2, + }, + }, + FrontendAttrs: map[string]string{}, + }) +} + func runShell(dir string, cmds ...string) error { for _, args := range cmds { var cmd *exec.Cmd diff --git a/frontend/dockerfile/linter/ruleset.go b/frontend/dockerfile/linter/ruleset.go index 3c15dccab12d9..ca16317048ae4 100644 --- a/frontend/dockerfile/linter/ruleset.go +++ b/frontend/dockerfile/linter/ruleset.go @@ -105,4 +105,11 @@ var ( return fmt.Sprintf("Multiple %s instructions should not be used in the same stage because only the last one will be used", instructionName) }, } + RuleInvalidBaseImagePlatform = LinterRule[func(string, string, string) string]{ + Name: "InvalidBaseImagePlatform", + Description: "Base image platform does not match expected target platform", + Format: func(image, expected, actual string) string { + return fmt.Sprintf("Base image %s was pulled with platform %q, expected %q for current build", image, actual, expected) + }, + } )