Skip to content

Commit fe6a7dc

Browse files
committed
linter: add redundant target platform check
Adds a linter check that checks if `--platform=$TARGETPLATFORM` is used and sends a linter message marking this as redundant. This is because `$TARGETPLATFORM` is the default. Signed-off-by: Jonathan A. Sternberg <[email protected]>
1 parent aaaf86e commit fe6a7dc

File tree

7 files changed

+152
-10
lines changed

7 files changed

+152
-10
lines changed

frontend/dockerfile/dockerfile2llb/convert.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,8 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
289289

290290
// set base state for every image
291291
for i, st := range stages {
292-
nameMatch, err := shlex.ProcessWordWithMatches(st.BaseName, metaArgsToEnvs(optMetaArgs))
292+
env := metaArgsToEnvs(optMetaArgs)
293+
nameMatch, err := shlex.ProcessWordWithMatches(st.BaseName, env)
293294
reportUnusedFromArgs(metaArgsKeys(optMetaArgs), nameMatch.Unmatched, st.Location, lint)
294295
used := nameMatch.Matched
295296
if used == nil {
@@ -316,8 +317,10 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
316317
}
317318

318319
if v := st.Platform; v != "" {
319-
platMatch, err := shlex.ProcessWordWithMatches(v, metaArgsToEnvs(optMetaArgs))
320+
platEnv := metaArgsToEnvs(optMetaArgs)
321+
platMatch, err := shlex.ProcessWordWithMatches(v, platEnv)
320322
reportUnusedFromArgs(metaArgsKeys(optMetaArgs), platMatch.Unmatched, st.Location, lint)
323+
reportRedundantTargetPlatform(st.Platform, platMatch, st.Location, platEnv, lint)
321324

322325
if err != nil {
323326
return nil, parser.WithLocation(errors.Wrapf(err, "failed to process arguments for platform %s", platMatch.Result), st.Location)
@@ -2278,6 +2281,26 @@ func reportUnusedFromArgs(values []string, unmatched map[string]struct{}, locati
22782281
}
22792282
}
22802283

2284+
func reportRedundantTargetPlatform(platformVar string, nameMatch shell.ProcessWordResult, location []parser.Range, env shell.EnvGetter, lint *linter.Linter) {
2285+
// Only match this rule if there was only one matched name.
2286+
// It's psosible there were multiple args and that one of them expanded to an empty
2287+
// string and we don't want to report a warning when that happens.
2288+
if len(nameMatch.Matched) == 1 && len(nameMatch.Unmatched) == 0 {
2289+
const targetPlatform = "TARGETPLATFORM"
2290+
// If target platform is the only environment variable that was substituted and the result
2291+
// matches the target platform exactly, we can infer that the input was ${TARGETPLATFORM} or
2292+
// $TARGETPLATFORM.
2293+
if _, ok := nameMatch.Matched[targetPlatform]; !ok {
2294+
return
2295+
}
2296+
2297+
if result, _ := env.Get(targetPlatform); nameMatch.Result == result {
2298+
msg := linter.RuleRedundantTargetPlatform.Format(platformVar)
2299+
lint.Run(&linter.RuleRedundantTargetPlatform, location, msg)
2300+
}
2301+
}
2302+
}
2303+
22812304
type instructionTracker struct {
22822305
Loc []parser.Range
22832306
IsSet bool

frontend/dockerfile/dockerfile_lint_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var lintTests = integration.TestFuncs(
4040
testLegacyKeyValueFormat,
4141
testBaseImagePlatformMismatch,
4242
testAllTargetUnmarshal,
43+
testRedundantTargetPlatform,
4344
)
4445

4546
func testAllTargetUnmarshal(t *testing.T, sb integration.Sandbox) {
@@ -923,6 +924,42 @@ FROM a AS c
923924
})
924925
}
925926

927+
func testRedundantTargetPlatform(t *testing.T, sb integration.Sandbox) {
928+
dockerfile := []byte(`
929+
FROM --platform=$TARGETPLATFORM scratch
930+
`)
931+
checkLinterWarnings(t, sb, &lintTestParams{
932+
Dockerfile: dockerfile,
933+
Warnings: []expectedLintWarning{
934+
{
935+
RuleName: "RedundantTargetPlatform",
936+
Description: "Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior",
937+
URL: "https://docs.docker.com/go/dockerfile/rule/redundant-target-platform/",
938+
Detail: "Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior",
939+
Line: 2,
940+
Level: 1,
941+
},
942+
},
943+
})
944+
945+
dockerfile = []byte(`
946+
FROM --platform=${TARGETPLATFORM} scratch
947+
`)
948+
checkLinterWarnings(t, sb, &lintTestParams{
949+
Dockerfile: dockerfile,
950+
Warnings: []expectedLintWarning{
951+
{
952+
RuleName: "RedundantTargetPlatform",
953+
Description: "Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior",
954+
URL: "https://docs.docker.com/go/dockerfile/rule/redundant-target-platform/",
955+
Detail: "Setting platform to predefined ${TARGETPLATFORM} in FROM is redundant as this is the default behavior",
956+
Line: 2,
957+
Level: 1,
958+
},
959+
},
960+
})
961+
}
962+
926963
func checkUnmarshal(t *testing.T, sb integration.Sandbox, lintTest *lintTestParams) {
927964
destDir, err := os.MkdirTemp("", "buildkit")
928965
require.NoError(t, err)

frontend/dockerfile/dockerfile_test.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ var allTests = integration.TestFuncs(
122122
testErrorsSourceMap,
123123
testMultiArgs,
124124
testFrontendSubrequests,
125-
testDockefileCheckHostname,
125+
testDockerfileCheckHostname,
126126
testDefaultShellAndPath,
127127
testDockerfileLowercase,
128128
testExportCacheLoop,
@@ -199,8 +199,10 @@ var reproTests = integration.TestFuncs(
199199
testReproSourceDateEpoch,
200200
)
201201

202-
var opts []integration.TestOpt
203-
var securityOpts []integration.TestOpt
202+
var (
203+
opts []integration.TestOpt
204+
securityOpts []integration.TestOpt
205+
)
204206

205207
type frontend interface {
206208
Solve(context.Context, *client.Client, client.SolveOpt, chan *client.SolveStatus) (*client.SolveResponse, error)
@@ -4324,6 +4326,7 @@ COPY --from=base unique /
43244326
require.NoError(t, err)
43254327
require.Equal(t, string(dt), string(dt2))
43264328
}
4329+
43274330
func testCacheImportExport(t *testing.T, sb integration.Sandbox) {
43284331
integration.SkipOnPlatform(t, "windows")
43294332
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal)
@@ -5222,7 +5225,7 @@ COPY Dockerfile Dockerfile
52225225
}
52235226

52245227
// moby/buildkit#1301
5225-
func testDockefileCheckHostname(t *testing.T, sb integration.Sandbox) {
5228+
func testDockerfileCheckHostname(t *testing.T, sb integration.Sandbox) {
52265229
integration.SkipOnPlatform(t, "windows")
52275230
f := getFrontend(t, sb)
52285231
dockerfile := []byte(`
@@ -7433,6 +7436,7 @@ func (f *clientFrontend) SolveGateway(ctx context.Context, c gateway.Client, req
74337436
func (f *clientFrontend) DFCmdArgs(ctx, dockerfile string) (string, string) {
74347437
return "", ""
74357438
}
7439+
74367440
func (f *clientFrontend) RequiresBuildctl(t *testing.T) {
74377441
t.Skip()
74387442
}
@@ -7493,8 +7497,10 @@ func (*secModeInsecure) UpdateConfigFile(in string) string {
74937497
return in + "\n\ninsecure-entitlements = [\"security.insecure\"]\n"
74947498
}
74957499

7496-
var securityInsecureGranted integration.ConfigUpdater = &secModeInsecure{}
7497-
var securityInsecureDenied integration.ConfigUpdater = &secModeSandbox{}
7500+
var (
7501+
securityInsecureGranted integration.ConfigUpdater = &secModeInsecure{}
7502+
securityInsecureDenied integration.ConfigUpdater = &secModeSandbox{}
7503+
)
74987504

74997505
type networkModeHost struct{}
75007506

@@ -7508,8 +7514,10 @@ func (*networkModeSandbox) UpdateConfigFile(in string) string {
75087514
return in
75097515
}
75107516

7511-
var networkHostGranted integration.ConfigUpdater = &networkModeHost{}
7512-
var networkHostDenied integration.ConfigUpdater = &networkModeSandbox{}
7517+
var (
7518+
networkHostGranted integration.ConfigUpdater = &networkModeHost{}
7519+
networkHostDenied integration.ConfigUpdater = &networkModeSandbox{}
7520+
)
75137521

75147522
func fixedWriteCloser(wc io.WriteCloser) filesync.FileOutputFunc {
75157523
return func(map[string]string) (io.WriteCloser, error) {

frontend/dockerfile/docs/rules/_index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,9 @@ $ docker build --check .
8080
<td><a href="./legacy-key-value-format/">LegacyKeyValueFormat</a></td>
8181
<td>Legacy key/value format with whitespace separator should not be used</td>
8282
</tr>
83+
<tr>
84+
<td><a href="./redundant-target-platform/">RedundantTargetPlatform</a></td>
85+
<td>Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior</td>
86+
</tr>
8387
</tbody>
8488
</table>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
title: RedundantTargetPlatform
3+
description: Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior
4+
aliases:
5+
- /go/dockerfile/rule/redundant-target-platform/
6+
---
7+
8+
## Output
9+
10+
```text
11+
Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior
12+
```
13+
14+
## Description
15+
16+
A custom platform can be used for a base image. The default platform is the
17+
same platform as the target output so setting the platform to `$TARGETPLATFORM`
18+
is redundant and unnecessary.
19+
20+
## Examples
21+
22+
❌ Bad: this usage of `--platform` is redundant since `$TARGETPLATFORM` is the default.
23+
24+
```dockerfile
25+
FROM --platform=$TARGETPLATFORM alpine AS builder
26+
RUN apk add --no-cache git
27+
```
28+
29+
✅ Good: omit the `--platform` argument.
30+
31+
```dockerfile
32+
FROM alpine AS builder
33+
RUN apk add --no-cache git
34+
```
35+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## Output
2+
3+
```text
4+
Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior
5+
```
6+
7+
## Description
8+
9+
A custom platform can be used for a base image. The default platform is the
10+
same platform as the target output so setting the platform to `$TARGETPLATFORM`
11+
is redundant and unnecessary.
12+
13+
## Examples
14+
15+
❌ Bad: this usage of `--platform` is redundant since `$TARGETPLATFORM` is the default.
16+
17+
```dockerfile
18+
FROM --platform=$TARGETPLATFORM alpine AS builder
19+
RUN apk add --no-cache git
20+
```
21+
22+
✅ Good: omit the `--platform` argument.
23+
24+
```dockerfile
25+
FROM alpine AS builder
26+
RUN apk add --no-cache git
27+
```

frontend/dockerfile/linter/ruleset.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,12 @@ var (
124124
return fmt.Sprintf("Base image %s was pulled with platform %q, expected %q for current build", image, actual, expected)
125125
},
126126
}
127+
RuleRedundantTargetPlatform = LinterRule[func(string) string]{
128+
Name: "RedundantTargetPlatform",
129+
Description: "Setting platform to predefined $TARGETPLATFORM in FROM is redundant as this is the default behavior",
130+
URL: "https://docs.docker.com/go/dockerfile/rule/redundant-target-platform/",
131+
Format: func(platformVar string) string {
132+
return fmt.Sprintf("Setting platform to predefined %s in FROM is redundant as this is the default behavior", platformVar)
133+
},
134+
}
127135
)

0 commit comments

Comments
 (0)