Skip to content

Commit e3c0a07

Browse files
committed
Adds controls for checking dockerfile lint rules
Signed-off-by: Talon Bowler <[email protected]>
1 parent 1a43db6 commit e3c0a07

File tree

7 files changed

+385
-73
lines changed

7 files changed

+385
-73
lines changed

frontend/dockerfile/dockerfile2llb/convert.go

Lines changed: 106 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,64 @@ func ListTargets(ctx context.Context, dt []byte) (*targets.List, error) {
162162
return l, nil
163163
}
164164

165+
func parseLintOptions(checkStr string) (*linter.Config, error) {
166+
checkStr = strings.TrimSpace(checkStr)
167+
if checkStr == "" {
168+
return &linter.Config{}, nil
169+
}
170+
171+
parts := strings.SplitN(checkStr, ";", 2)
172+
var skipSet []string
173+
var errorOnWarn, skipAll bool
174+
for _, p := range parts {
175+
k, v, ok := strings.Cut(p, "=")
176+
if !ok {
177+
return nil, errors.Errorf("invalid check option %q", p)
178+
}
179+
k = strings.TrimSpace(k)
180+
switch k {
181+
case "skip":
182+
v = strings.TrimSpace(v)
183+
if v == "all" {
184+
skipAll = true
185+
} else {
186+
skipSet = strings.Split(v, ",")
187+
for i, rule := range skipSet {
188+
skipSet[i] = strings.TrimSpace(rule)
189+
}
190+
}
191+
case "error":
192+
v, err := strconv.ParseBool(strings.TrimSpace(v))
193+
if err != nil {
194+
return nil, errors.Wrapf(err, "failed to parse check option %q", p)
195+
}
196+
errorOnWarn = v
197+
default:
198+
return nil, errors.Errorf("invalid check option %q", k)
199+
}
200+
}
201+
return &linter.Config{
202+
SkipRules: skipSet,
203+
SkipAll: skipAll,
204+
ReturnAsError: errorOnWarn,
205+
}, nil
206+
}
207+
208+
func newRuleLinter(dt []byte, opt *ConvertOpt) (*linter.Linter, error) {
209+
var lintOptionStr string
210+
if opt.Client != nil && opt.Client.LinterConfig != nil {
211+
lintOptionStr = *opt.Client.LinterConfig
212+
} else {
213+
lintOptionStr, _, _, _ = parser.ParseDirective("check", dt)
214+
}
215+
lintConfig, err := parseLintOptions(lintOptionStr)
216+
if err != nil {
217+
return nil, errors.Wrapf(err, "failed to parse check options")
218+
}
219+
lintConfig.Warn = opt.Warn
220+
return linter.New(lintConfig), nil
221+
}
222+
165223
func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchState, error) {
166224
if len(dt) == 0 {
167225
return nil, errors.Errorf("the Dockerfile cannot be empty")
@@ -184,8 +242,9 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
184242
return nil, nil, nil
185243
}
186244

187-
if opt.Warn == nil {
188-
opt.Warn = func(string, string, string, string, []parser.Range) {}
245+
lint, err := newRuleLinter(dt, &opt)
246+
if err != nil {
247+
return nil, err
189248
}
190249

191250
if opt.Client != nil && opt.LLBCaps == nil {
@@ -214,19 +273,19 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
214273
if warning.URL == linter.RuleNoEmptyContinuations.URL {
215274
location := []parser.Range{*warning.Location}
216275
msg := linter.RuleNoEmptyContinuations.Format()
217-
linter.RuleNoEmptyContinuations.Run(opt.Warn, location, msg)
276+
lint.Run(&linter.RuleNoEmptyContinuations, location, msg)
218277
}
219278
}
220279

221-
validateCommandCasing(dockerfile, opt.Warn)
280+
validateCommandCasing(dockerfile, lint)
222281

223282
proxyEnv := proxyEnvFromBuildArgs(opt.BuildArgs)
224283

225-
stages, metaArgs, err := instructions.Parse(dockerfile.AST, opt.Warn)
284+
stages, metaArgs, err := instructions.Parse(dockerfile.AST, lint)
226285
if err != nil {
227286
return nil, err
228287
}
229-
validateStageNames(stages, opt.Warn)
288+
validateStageNames(stages, lint)
230289

231290
shlex := shell.NewLex(dockerfile.EscapeToken)
232291
outline := newOutlineCapture()
@@ -264,7 +323,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
264323
// set base state for every image
265324
for i, st := range stages {
266325
nameMatch, err := shlex.ProcessWordWithMatches(st.BaseName, metaArgsToMap(optMetaArgs))
267-
reportUnusedFromArgs(metaArgsKeys(optMetaArgs), nameMatch.Unmatched, st.Location, opt.Warn)
326+
reportUnusedFromArgs(metaArgsKeys(optMetaArgs), nameMatch.Unmatched, st.Location, lint)
268327
used := nameMatch.Matched
269328

270329
if err != nil {
@@ -288,7 +347,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
288347

289348
if v := st.Platform; v != "" {
290349
platMatch, err := shlex.ProcessWordWithMatches(v, metaArgsToMap(optMetaArgs))
291-
reportUnusedFromArgs(metaArgsKeys(optMetaArgs), platMatch.Unmatched, st.Location, opt.Warn)
350+
reportUnusedFromArgs(metaArgsKeys(optMetaArgs), platMatch.Unmatched, st.Location, lint)
292351

293352
if err != nil {
294353
return nil, parser.WithLocation(errors.Wrapf(err, "failed to process arguments for platform %s", platMatch.Result), st.Location)
@@ -542,7 +601,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
542601
llb.WithCustomName(prefixCommand(d, "FROM "+d.stage.BaseName, opt.MultiPlatformRequested, platform, nil)),
543602
location(opt.SourceMap, d.stage.Location),
544603
)
545-
validateBaseImagePlatform(origName, *platform, d.image.Platform, d.stage.Location, opt.Warn)
604+
validateBaseImagePlatform(origName, *platform, d.image.Platform, d.stage.Location, lint)
546605
}
547606
d.platform = platform
548607
return nil
@@ -614,7 +673,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
614673
cgroupParent: opt.CgroupParent,
615674
llbCaps: opt.LLBCaps,
616675
sourceMap: opt.SourceMap,
617-
lintWarn: opt.Warn,
676+
lint: lint,
618677
}
619678

620679
if err = dispatchOnBuildTriggers(d, d.image.Config.OnBuild, opt); err != nil {
@@ -658,6 +717,14 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
658717
target.image.Config.Labels[k] = v
659718
}
660719

720+
// If lint.Error() returns an error, it means that
721+
// there were warnings, and that our linter has been
722+
// configured to return an error on warnings,
723+
// so we appropriately return that error here.
724+
if err := lint.Error(); err != nil {
725+
return nil, err
726+
}
727+
661728
opts := filterPaths(ctxPaths)
662729
bctx := opt.MainContext
663730
if opt.Client != nil {
@@ -758,7 +825,7 @@ type dispatchOpt struct {
758825
cgroupParent string
759826
llbCaps *apicaps.CapSet
760827
sourceMap *llb.SourceMap
761-
lintWarn linter.LintWarnFunc
828+
lint *linter.Linter
762829
}
763830

764831
func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error {
@@ -839,11 +906,11 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error {
839906
case *instructions.OnbuildCommand:
840907
err = dispatchOnbuild(d, c)
841908
case *instructions.CmdCommand:
842-
err = dispatchCmd(d, c, opt.lintWarn)
909+
err = dispatchCmd(d, c, opt.lint)
843910
case *instructions.EntrypointCommand:
844-
err = dispatchEntrypoint(d, c, opt.lintWarn)
911+
err = dispatchEntrypoint(d, c, opt.lint)
845912
case *instructions.HealthCheckCommand:
846-
err = dispatchHealthcheck(d, c, opt.lintWarn)
913+
err = dispatchHealthcheck(d, c, opt.lint)
847914
case *instructions.ExposeCommand:
848915
err = dispatchExpose(d, c, opt.shlex)
849916
case *instructions.UserCommand:
@@ -1183,7 +1250,7 @@ func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bo
11831250
// by fixing the first one.
11841251
if !d.workdirSet && !system.IsAbs(c.Path, d.platform.OS) {
11851252
msg := linter.RuleWorkdirRelativePath.Format(c.Path)
1186-
linter.RuleWorkdirRelativePath.Run(opt.lintWarn, c.Location(), msg)
1253+
opt.lint.Run(&linter.RuleWorkdirRelativePath, c.Location(), msg)
11871254
}
11881255
d.workdirSet = true
11891256
}
@@ -1499,14 +1566,14 @@ func dispatchOnbuild(d *dispatchState, c *instructions.OnbuildCommand) error {
14991566
return nil
15001567
}
15011568

1502-
func dispatchCmd(d *dispatchState, c *instructions.CmdCommand, warn linter.LintWarnFunc) error {
1503-
validateUsedOnce(c, &d.cmd, warn)
1569+
func dispatchCmd(d *dispatchState, c *instructions.CmdCommand, lint *linter.Linter) error {
1570+
validateUsedOnce(c, &d.cmd, lint)
15041571

15051572
var args []string = c.CmdLine
15061573
if c.PrependShell {
15071574
if len(d.image.Config.Shell) == 0 {
15081575
msg := linter.RuleJSONArgsRecommended.Format(c.Name())
1509-
linter.RuleJSONArgsRecommended.Run(warn, c.Location(), msg)
1576+
lint.Run(&linter.RuleJSONArgsRecommended, c.Location(), msg)
15101577
}
15111578
args = withShell(d.image, args)
15121579
}
@@ -1515,14 +1582,14 @@ func dispatchCmd(d *dispatchState, c *instructions.CmdCommand, warn linter.LintW
15151582
return commitToHistory(&d.image, fmt.Sprintf("CMD %q", args), false, nil, d.epoch)
15161583
}
15171584

1518-
func dispatchEntrypoint(d *dispatchState, c *instructions.EntrypointCommand, warn linter.LintWarnFunc) error {
1519-
validateUsedOnce(c, &d.entrypoint, warn)
1585+
func dispatchEntrypoint(d *dispatchState, c *instructions.EntrypointCommand, lint *linter.Linter) error {
1586+
validateUsedOnce(c, &d.entrypoint, lint)
15201587

15211588
var args []string = c.CmdLine
15221589
if c.PrependShell {
15231590
if len(d.image.Config.Shell) == 0 {
15241591
msg := linter.RuleJSONArgsRecommended.Format(c.Name())
1525-
linter.RuleJSONArgsRecommended.Run(warn, c.Location(), msg)
1592+
lint.Run(&linter.RuleJSONArgsRecommended, c.Location(), msg)
15261593
}
15271594
args = withShell(d.image, args)
15281595
}
@@ -1533,8 +1600,8 @@ func dispatchEntrypoint(d *dispatchState, c *instructions.EntrypointCommand, war
15331600
return commitToHistory(&d.image, fmt.Sprintf("ENTRYPOINT %q", args), false, nil, d.epoch)
15341601
}
15351602

1536-
func dispatchHealthcheck(d *dispatchState, c *instructions.HealthCheckCommand, warn linter.LintWarnFunc) error {
1537-
validateUsedOnce(c, &d.healthcheck, warn)
1603+
func dispatchHealthcheck(d *dispatchState, c *instructions.HealthCheckCommand, lint *linter.Linter) error {
1604+
validateUsedOnce(c, &d.healthcheck, lint)
15381605
d.image.Config.Healthcheck = &dockerspec.HealthcheckConfig{
15391606
Test: c.Health.Test,
15401607
Interval: c.Health.Interval,
@@ -2058,7 +2125,7 @@ func isSelfConsistentCasing(s string) bool {
20582125
return s == strings.ToLower(s) || s == strings.ToUpper(s)
20592126
}
20602127

2061-
func validateCommandCasing(dockerfile *parser.Result, warn linter.LintWarnFunc) {
2128+
func validateCommandCasing(dockerfile *parser.Result, lint *linter.Linter) {
20622129
var lowerCount, upperCount int
20632130
for _, node := range dockerfile.AST.Children {
20642131
if isSelfConsistentCasing(node.Value) {
@@ -2077,7 +2144,7 @@ func validateCommandCasing(dockerfile *parser.Result, warn linter.LintWarnFunc)
20772144
// command to the casing of the majority of commands.
20782145
if !isSelfConsistentCasing(node.Value) {
20792146
msg := linter.RuleSelfConsistentCommandCasing.Format(node.Value)
2080-
linter.RuleSelfConsistentCommandCasing.Run(warn, node.Location(), msg)
2147+
lint.Run(&linter.RuleSelfConsistentCommandCasing, node.Location(), msg)
20812148
} else {
20822149
var msg string
20832150
var needsLintWarn bool
@@ -2089,7 +2156,7 @@ func validateCommandCasing(dockerfile *parser.Result, warn linter.LintWarnFunc)
20892156
needsLintWarn = true
20902157
}
20912158
if needsLintWarn {
2092-
linter.RuleFileConsistentCommandCasing.Run(warn, node.Location(), msg)
2159+
lint.Run(&linter.RuleFileConsistentCommandCasing, node.Location(), msg)
20932160
}
20942161
}
20952162
}
@@ -2100,25 +2167,28 @@ var reservedStageNames = map[string]struct{}{
21002167
"scratch": {},
21012168
}
21022169

2103-
func validateStageNames(stages []instructions.Stage, warn linter.LintWarnFunc) {
2170+
func validateStageNames(stages []instructions.Stage, lint *linter.Linter) {
21042171
stageNames := make(map[string]struct{})
21052172
for _, stage := range stages {
21062173
if stage.Name != "" {
21072174
if _, ok := reservedStageNames[stage.Name]; ok {
21082175
msg := linter.RuleReservedStageName.Format(stage.Name)
2109-
linter.RuleReservedStageName.Run(warn, stage.Location, msg)
2176+
lint.Run(&linter.RuleReservedStageName, stage.Location, msg)
21102177
}
21112178

21122179
if _, ok := stageNames[stage.Name]; ok {
21132180
msg := linter.RuleDuplicateStageName.Format(stage.Name)
2114-
linter.RuleDuplicateStageName.Run(warn, stage.Location, msg)
2181+
lint.Run(&linter.RuleDuplicateStageName, stage.Location, msg)
21152182
}
21162183
stageNames[stage.Name] = struct{}{}
21172184
}
21182185
}
21192186
}
21202187

21212188
func reportUnmatchedVariables(cmd instructions.Command, buildArgs []instructions.KeyValuePairOptional, env []string, unmatched map[string]struct{}, opt *dispatchOpt) {
2189+
if len(unmatched) == 0 {
2190+
return
2191+
}
21222192
for _, buildArg := range buildArgs {
21232193
delete(unmatched, buildArg.Key)
21242194
}
@@ -2136,7 +2206,7 @@ func reportUnmatchedVariables(cmd instructions.Command, buildArgs []instructions
21362206
}
21372207
match, _ := suggest.Search(cmdVar, options, true)
21382208
msg := linter.RuleUndefinedVar.Format(cmdVar, match)
2139-
linter.RuleUndefinedVar.Run(opt.lintWarn, cmd.Location(), msg)
2209+
opt.lint.Run(&linter.RuleUndefinedVar, cmd.Location(), msg)
21402210
}
21412211
}
21422212

@@ -2190,11 +2260,11 @@ func toPBLocation(sourceIndex int, location []parser.Range) pb.Location {
21902260
}
21912261
}
21922262

2193-
func reportUnusedFromArgs(values []string, unmatched map[string]struct{}, location []parser.Range, warn linter.LintWarnFunc) {
2263+
func reportUnusedFromArgs(values []string, unmatched map[string]struct{}, location []parser.Range, lint *linter.Linter) {
21942264
for arg := range unmatched {
21952265
suggest, _ := suggest.Search(arg, values, true)
21962266
msg := linter.RuleUndeclaredArgInFrom.Format(arg, suggest)
2197-
linter.RuleUndeclaredArgInFrom.Run(warn, location, msg)
2267+
lint.Run(&linter.RuleUndeclaredArgInFrom, location, msg)
21982268
}
21992269
}
22002270

@@ -2208,12 +2278,12 @@ func (v *instructionTracker) MarkUsed(loc []parser.Range) {
22082278
v.IsSet = true
22092279
}
22102280

2211-
func validateUsedOnce(c instructions.Command, loc *instructionTracker, warn linter.LintWarnFunc) {
2281+
func validateUsedOnce(c instructions.Command, loc *instructionTracker, lint *linter.Linter) {
22122282
if loc.IsSet {
22132283
msg := linter.RuleMultipleInstructionsDisallowed.Format(c.Name())
22142284
// Report the location of the previous invocation because it is the one
22152285
// that will be ignored.
2216-
linter.RuleMultipleInstructionsDisallowed.Run(warn, loc.Loc, msg)
2286+
lint.Run(&linter.RuleMultipleInstructionsDisallowed, loc.Loc, msg)
22172287
}
22182288
loc.MarkUsed(c.Location())
22192289
}
@@ -2229,11 +2299,11 @@ func wrapSuggestAny(err error, keys map[string]struct{}, options []string) error
22292299
return err
22302300
}
22312301

2232-
func validateBaseImagePlatform(name string, expected, actual ocispecs.Platform, location []parser.Range, warn linter.LintWarnFunc) {
2302+
func validateBaseImagePlatform(name string, expected, actual ocispecs.Platform, location []parser.Range, lint *linter.Linter) {
22332303
if expected.OS != actual.OS || expected.Architecture != actual.Architecture {
22342304
expectedStr := platforms.Format(platforms.Normalize(expected))
22352305
actualStr := platforms.Format(platforms.Normalize(actual))
22362306
msg := linter.RuleInvalidBaseImagePlatform.Format(name, expectedStr, actualStr)
2237-
linter.RuleInvalidBaseImagePlatform.Run(warn, location, msg)
2307+
lint.Run(&linter.RuleInvalidBaseImagePlatform, location, msg)
22382308
}
22392309
}

0 commit comments

Comments
 (0)