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

linter: lint rule for using the legacy key/value format with whitespace #4923

Merged
merged 1 commit into from
May 30, 2024
Merged
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
37 changes: 36 additions & 1 deletion 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,
testLegacyKeyValueFormat,
)

func testStageName(t *testing.T, sb integration.Sandbox) {
Expand Down Expand Up @@ -604,7 +605,7 @@ EOF
ENTRYPOINT ["/myotherapp"]
CMD ["/myotherapp"]
HEALTHCHECK CMD ["/myotherapp"]
`)
`)
checkLinterWarnings(t, sb, &lintTestParams{
Dockerfile: dockerfile,
Warnings: []expectedLintWarning{
Expand All @@ -628,6 +629,40 @@ HEALTHCHECK CMD ["/myotherapp"]
checkLinterWarnings(t, sb, &lintTestParams{Dockerfile: dockerfile})
}

func testLegacyKeyValueFormat(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM scratch
ENV key value
LABEL key value
Comment on lines +634 to +636
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know there were some concerns about warning about these being potentially too strict.

Wondering if we should have a "more permissive" and "more strict" rule. The most problematic ones are when multiple whitespaces are present;

ARG key one two
ENV key one two

Those look very much alike, but ARG means 3 args are defined, but the ENV one defines a single key env with value one two

`)
checkLinterWarnings(t, sb, &lintTestParams{
Dockerfile: dockerfile,
Warnings: []expectedLintWarning{
{
RuleName: "LegacyKeyValueFormat",
Description: "Legacy key/value format with whitespace separator should not be used",
Detail: "\"ENV key=value\" should be used instead of legacy \"ENV key value\" format",
Line: 3,
Level: 1,
},
{
RuleName: "LegacyKeyValueFormat",
Description: "Legacy key/value format with whitespace separator should not be used",
Detail: "\"LABEL key=value\" should be used instead of legacy \"LABEL key value\" format",
Line: 4,
Level: 1,
},
},
})

dockerfile = []byte(`
FROM scratch
ENV key=value
LABEL key=value
`)
checkLinterWarnings(t, sb, &lintTestParams{Dockerfile: dockerfile})
}

func checkUnmarshal(t *testing.T, sb integration.Sandbox, lintTest *lintTestParams) {
destDir, err := os.MkdirTemp("", "buildkit")
require.NoError(t, err)
Expand Down
25 changes: 14 additions & 11 deletions frontend/dockerfile/instructions/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ func ParseInstructionWithLinter(node *parser.Node, lintWarn linter.LintWarnFunc)
req := newParseRequestFromNode(node)
switch strings.ToLower(node.Value) {
case command.Env:
return parseEnv(req)
return parseEnv(req, lintWarn)
case command.Maintainer:
if lintWarn != nil {
msg := linter.RuleMaintainerDeprecated.Format()
linter.RuleMaintainerDeprecated.Run(lintWarn, node.Location(), msg)
}
return parseMaintainer(req)
case command.Label:
return parseLabel(req)
return parseLabel(req, lintWarn)
case command.Add:
return parseAdd(req)
case command.Copy:
Expand Down Expand Up @@ -195,31 +195,34 @@ func Parse(ast *parser.Node, lint linter.LintWarnFunc) (stages []Stage, metaArgs
return stages, metaArgs, nil
}

func parseKvps(args []string, cmdName string) (KeyValuePairs, error) {
func parseKvps(args []string, cmdName string, location []parser.Range, lint linter.LintWarnFunc) (KeyValuePairs, error) {
if len(args) == 0 {
return nil, errAtLeastOneArgument(cmdName)
}
if len(args)%2 != 0 {
if len(args)%3 != 0 {
// should never get here, but just in case
return nil, errTooManyArguments(cmdName)
}
var res KeyValuePairs
for j := 0; j < len(args); j += 2 {
for j := 0; j < len(args); j += 3 {
if len(args[j]) == 0 {
return nil, errBlankCommandNames(cmdName)
}
name := args[j]
value := args[j+1]
name, value, sep := args[j], args[j+1], args[j+2]
if sep == "" {
msg := linter.RuleLegacyKeyValueFormat.Format(cmdName)
linter.RuleLegacyKeyValueFormat.Run(lint, location, msg)
}
res = append(res, KeyValuePair{Key: name, Value: value})
}
return res, nil
}

func parseEnv(req parseRequest) (*EnvCommand, error) {
func parseEnv(req parseRequest, lint linter.LintWarnFunc) (*EnvCommand, error) {
if err := req.flags.Parse(); err != nil {
return nil, err
}
envs, err := parseKvps(req.args, "ENV")
envs, err := parseKvps(req.args, "ENV", req.location, lint)
if err != nil {
return nil, err
}
Expand All @@ -243,12 +246,12 @@ func parseMaintainer(req parseRequest) (*MaintainerCommand, error) {
}, nil
}

func parseLabel(req parseRequest) (*LabelCommand, error) {
func parseLabel(req parseRequest, lint linter.LintWarnFunc) (*LabelCommand, error) {
if err := req.flags.Parse(); err != nil {
return nil, err
}

labels, err := parseKvps(req.args, "LABEL")
labels, err := parseKvps(req.args, "LABEL", req.location, lint)
if err != nil {
return nil, err
}
Expand Down
6 changes: 6 additions & 0 deletions frontend/dockerfile/instructions/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ func TestCommandsTooManyArguments(t *testing.T) {
Value: "arg2",
Next: &parser.Node{
Value: "arg3",
Next: &parser.Node{
Value: "",
},
},
},
},
Expand All @@ -97,6 +100,9 @@ func TestCommandsBlankNames(t *testing.T) {
Value: "",
Next: &parser.Node{
Value: "arg2",
Next: &parser.Node{
Value: "=",
},
},
},
}
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)
},
}
RuleLegacyKeyValueFormat = LinterRule[func(cmdName string) string]{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should provide some hint that key=value should be used instead in the message. Most users will not know what is "legacy format"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this message to read differently so now it explicitly mentions the solution in the detailed description.

Name: "LegacyKeyValueFormat",
Description: "Legacy key/value format with whitespace separator should not be used",
Format: func(cmdName string) string {
return fmt.Sprintf("\"%s key=value\" should be used instead of legacy \"%s key value\" format", cmdName, cmdName)
},
}
)
15 changes: 10 additions & 5 deletions frontend/dockerfile/parser/line_parsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func parseNameVal(rest string, key string, d *directives) (*Node, error) {
if len(parts) < 2 {
return nil, errors.Errorf("%s must have two arguments", key)
}
return newKeyValueNode(parts[0], parts[1]), nil
return newKeyValueNode(parts[0], parts[1], ""), nil
}

var rootNode *Node
Expand All @@ -165,17 +165,20 @@ func parseNameVal(rest string, key string, d *directives) (*Node, error) {
}

parts := strings.SplitN(word, "=", 2)
node := newKeyValueNode(parts[0], parts[1])
node := newKeyValueNode(parts[0], parts[1], "=")
rootNode, prevNode = appendKeyValueNode(node, rootNode, prevNode)
}

return rootNode, nil
}

func newKeyValueNode(key, value string) *Node {
func newKeyValueNode(key, value, sep string) *Node {
return &Node{
Value: key,
Next: &Node{Value: value},
Next: &Node{
Value: value,
Next: &Node{Value: sep},
},
}
}

Expand All @@ -187,7 +190,9 @@ func appendKeyValueNode(node, rootNode, prevNode *Node) (*Node, *Node) {
prevNode.Next = node
}

prevNode = node.Next
for prevNode = node.Next; prevNode.Next != nil; {
prevNode = prevNode.Next
}
return rootNode, prevNode
}

Expand Down
15 changes: 12 additions & 3 deletions frontend/dockerfile/parser/line_parsers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ func TestParseNameValOldFormat(t *testing.T) {

expected := &Node{
Value: "foo",
Next: &Node{Value: "bar"},
Next: &Node{
Value: "bar",
Next: &Node{Value: ""},
},
}
require.Equal(t, expected, node, cmpNodeOpt)
}
Expand All @@ -31,9 +34,15 @@ func TestParseNameValNewFormat(t *testing.T) {
Next: &Node{
Value: "bar",
Next: &Node{
Value: "thing",
Value: "=",
Next: &Node{
Value: "star",
Value: "thing",
Next: &Node{
Value: "star",
Next: &Node{
Value: "=",
},
},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(from "ubuntu:14.04")
(label "maintainer" "Seongyeol Lim <seongyeol37@gmail.com>")
(label "maintainer" "Seongyeol Lim <seongyeol37@gmail.com>" "")
(copy "." "/go/src/github.com/docker/docker")
(add "." "/")
(add "null" "/")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(from "brimstone/ubuntu:14.04")
(label "maintainer" "brimstone@the.narro.ws")
(env "GOPATH" "/go")
(label "maintainer" "brimstone@the.narro.ws" "")
(env "GOPATH" "/go" "")
(entrypoint "/usr/local/bin/consuldock")
(run "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get --no-install-recommends install -y git golang ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists \t&& go get -v github.com/brimstone/consuldock && mv $GOPATH/bin/consuldock /usr/local/bin/consuldock \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.* \t&& rm -rf $GOPATH")
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
(run "apt-get update && apt-get --no-install-recommends install -y unzip wget \t&& apt-get clean \t&& rm -rf /var/lib/apt/lists")
(run "cd /tmp && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip -O web_ui.zip && unzip web_ui.zip && mv dist /webui && rm web_ui.zip")
(run "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get --no-install-recommends install -y unzip wget && apt-get clean && rm -rf /var/lib/apt/lists && cd /tmp && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip -O web_ui.zip && unzip web_ui.zip && mv dist /webui && rm web_ui.zip \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.*")
(env "GOPATH" "/go")
(env "GOPATH" "/go" "")
(run "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get --no-install-recommends install -y git golang ca-certificates build-essential && apt-get clean && rm -rf /var/lib/apt/lists \t&& go get -v github.com/hashicorp/consul \t&& mv $GOPATH/bin/consul /usr/bin/consul \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.* \t&& rm -rf $GOPATH")
24 changes: 12 additions & 12 deletions frontend/dockerfile/parser/testfiles/cpuguy83-nagios/result
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
(from "cpuguy83/ubuntu")
(env "NAGIOS_HOME" "/opt/nagios")
(env "NAGIOS_USER" "nagios")
(env "NAGIOS_GROUP" "nagios")
(env "NAGIOS_CMDUSER" "nagios")
(env "NAGIOS_CMDGROUP" "nagios")
(env "NAGIOSADMIN_USER" "nagiosadmin")
(env "NAGIOSADMIN_PASS" "nagios")
(env "APACHE_RUN_USER" "nagios")
(env "APACHE_RUN_GROUP" "nagios")
(env "NAGIOS_TIMEZONE" "UTC")
(env "NAGIOS_HOME" "/opt/nagios" "")
(env "NAGIOS_USER" "nagios" "")
(env "NAGIOS_GROUP" "nagios" "")
(env "NAGIOS_CMDUSER" "nagios" "")
(env "NAGIOS_CMDGROUP" "nagios" "")
(env "NAGIOSADMIN_USER" "nagiosadmin" "")
(env "NAGIOSADMIN_PASS" "nagios" "")
(env "APACHE_RUN_USER" "nagios" "")
(env "APACHE_RUN_GROUP" "nagios" "")
(env "NAGIOS_TIMEZONE" "UTC" "")
(run "sed -i 's/universe/universe multiverse/' /etc/apt/sources.list")
(run "apt-get update && apt-get --no-install-recommends install -y iputils-ping netcat build-essential snmp snmpd snmp-mibs-downloader php5-cli apache2 libapache2-mod-php5 runit bc postfix bsd-mailx")
(run "( egrep -i \"^${NAGIOS_GROUP}\" /etc/group || groupadd $NAGIOS_GROUP ) && ( egrep -i \"^${NAGIOS_CMDGROUP}\" /etc/group || groupadd $NAGIOS_CMDGROUP )")
Expand All @@ -33,8 +33,8 @@
(add "postfix.init" "/etc/sv/postfix/run")
(add "postfix.stop" "/etc/sv/postfix/finish")
(add "start.sh" "/usr/local/bin/start_nagios")
(env "APACHE_LOCK_DIR" "/var/run")
(env "APACHE_LOG_DIR" "/var/log/apache2")
(env "APACHE_LOCK_DIR" "/var/run" "")
(env "APACHE_LOG_DIR" "/var/log/apache2" "")
(expose "80")
(volume "/opt/nagios/var" "/opt/nagios/etc" "/opt/nagios/libexec" "/var/log/apache2" "/usr/share/snmp/mibs")
(cmd "/usr/local/bin/start_nagios")
12 changes: 6 additions & 6 deletions frontend/dockerfile/parser/testfiles/docker/result
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
(from "ubuntu:14.04")
(label "maintainer" "Tianon Gravi <admwiggin@gmail.com> (@tianon)")
(label "maintainer" "Tianon Gravi <admwiggin@gmail.com> (@tianon)" "")
(run "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \tapt-utils \taufs-tools \tautomake \tbtrfs-tools \tbuild-essential \tcurl \tdpkg-sig \tgit \tiptables \tlibapparmor-dev \tlibcap-dev \tmercurial \tpandoc \tparallel \treprepro \truby1.9.1 \truby1.9.1-dev \ts3cmd=1.1.0* \t--no-install-recommends")
(run "git clone --no-checkout https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2 && cd /usr/local/lvm2 && git checkout -q v2_02_103")
(run "cd /usr/local/lvm2 && ./configure --enable-static_link && make device-mapper && make install_device-mapper")
(run "curl -sSL https://golang.org/dl/go1.3.src.tar.gz | tar -v -C /usr/local -xz")
(env "PATH" "/usr/local/go/bin:$PATH")
(env "GOPATH" "/go:/go/src/github.com/docker/docker/vendor")
(env "PATH" "/usr/local/go/bin:$PATH" "")
(env "GOPATH" "/go:/go/src/github.com/docker/docker/vendor" "")
(run "cd /usr/local/go/src && ./make.bash --no-clean 2>&1")
(env "DOCKER_CROSSPLATFORMS" "linux/386 linux/arm \tdarwin/amd64 darwin/386 \tfreebsd/amd64 freebsd/386 freebsd/arm")
(env "GOARM" "5")
(env "DOCKER_CROSSPLATFORMS" "linux/386 linux/arm \tdarwin/amd64 darwin/386 \tfreebsd/amd64 freebsd/386 freebsd/arm" "")
(env "GOARM" "5" "")
(run "cd /usr/local/go/src && bash -xc 'for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash --no-clean 2>&1; done'")
(run "go get golang.org/x/tools/cmd/cover")
(run "gem install --no-rdoc --no-ri fpm --version 1.0.2")
Expand All @@ -19,6 +19,6 @@
(run "useradd --create-home --gid docker unprivilegeduser")
(volume "/var/lib/docker")
(workdir "/go/src/github.com/docker/docker")
(env "DOCKER_BUILDTAGS" "apparmor selinux")
(env "DOCKER_BUILDTAGS" "apparmor selinux" "")
(entrypoint "hack/dind")
(copy "." "/go/src/github.com/docker/docker")
30 changes: 15 additions & 15 deletions frontend/dockerfile/parser/testfiles/env/result
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
(from "ubuntu")
(env "name" "value")
(env "name" "value")
(env "name" "value" "name2" "value2")
(env "name" "\"value value1\"")
(env "name" "value\\ value2")
(env "name" "\"value'quote space'value2\"")
(env "name" "'value\"double quote\"value2'")
(env "name" "value\\ value2" "name2" "value2\\ value3")
(env "name" "\"a\\\"b\"")
(env "name" "\"a\\'b\"")
(env "name" "'a\\'b'")
(env "name" "'a\\'b''")
(env "name" "'a\\\"b'")
(env "name" "\"''\"")
(env "name" "value" "name1" "value1" "name2" "\"value2a value2b\"" "name3" "\"value3a\\n\\\"value3b\\\"\"" "name4" "\"value4a\\\\nvalue4b\"")
(env "name" "value" "")
(env "name" "value" "=")
(env "name" "value" "=" "name2" "value2" "=")
(env "name" "\"value value1\"" "=")
(env "name" "value\\ value2" "=")
(env "name" "\"value'quote space'value2\"" "=")
(env "name" "'value\"double quote\"value2'" "=")
(env "name" "value\\ value2" "=" "name2" "value2\\ value3" "=")
(env "name" "\"a\\\"b\"" "=")
(env "name" "\"a\\'b\"" "=")
(env "name" "'a\\'b'" "=")
(env "name" "'a\\'b''" "=")
(env "name" "'a\\\"b'" "=")
(env "name" "\"''\"" "=")
(env "name" "value" "=" "name1" "value1" "=" "name2" "\"value2a value2b\"" "=" "name3" "\"value3a\\n\\\"value3b\\\"\"" "=" "name4" "\"value4a\\\\nvalue4b\"" "=")
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
(from "image")
(label "maintainer" "foo@bar.com")
(env "GOPATH" "\\go")
(label "maintainer" "foo@bar.com" "")
(env "GOPATH" "\\go" "")
4 changes: 2 additions & 2 deletions frontend/dockerfile/parser/testfiles/escape-nonewline/result
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
(from "image")
(label "maintainer" "foo@bar.com")
(env "GOPATH" "\\go")
(label "maintainer" "foo@bar.com" "")
(env "GOPATH" "\\go" "")
4 changes: 2 additions & 2 deletions frontend/dockerfile/parser/testfiles/escape/result
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
(from "image")
(label "maintainer" "foo@bar.com")
(env "GOPATH" "\\go")
(label "maintainer" "foo@bar.com" "")
(env "GOPATH" "\\go" "")
2 changes: 1 addition & 1 deletion frontend/dockerfile/parser/testfiles/escapes/result
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(from "ubuntu:14.04")
(label "maintainer" "Erik \\\\Hollensbe <erik@hollensbe.org>\\\"")
(label "maintainer" "Erik \\\\Hollensbe <erik@hollensbe.org>\\\"" "")
(run "apt-get \\update && apt-get \\\"install znc -y")
(add "\\conf\\\\\"" "/.znc")
(run "foo bar baz")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(from "ubuntu:14.04")
(label "maintainer" "James Turnbull \"james@example.com\"")
(env "REFRESHED_AT" "2014-06-01")
(label "maintainer" "James Turnbull \"james@example.com\"" "")
(env "REFRESHED_AT" "2014-06-01" "")
(run "apt-get update")
(run "apt-get --no-install-recommends install -y redis-server redis-tools")
(expose "6379")
Expand Down
Loading
Loading