Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds linter warning for using variables defined in parent stages #4935

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 71 additions & 13 deletions frontend/dockerfile/dockerfile2llb/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -748,9 +748,9 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error {
return "", err
}

newword, unmatched, err := opt.shlex.ProcessWord(word, env)
reportUnmatchedVariables(cmd, d.buildArgs, unmatched, &opt)
return newword, err
result, err := opt.shlex.ProcessWord(word, env)
reportUnmatchedVariables(cmd, d.buildArgs, result.Unmatched, &opt)
return result.Result, err
})
if err != nil {
return err
Expand All @@ -764,9 +764,9 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error {
}
lex := shell.NewLex('\\')
lex.SkipProcessQuotes = true
newword, unmatched, err := lex.ProcessWord(word, env)
reportUnmatchedVariables(cmd, d.buildArgs, unmatched, &opt)
return newword, err
result, err := lex.ProcessWord(word, env)
reportUnmatchedVariables(cmd, d.buildArgs, result.Unmatched, &opt)
return result.Result, err
})
if err != nil {
return err
Expand Down Expand Up @@ -1125,7 +1125,17 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
if err != nil {
return err
}
opt = append(opt, llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(&shlex, customname, env)), d.prefixPlatform, pl, env)))

name := processCmdEnv(processCmdArgs{
cmd: customname,
env: env,
dispatchState: d,
dispatchOpt: &dopt,
location: c.Location(),
shlex: &shlex,
})

opt = append(opt, llb.WithCustomName(prefixCommand(d, uppercaseCmd(name), d.prefixPlatform, pl, env)))
for _, h := range dopt.extraHosts {
opt = append(opt, llb.AddExtraHost(h.Host, h.IP))
}
Expand Down Expand Up @@ -1193,8 +1203,18 @@ func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bo
if err != nil {
return err
}

name := processCmdEnv(processCmdArgs{
cmd: c.String(),
env: env,
dispatchState: d,
dispatchOpt: opt,
location: c.Location(),
shlex: opt.shlex,
})

d.state = d.state.File(llb.Mkdir(wd, 0755, mkdirOpt...),
llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(opt.shlex, c.String(), env)), d.prefixPlatform, &platform, env)),
llb.WithCustomName(prefixCommand(d, uppercaseCmd(name), d.prefixPlatform, &platform, env)),
location(opt.sourceMap, c.Location()),
llb.Platform(*d.platform),
)
Expand Down Expand Up @@ -1272,7 +1292,15 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error {
return err
}

name := uppercaseCmd(processCmdEnv(cfg.opt.shlex, cfg.cmdToPrint.String(), env))
name := uppercaseCmd(processCmdEnv(processCmdArgs{
cmd: cfg.cmdToPrint.String(),
env: env,
dispatchState: d,
dispatchOpt: &cfg.opt,
location: cfg.location,
shlex: cfg.opt.shlex,
}))

pgName := prefixCommand(d, name, d.prefixPlatform, &platform, env)

var a *llb.FileAction
Expand Down Expand Up @@ -1886,12 +1914,24 @@ func uppercaseCmd(str string) string {
return strings.Join(p, " ")
}

