Skip to content

Commit

Permalink
dockerfile: detect base image with wrong platform being used
Browse files Browse the repository at this point in the history
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
  • Loading branch information
tonistiigi committed May 28, 2024
1 parent 4cf5e34 commit dbd8208
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 3 deletions.
10 changes: 10 additions & 0 deletions frontend/dockerfile/dockerfile2llb/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
13 changes: 10 additions & 3 deletions frontend/dockerfile/dockerfile_lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var lintTests = integration.TestFuncs(
testWorkdirRelativePath,
testUnmatchedVars,
testMultipleInstructionsDisallowed,
testBaseImagePlatformMismatch,
)

func testStageName(t *testing.T, sb integration.Sandbox) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -821,4 +827,5 @@ type lintTestParams struct {
StreamBuildErr string
UnmarshalBuildErr string
BuildErrLocation int32
FrontendAttrs map[string]string
}
74 changes: 74 additions & 0 deletions frontend/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions frontend/dockerfile/linter/ruleset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}
)

0 comments on commit dbd8208

Please sign in to comment.