func processCmdEnv(shlex *shell.Lex, cmd string, env []string) string {
w, _, err := shlex.ProcessWord(cmd, env)
type processCmdArgs struct {
cmd string
dispatchOpt *dispatchOpt
dispatchState *dispatchState
env []string
location []parser.Range
shlex *shell.Lex
}

func processCmdEnv(args processCmdArgs) string {
results, err := args.shlex.ProcessWord(args.cmd, args.env)
if err != nil {
return cmd
return args.cmd
}
return w
if args.dispatchOpt.lintWarn != nil {
reportOutOfScopeVars(args, results.Matched)
}
return results.Result
}

func prefixCommand(ds *dispatchState, str string, prefixPlatform bool, platform *ocispecs.Platform, env []string) string {
Expand Down Expand Up @@ -2086,6 +2126,24 @@ func validateStageNames(stages []instructions.Stage, warn linter.LintWarnFunc) {
}
}

func reportOutOfScopeVars(args processCmdArgs, matched map[string]struct{}) {
parentBuildArgs := make(map[string]struct{})
for base := args.dispatchState.base; base != nil; base = base.base {
for _, ba := range base.buildArgs {
parentBuildArgs[ba.Key] = struct{}{}
}
}
for _, ba := range args.dispatchState.buildArgs {
delete(parentBuildArgs, ba.Key)
}
for match := range matched {
if _, ok := parentBuildArgs[match]; ok {
msg := linter.RuleOutOfScopeVar.Format(match, args.dispatchState.stageName)
linter.RuleOutOfScopeVar.Run(args.dispatchOpt.lintWarn, args.location, msg)
}
}
}

func reportUnmatchedVariables(cmd instructions.Command, buildArgs []instructions.KeyValuePairOptional, unmatched map[string]struct{}, opt *dispatchOpt) {
if len(unmatched) == 0 {
return
Expand Down
61 changes: 59 additions & 2 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,
testOutOfScopeVar,
)

func testStageName(t *testing.T, sb integration.Sandbox) {
Expand Down Expand Up @@ -531,10 +532,24 @@ COPY Dockerfile${foo} .
FROM alpine AS base
ARG foo=Dockerfile

FROM base
FROM base AS first
COPY $foo .
ARG foo=Dockerfile
COPY $foo .

`)
checkLinterWarnings(t, sb, &lintTestParams{Dockerfile: dockerfile})
checkLinterWarnings(t, sb, &lintTestParams{
Dockerfile: dockerfile,
Warnings: []expectedLintWarning{
{
RuleName: "OutOfScopeVar",
Description: "Variables should be defined within the same stage",
Detail: "Variable 'foo' is out of scope for stage 'first', it should be defined within the same stage",
Level: 1,
Line: 6,
},
},
})

dockerfile = []byte(`
FROM alpine
Expand Down Expand Up @@ -628,6 +643,48 @@ HEALTHCHECK CMD ["/myotherapp"]
checkLinterWarnings(t, sb, &lintTestParams{Dockerfile: dockerfile})
}

func testOutOfScopeVar(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM alpine AS base
ARG foo=bar
`)
checkLinterWarnings(t, sb, &lintTestParams{Dockerfile: dockerfile})

dockerfile = []byte(`
FROM alpine AS base
ARG foo=bar

FROM base AS first
RUN echo $foo
ARG foo=baz
RUN echo $foo

FROM first AS second
RUN echo $foo
ARG foo=baz
RUN echo $foo
`)
checkLinterWarnings(t, sb, &lintTestParams{
Dockerfile: dockerfile,
Warnings: []expectedLintWarning{
{
RuleName: "OutOfScopeVar",
Description: "Variables should be defined within the same stage",
Detail: "Variable 'foo' is out of scope for stage 'first', it should be defined within the same stage",
Level: 1,
Line: 6,
},
{
RuleName: "OutOfScopeVar",
Description: "Variables should be defined within the same stage",
Detail: "Variable 'foo' is out of scope for stage 'second', it should be defined within the same stage",
Level: 1,
Line: 11,
},
},
})
}

func checkUnmarshal(t *testing.T, sb integration.Sandbox, lintTest *lintTestParams) {
destDir, err := os.MkdirTemp("", "buildkit")
require.NoError(t, err)
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)
},
}
RuleOutOfScopeVar = LinterRule[func(string, string) string]{
Name: "OutOfScopeVar",
Description: "Variables should be defined within the same stage",
Format: func(word, stageName string) string {
return fmt.Sprintf("Variable '%s' is out of scope for stage '%s', it should be defined within the same stage", word, stageName)
},
}
)
4 changes: 2 additions & 2 deletions frontend/dockerfile/shell/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ func NewLex(escapeToken rune) *Lex {
// and replace any env var references in 'word'. It will also
// return variables in word which were not found in the 'env' list,
// which is useful in later linting.
func (s *Lex) ProcessWord(word string, env []string) (string, map[string]struct{}, error) {
func (s *Lex) ProcessWord(word string, env []string) (*ProcessWordResult, error) {
result, err := s.process(word, BuildEnvs(env))
return result.Result, result.Unmatched, err
return result, err
}

// ProcessWords will use the 'env' list of environment variables,
Expand Down
27 changes: 13 additions & 14 deletions frontend/dockerfile/shell/lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ func TestReversePattern(t *testing.T) {
}

func TestShellParserMandatoryEnvVars(t *testing.T) {
var newWord string
var err error
shlex := NewLex('\\')
setEnvs := []string{"VAR=plain", "ARG=x"}
Expand All @@ -61,26 +60,26 @@ func TestShellParserMandatoryEnvVars(t *testing.T) {
noUnset := "${VAR?message here$ARG}"

// disallow empty
newWord, _, err = shlex.ProcessWord(noEmpty, setEnvs)
result, err := shlex.ProcessWord(noEmpty, setEnvs)
require.NoError(t, err)
require.Equal(t, "plain", newWord)
require.Equal(t, "plain", result.Result)

_, _, err = shlex.ProcessWord(noEmpty, emptyEnvs)
_, err = shlex.ProcessWord(noEmpty, emptyEnvs)
require.ErrorContains(t, err, "message herex")

_, _, err = shlex.ProcessWord(noEmpty, unsetEnvs)
_, err = shlex.ProcessWord(noEmpty, unsetEnvs)
require.ErrorContains(t, err, "message herex")

// disallow unset
newWord, _, err = shlex.ProcessWord(noUnset, setEnvs)
result, err = shlex.ProcessWord(noUnset, setEnvs)
require.NoError(t, err)
require.Equal(t, "plain", newWord)
require.Equal(t, "plain", result.Result)

newWord, _, err = shlex.ProcessWord(noUnset, emptyEnvs)
result, err = shlex.ProcessWord(noUnset, emptyEnvs)
require.NoError(t, err)
require.Empty(t, newWord)
require.Empty(t, result.Result)

_, _, err = shlex.ProcessWord(noUnset, unsetEnvs)
_, err = shlex.ProcessWord(noUnset, unsetEnvs)
require.ErrorContains(t, err, "message herex")
}

Expand Down Expand Up @@ -123,15 +122,15 @@ func TestShellParser4EnvVars(t *testing.T) {

if ((platform == "W" || platform == "A") && runtime.GOOS == "windows") ||
((platform == "U" || platform == "A") && runtime.GOOS != "windows") {
newWord, _, err := shlex.ProcessWord(source, envs)
result, err := shlex.ProcessWord(source, envs)
if expected == "error" {
require.Errorf(t, err, "input: %q, result: %q", source, newWord)
require.Errorf(t, err, "input: %q, result: %q", source, result.Result)
} else {
require.NoError(t, err, "at line %d of %s", lineCount, fn)
require.Equal(t, expected, newWord, "at line %d of %s", lineCount, fn)
require.Equal(t, expected, result.Result, "at line %d of %s", lineCount, fn)
}

newWord, err = shlex.ProcessWordWithMap(source, envsMap)
newWord, err := shlex.ProcessWordWithMap(source, envsMap)
if expected == "error" {
require.Errorf(t, err, "input: %q, result: %q", source, newWord)
} else {
Expand Down
Loading