From d285b8447e509a79a3565170524c26ce9ab39ef2 Mon Sep 17 00:00:00 2001 From: Ernst von Oelsen Date: Mon, 3 Feb 2025 13:47:17 +0100 Subject: [PATCH] Define dockerfile instructions set centrally. Signed-off-by: Ernst von Oelsen --- pkg/docker/dockerfile/ast/line_parsers.go | 11 +- pkg/docker/dockerfile/ast/parser.go | 38 +++---- pkg/docker/dockerfile/ast/split_command.go | 2 +- pkg/docker/dockerfile/dockerfile.go | 54 +++++---- pkg/docker/dockerfile/parser/parser.go | 15 +-- pkg/docker/dockerfile/parser/parser_test.go | 25 ++++ pkg/docker/dockerfile/reverse/reverse.go | 120 +++++++------------- pkg/docker/instruction/instruction.go | 97 +++++++--------- pkg/docker/linter/check/id20008.go | 5 +- pkg/docker/linter/check/id20010.go | 5 +- pkg/docker/linter/check/id20011.go | 5 +- pkg/docker/linter/check/id20012.go | 7 +- pkg/docker/linter/check/id20014.go | 5 +- pkg/docker/linter/check/id20015.go | 5 +- pkg/docker/linter/check/id20017.go | 5 +- pkg/docker/linter/check/id20018.go | 5 +- pkg/docker/linter/check/id20019.go | 5 +- pkg/docker/linter/check/id20020.go | 11 +- pkg/docker/linter/check/id20021.go | 5 +- pkg/docker/linter/check/id20022.go | 5 +- pkg/docker/linter/linter_test.go | 59 ++++++++++ pkg/imagebuilder/imagebuilder.go | 39 ++++--- 22 files changed, 275 insertions(+), 253 deletions(-) create mode 100644 pkg/docker/dockerfile/parser/parser_test.go create mode 100644 pkg/docker/linter/linter_test.go diff --git a/pkg/docker/dockerfile/ast/line_parsers.go b/pkg/docker/dockerfile/ast/line_parsers.go index 5ed96d46d..7e6be1007 100644 --- a/pkg/docker/dockerfile/ast/line_parsers.go +++ b/pkg/docker/dockerfile/ast/line_parsers.go @@ -12,11 +12,10 @@ import ( "encoding/json" "errors" "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "strings" "unicode" "unicode/utf8" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) type NameValError struct { @@ -76,8 +75,8 @@ func parseSubCommand(rest string, d *Directive) (*Node, map[string]bool, error) return &Node{Children: []*Node{child}}, nil, nil } -// helper to parse words (i.e space delimited or quoted strings) in a statement. -// The quotes are preserved as part of this function and they are stripped later +// helper to parse words (i.e. space delimited or quoted strings) in a statement. +// The quotes are preserved as part of this function, and they are stripped later // as part of processWords(). func parseWords(rest string, d *Directive) []string { const ( @@ -224,12 +223,12 @@ func appendKeyValueNode(node, rootNode, prevNode *Node) (*Node, *Node) { } func parseEnv(rest string, d *Directive) (*Node, map[string]bool, error) { - node, err := parseNameVal(rest, instruction.Env, d) + node, err := parseNameVal(rest, df.InstTypeEnv, d) return node, nil, err } func parseLabel(rest string, d *Directive) (*Node, map[string]bool, error) { - node, err := parseNameVal(rest, instruction.Label, d) + node, err := parseNameVal(rest, df.InstTypeLabel, d) return node, nil, err } diff --git a/pkg/docker/dockerfile/ast/parser.go b/pkg/docker/dockerfile/ast/parser.go index 8ca47a9ae..e43c8e7d3 100644 --- a/pkg/docker/dockerfile/ast/parser.go +++ b/pkg/docker/dockerfile/ast/parser.go @@ -7,13 +7,13 @@ import ( "bufio" "bytes" "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "io" "regexp" "strconv" "strings" "unicode" - "github.com/mintoolkit/mint/pkg/docker/instruction" "github.com/pkg/errors" ) @@ -190,24 +190,24 @@ func init() { // functions. Errors are propagated up by Parse() and the resulting AST can // be incorporated directly into the existing AST as a next. dispatch = map[string]func(string, *Directive) (*Node, map[string]bool, error){ - instruction.Add: parseMaybeJSONToList, - instruction.Arg: parseNameOrNameVal, - instruction.Cmd: parseMaybeJSON, - instruction.Copy: parseMaybeJSONToList, - instruction.Entrypoint: parseMaybeJSON, - instruction.Env: parseEnv, - instruction.Expose: parseStringsWhitespaceDelimited, - instruction.From: parseStringsWhitespaceDelimited, - instruction.Healthcheck: parseHealthConfig, - instruction.Label: parseLabel, - instruction.Maintainer: parseString, - instruction.Onbuild: parseSubCommand, - instruction.Run: parseMaybeJSON, - instruction.Shell: parseMaybeJSON, - instruction.StopSignal: parseString, - instruction.User: parseString, - instruction.Volume: parseMaybeJSONToList, - instruction.Workdir: parseString, + df.InstTypeAdd: parseMaybeJSONToList, + df.InstTypeArg: parseNameOrNameVal, + df.InstTypeCmd: parseMaybeJSON, + df.InstTypeCopy: parseMaybeJSONToList, + df.InstTypeEntrypoint: parseMaybeJSON, + df.InstTypeEnv: parseEnv, + df.InstTypeExpose: parseStringsWhitespaceDelimited, + df.InstTypeFrom: parseStringsWhitespaceDelimited, + df.InstTypeHealthcheck: parseHealthConfig, + df.InstTypeLabel: parseLabel, + df.InstTypeMaintainer: parseString, + df.InstTypeOnbuild: parseSubCommand, + df.InstTypeRun: parseMaybeJSON, + df.InstTypeShell: parseMaybeJSON, + df.InstTypeStopSignal: parseString, + df.InstTypeUser: parseString, + df.InstTypeVolume: parseMaybeJSONToList, + df.InstTypeWorkdir: parseString, } } diff --git a/pkg/docker/dockerfile/ast/split_command.go b/pkg/docker/dockerfile/ast/split_command.go index 729c4c7a5..cb27c9bc3 100644 --- a/pkg/docker/dockerfile/ast/split_command.go +++ b/pkg/docker/dockerfile/ast/split_command.go @@ -15,7 +15,7 @@ func splitCommand(line string) (string, []string, string, error) { // Make sure we get the same results irrespective of leading/trailing spaces cmdline := tokenWhitespace.Split(strings.TrimSpace(line), 2) - cmd := strings.ToLower(cmdline[0]) + cmd := strings.ToUpper(cmdline[0]) if len(cmdline) == 2 { var err error diff --git a/pkg/docker/dockerfile/dockerfile.go b/pkg/docker/dockerfile/dockerfile.go index e58fe4ff2..ad6823f1d 100644 --- a/pkg/docker/dockerfile/dockerfile.go +++ b/pkg/docker/dockerfile/dockerfile.go @@ -15,37 +15,47 @@ import ( v "github.com/mintoolkit/mint/pkg/version" ) -// note: dup (todo: refactor) const ( + //FROM + InstTypeFrom = "FROM" //MAINTAINER: - instPrefixMaintainer = "MAINTAINER " + InstTypeMaintainer = "MAINTAINER" + InstPrefixMaintainer = "MAINTAINER " //ENTRYPOINT: - instTypeEntrypoint = "ENTRYPOINT" - instPrefixEntrypoint = "ENTRYPOINT " + InstTypeEntrypoint = "ENTRYPOINT" + InstPrefixEntrypoint = "ENTRYPOINT " //CMD: - instTypeCmd = "CMD" - instPrefixCmd = "CMD " + InstTypeCmd = "CMD" + InstPrefixCmd = "CMD " //USER: - instTypeUser = "USER" - instPrefixUser = "USER " + InstTypeUser = "USER" + InstPrefixUser = "USER " //EXPOSE: - instTypeExpose = "EXPOSE" - instPrefixExpose = "EXPOSE " + InstTypeExpose = "EXPOSE" + InstPrefixExpose = "EXPOSE " //WORKDIR: - instTypeWorkdir = "WORKDIR" - instPrefixWorkdir = "WORKDIR " + InstTypeWorkdir = "WORKDIR" + InstPrefixWorkdir = "WORKDIR " //HEALTHCHECK: - instTypeHealthcheck = "HEALTHCHECK" - instPrefixHealthcheck = "HEALTHCHECK " + InstTypeHealthcheck = "HEALTHCHECK" + InstPrefixHealthcheck = "HEALTHCHECK " + InstPrefixBasicEncHealthcheck = "HEALTHCHECK --" //ONBUILD: - instTypeOnbuild = "ONBUILD" + InstTypeOnbuild = "ONBUILD" //RUN: - instTypeRun = "RUN" - instPrefixRun = "RUN " + InstTypeRun = "RUN" + InstPrefixRun = "RUN " //ADD: - instTypeAdd = "ADD" + InstTypeAdd = "ADD" //COPY: - instTypeCopy = "COPY" + InstTypeCopy = "COPY" + + InstTypeVolume = "VOLUME" + InstTypeEnv = "ENV" + InstTypeLabel = "LABEL" + InstTypeStopSignal = "STOPSIGNAL" + InstTypeShell = "SHELL" + InstTypeArg = "ARG" //shouldn't see it as a standalone instruction ) // GenerateFromInfo builds and saves a Dockerfile file object @@ -115,20 +125,20 @@ func GenerateFromInfo( } if workingDir != "" { - dfData.WriteString(instPrefixWorkdir) + dfData.WriteString(InstPrefixWorkdir) dfData.WriteString(workingDir) dfData.WriteByte('\n') } if user != "" { - dfData.WriteString(instPrefixUser) + dfData.WriteString(InstPrefixUser) dfData.WriteString(user) dfData.WriteByte('\n') } if len(exposedPorts) > 0 { for portInfo := range exposedPorts { - dfData.WriteString(instPrefixExpose) + dfData.WriteString(InstPrefixExpose) dfData.WriteString(string(portInfo)) dfData.WriteByte('\n') } diff --git a/pkg/docker/dockerfile/parser/parser.go b/pkg/docker/dockerfile/parser/parser.go index 01ee7412a..ffef687f2 100644 --- a/pkg/docker/dockerfile/parser/parser.go +++ b/pkg/docker/dockerfile/parser/parser.go @@ -3,6 +3,7 @@ package parser import ( "errors" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "os" "path/filepath" "strconv" @@ -71,7 +72,7 @@ func FromFile(fpath string) (*spec.Dockerfile, error) { inst.Errors = append(inst.Errors, node.Errors...) } - if inst.Name == instruction.Onbuild && + if inst.Name == df.InstTypeOnbuild && node.Next != nil && len(node.Next.Children) > 0 { inst.IsOnBuild = true @@ -89,7 +90,7 @@ func FromFile(fpath string) (*spec.Dockerfile, error) { inst.IsJSONForm = true } - if inst.Name == instruction.From { + if inst.Name == df.InstTypeFrom { currentStage = spec.NewBuildStage() currentStage.FromInstruction = inst currentStage.Index = len(dockerfile.Stages) @@ -219,9 +220,9 @@ func FromFile(fpath string) (*spec.Dockerfile, error) { inst.StageIndex = instStageIndex currentStage.AllInstructions = append(currentStage.AllInstructions, inst) - if inst.Name == instruction.Onbuild { + if inst.Name == df.InstTypeOnbuild { currentStage.OnBuildInstructions = append(currentStage.OnBuildInstructions, inst) - } else if inst.Name == instruction.Copy { + } else if inst.Name == df.InstTypeCopy { for _, flag := range inst.Flags { if strings.HasPrefix(flag, "--from=") { fparts := strings.SplitN(flag, "=", 2) @@ -252,7 +253,7 @@ func FromFile(fpath string) (*spec.Dockerfile, error) { if inst.IsValid { switch inst.Name { - case instruction.Arg: + case df.InstTypeArg: for _, iarg := range inst.Args { if iarg == "" { continue @@ -268,7 +269,7 @@ func FromFile(fpath string) (*spec.Dockerfile, error) { //only one ARG is supposed to be defined, but we'll use all //the 'ARG' value count lint check will detect the extra values //the k=v ARG values are also not parsed (unlike ENV k=v values) - case instruction.Env: + case df.InstTypeEnv: for i := 0; i < len(inst.Args) && (i+1) < len(inst.Args); i += 2 { if len(inst.Args[i]) == 0 { continue @@ -280,7 +281,7 @@ func FromFile(fpath string) (*spec.Dockerfile, error) { } } } else { - if inst.Name == instruction.Arg { + if inst.Name == df.InstTypeArg { if inst.IsValid && len(inst.Args) > 0 { dockerfile.ArgInstructions = append(dockerfile.ArgInstructions, inst) parts := strings.Split(inst.Args[0], "=") diff --git a/pkg/docker/dockerfile/parser/parser_test.go b/pkg/docker/dockerfile/parser/parser_test.go new file mode 100644 index 000000000..7be15c5be --- /dev/null +++ b/pkg/docker/dockerfile/parser/parser_test.go @@ -0,0 +1,25 @@ +package parser + +import ( + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestFromFile(t *testing.T) { + tmpFile, err := os.CreateTemp(".", "Dockerfile") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + for _, s := range []string{"MAINTAINER", "Maintainer", "maintainer"} { + tmpFile.WriteString(s + " author@example.com") + Dockerfile, err := FromFile(tmpFile.Name()) + + require.NoError(t, err) + require.Equal(t, Dockerfile.AllInstructions[0].Name, "MAINTAINER") + for _, inst := range Dockerfile.AllInstructions { + require.Equal(t, inst.IsValid, true) + require.Equal(t, inst.Errors, []string(nil)) + } + } +} diff --git a/pkg/docker/dockerfile/reverse/reverse.go b/pkg/docker/dockerfile/reverse/reverse.go index 538697c55..4873a27d9 100644 --- a/pkg/docker/dockerfile/reverse/reverse.go +++ b/pkg/docker/dockerfile/reverse/reverse.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "os" "sort" "strconv" @@ -105,65 +106,24 @@ const ( runInstArgsPrefix = "|" ) -const ( - //MAINTAINER: - instTypeMaintainer = "MAINTAINER" - instPrefixMaintainer = "MAINTAINER " - //ENTRYPOINT: - instTypeEntrypoint = "ENTRYPOINT" - instPrefixEntrypoint = "ENTRYPOINT " - //CMD: - instTypeCmd = "CMD" - instPrefixCmd = "CMD " - //USER: - instTypeUser = "USER" - instPrefixUser = "USER " - //EXPOSE: - instTypeExpose = "EXPOSE" - instPrefixExpose = "EXPOSE " - //WORKDIR: - instTypeWorkdir = "WORKDIR" - instPrefixWorkdir = "WORKDIR " - //HEALTHCHECK: - instTypeHealthcheck = "HEALTHCHECK" - instPrefixHealthcheck = "HEALTHCHECK " - instPrefixBasicEncHealthcheck = "HEALTHCHECK --" - //ONBUILD: - instTypeOnbuild = "ONBUILD" - //RUN: - instTypeRun = "RUN" - instPrefixRun = "RUN " - //ADD: - instTypeAdd = "ADD" - //COPY: - instTypeCopy = "COPY" - - instTypeVolume = "VOLUME" - instTypeEnv = "ENV" - instTypeLabel = "LABEL" - instTypeStopSignal = "STOPSIGNAL" - instTypeShell = "SHELL" - instTypeArg = "ARG" //shouldn't see it as an standalone instruction -) - var instructionTypes = map[string]struct{}{ - instTypeRun: {}, - instTypeEntrypoint: {}, - instTypeCmd: {}, - instTypeUser: {}, - instTypeExpose: {}, - instTypeWorkdir: {}, - instTypeHealthcheck: {}, - instTypeOnbuild: {}, - instTypeAdd: {}, - instTypeCopy: {}, - instTypeMaintainer: {}, - instTypeVolume: {}, - instTypeEnv: {}, - instTypeLabel: {}, - instTypeStopSignal: {}, - instTypeShell: {}, - instTypeArg: {}, + df.InstTypeRun: {}, + df.InstTypeEntrypoint: {}, + df.InstTypeCmd: {}, + df.InstTypeUser: {}, + df.InstTypeExpose: {}, + df.InstTypeWorkdir: {}, + df.InstTypeHealthcheck: {}, + df.InstTypeOnbuild: {}, + df.InstTypeAdd: {}, + df.InstTypeCopy: {}, + df.InstTypeMaintainer: {}, + df.InstTypeVolume: {}, + df.InstTypeEnv: {}, + df.InstTypeLabel: {}, + df.InstTypeStopSignal: {}, + df.InstTypeShell: {}, + df.InstTypeArg: {}, } func isInstructionType(input string) bool { @@ -243,7 +203,7 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, } var rawInst string - isRunInst := strings.HasPrefix(rawLine, instPrefixRun) + isRunInst := strings.HasPrefix(rawLine, df.InstPrefixRun) if isRunInst { parts := strings.SplitN(rawLine, " ", 2) rawInst = parts[1] @@ -279,9 +239,9 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, parts[i] = partPrefix + strings.TrimSpace(parts[i]) } runDataFormatted := strings.Join(parts, " && \\\n") - inst = instPrefixRun + runDataFormatted + inst = df.InstPrefixRun + runDataFormatted } else { - inst = instPrefixRun + runData + inst = df.InstPrefixRun + runData } default: //TODO: need to refactor @@ -301,7 +261,7 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, if !processed { //default to RUN instruction in exec form isExecForm = true - inst = instPrefixRun + rawInst + inst = df.InstPrefixRun + rawInst if outArray, err := shlex.Split(rawInst); err == nil { var outJson bytes.Buffer encoder := json.NewEncoder(&outJson) @@ -318,7 +278,7 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, //NOTE: Dockerfile instructions can be any case, but the instructions from history are always uppercase cleanInst := strings.TrimSpace(inst) - if strings.HasPrefix(cleanInst, instPrefixEntrypoint) { + if strings.HasPrefix(cleanInst, df.InstPrefixEntrypoint) { cleanInst = strings.Replace(cleanInst, "&{[", "[", -1) cleanInst = strings.Replace(cleanInst, "]}", "]", -1) @@ -326,32 +286,32 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, if strings.HasPrefix(cleanInst, entrypointShellFormPrefix) { instData := strings.TrimPrefix(cleanInst, entrypointShellFormPrefix) instData = strings.TrimSuffix(instData, `"]`) - cleanInst = instPrefixEntrypoint + instData + cleanInst = df.InstPrefixEntrypoint + instData } else { isExecForm = true - instData := strings.TrimPrefix(cleanInst, instPrefixEntrypoint) + instData := strings.TrimPrefix(cleanInst, df.InstPrefixEntrypoint) instData = fixJSONArray(instData) - cleanInst = instPrefixEntrypoint + instData + cleanInst = df.InstPrefixEntrypoint + instData } } - if strings.HasPrefix(cleanInst, instPrefixCmd) { + if strings.HasPrefix(cleanInst, df.InstPrefixCmd) { cmdShellFormPrefix := `CMD ["/bin/sh" "-c" "` if strings.HasPrefix(cleanInst, cmdShellFormPrefix) { instData := strings.TrimPrefix(cleanInst, cmdShellFormPrefix) instData = strings.TrimSuffix(instData, `"]`) - cleanInst = instPrefixCmd + instData + cleanInst = df.InstPrefixCmd + instData } else { isExecForm = true - instData := strings.TrimPrefix(cleanInst, instPrefixCmd) + instData := strings.TrimPrefix(cleanInst, df.InstPrefixCmd) instData = fixJSONArray(instData) - cleanInst = instPrefixCmd + instData + cleanInst = df.InstPrefixCmd + instData } } - if strings.HasPrefix(cleanInst, instPrefixMaintainer) { + if strings.HasPrefix(cleanInst, df.InstPrefixMaintainer) { parts := strings.SplitN(cleanInst, " ", 2) if len(parts) == 2 { maintainer := strings.TrimSpace(parts[1]) @@ -362,7 +322,7 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, } } - if strings.HasPrefix(cleanInst, instPrefixUser) { + if strings.HasPrefix(cleanInst, df.InstPrefixUser) { parts := strings.SplitN(cleanInst, " ", 2) if len(parts) == 2 { userName := strings.TrimSpace(parts[1]) @@ -374,7 +334,7 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, } } - if strings.HasPrefix(cleanInst, instPrefixExpose) { + if strings.HasPrefix(cleanInst, df.InstPrefixExpose) { parts := strings.SplitN(cleanInst, " ", 2) if len(parts) == 2 { portInfo := strings.TrimSpace(parts[1]) @@ -416,7 +376,7 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, instInfo.Type = instParts[0] } - if instInfo.Type == instTypeOnbuild { + if instInfo.Type == df.InstTypeOnbuild { out.HasOnbuild = true } @@ -425,7 +385,7 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, instInfo.CommandAll = "# no instruction info" } - if instInfo.Type == instTypeRun { + if instInfo.Type == df.InstTypeRun { var cmdParts []string cmds := strings.Replace(instParts[1], "\\", "", -1) if strings.Contains(cmds, "&&") { @@ -446,12 +406,12 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, } } - if instInfo.Type == instTypeWorkdir { + if instInfo.Type == df.InstTypeWorkdir { instInfo.SystemCommands = append(instInfo.SystemCommands, fmt.Sprintf("mkdir -p %s", instParts[1])) } switch instInfo.Type { - case instTypeAdd, instTypeCopy: + case df.InstTypeAdd, df.InstTypeCopy: if strings.Contains(instInfo.Params, ":") && strings.Contains(instInfo.Params, " in ") { pparts := strings.SplitN(instInfo.Params, ":", 2) if len(pparts) == 2 { @@ -470,7 +430,7 @@ func DockerfileFromHistoryStruct(imageHistory []crt.ImageHistory) (*Dockerfile, } } - if instInfo.Type == instTypeHealthcheck { + if instInfo.Type == df.InstTypeHealthcheck { healthInst, _, err := deserialiseHealtheckInstruction(instInfo.CommandAll) if err != nil { @@ -720,13 +680,13 @@ func deserialiseHealtheckInstruction(data string) (string, *crt.HealthConfig, er //Buildah example (raw/full): // /bin/sh -c #(nop) HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost/ || exit 1 cleanInst := strings.TrimSpace(data) - if !strings.HasPrefix(cleanInst, instPrefixHealthcheck) { + if !strings.HasPrefix(cleanInst, df.InstPrefixHealthcheck) { return "", nil, ErrBadInstPrefix } var config crt.HealthConfig var strTest string - if strings.HasPrefix(cleanInst, instPrefixBasicEncHealthcheck) || !strings.Contains(cleanInst, "&{[") { + if strings.HasPrefix(cleanInst, df.InstPrefixBasicEncHealthcheck) || !strings.Contains(cleanInst, "&{[") { //handling the basic Buildah encoding var err error diff --git a/pkg/docker/instruction/instruction.go b/pkg/docker/instruction/instruction.go index 0614984a8..98e82172c 100644 --- a/pkg/docker/instruction/instruction.go +++ b/pkg/docker/instruction/instruction.go @@ -2,31 +2,10 @@ package instruction import ( + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "strings" ) -// All supported instruction names -const ( - Add = "add" - Arg = "arg" - Cmd = "cmd" - Copy = "copy" - Entrypoint = "entrypoint" - Env = "env" - Expose = "expose" - From = "from" - Healthcheck = "healthcheck" - Label = "label" - Maintainer = "maintainer" - Onbuild = "onbuild" - Run = "run" - Shell = "shell" - StopSignal = "stopsignal" - User = "user" - Volume = "volume" - Workdir = "workdir" -) - type Field struct { GlobalIndex int `json:"start_index"` StageIndex int `json:"stage_index"` @@ -57,80 +36,80 @@ type Format struct { // Specs is a map of all available instructions and their format info (by name) var Specs = map[string]Format{ - Add: { - Name: Add, + df.InstTypeAdd: { + Name: df.InstTypeAdd, SupportsFlags: true, SupportsJSONForm: true, }, - Arg: { - Name: Arg, + df.InstTypeArg: { + Name: df.InstTypeArg, SupportsNameValues: true, }, - Cmd: { - Name: Cmd, + df.InstTypeCmd: { + Name: df.InstTypeCmd, SupportsJSONForm: true, }, - Copy: { - Name: Copy, + df.InstTypeCopy: { + Name: df.InstTypeCopy, SupportsFlags: true, SupportsJSONForm: true, }, - Entrypoint: { - Name: Entrypoint, + df.InstTypeEntrypoint: { + Name: df.InstTypeEntrypoint, SupportsJSONForm: true, }, - Env: { - Name: Env, + df.InstTypeEnv: { + Name: df.InstTypeEnv, RequiresNameValues: true, }, - Expose: { - Name: Expose, + df.InstTypeExpose: { + Name: df.InstTypeExpose, }, - From: { - Name: From, + df.InstTypeFrom: { + Name: df.InstTypeFrom, SupportsFlags: true, }, - Healthcheck: { - Name: Healthcheck, + df.InstTypeHealthcheck: { + Name: df.InstTypeHealthcheck, SupportsJSONForm: true, }, - Label: { - Name: Label, + df.InstTypeLabel: { + Name: df.InstTypeLabel, RequiresNameValues: true, }, - Maintainer: { - Name: Maintainer, + df.InstTypeMaintainer: { + Name: df.InstTypeMaintainer, IsDepricated: true, }, - Onbuild: { - Name: Label, + df.InstTypeOnbuild: { + Name: df.InstTypeLabel, SupportsSubInst: true, }, - Run: { - Name: Run, + df.InstTypeRun: { + Name: df.InstTypeRun, SupportsJSONForm: true, }, - Shell: { - Name: Shell, + df.InstTypeShell: { + Name: df.InstTypeShell, SupportsJSONForm: true, }, - StopSignal: { - Name: StopSignal, + df.InstTypeStopSignal: { + Name: df.InstTypeStopSignal, }, - User: { - Name: User, + df.InstTypeUser: { + Name: df.InstTypeUser, }, - Volume: { - Name: Volume, + df.InstTypeVolume: { + Name: df.InstTypeVolume, SupportsJSONForm: true, }, - Workdir: { - Name: Workdir, + df.InstTypeWorkdir: { + Name: df.InstTypeWorkdir, }, } func IsKnown(name string) bool { - name = strings.ToLower(name) + name = strings.ToUpper(name) _, ok := Specs[name] return ok } diff --git a/pkg/docker/linter/check/id20008.go b/pkg/docker/linter/check/id20008.go index 6fabd8acb..a3888d5ab 100644 --- a/pkg/docker/linter/check/id20008.go +++ b/pkg/docker/linter/check/id20008.go @@ -3,10 +3,9 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -38,7 +37,7 @@ func (c *DeprecatedInstruction) Run(opts *Options, ctx *Context) (*Result, error Source: &c.Info, } - if instructions, ok := ctx.Dockerfile.InstructionsByType[instruction.Maintainer]; ok { + if instructions, ok := ctx.Dockerfile.InstructionsByType[df.InstTypeMaintainer]; ok { result.Hit = true result.Message = c.MainMessage diff --git a/pkg/docker/linter/check/id20010.go b/pkg/docker/linter/check/id20010.go index f3f3f44e6..90d49811f 100644 --- a/pkg/docker/linter/check/id20010.go +++ b/pkg/docker/linter/check/id20010.go @@ -3,10 +3,9 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -39,7 +38,7 @@ func (c *MultipleEntrypointInstructions) Run(opts *Options, ctx *Context) (*Resu } for _, stage := range ctx.Dockerfile.Stages { - if instructions, ok := stage.CurrentInstructionsByType[instruction.Entrypoint]; ok { + if instructions, ok := stage.CurrentInstructionsByType[df.InstTypeEntrypoint]; ok { if len(instructions) > 1 { if !result.Hit { result.Hit = true diff --git a/pkg/docker/linter/check/id20011.go b/pkg/docker/linter/check/id20011.go index 34ad8a2c3..430066080 100644 --- a/pkg/docker/linter/check/id20011.go +++ b/pkg/docker/linter/check/id20011.go @@ -3,10 +3,9 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -39,7 +38,7 @@ func (c *MultipleCmdInstructions) Run(opts *Options, ctx *Context) (*Result, err } for _, stage := range ctx.Dockerfile.Stages { - if instructions, ok := stage.CurrentInstructionsByType[instruction.Cmd]; ok { + if instructions, ok := stage.CurrentInstructionsByType[df.InstTypeCmd]; ok { if len(instructions) > 1 { if !result.Hit { result.Hit = true diff --git a/pkg/docker/linter/check/id20012.go b/pkg/docker/linter/check/id20012.go index 22f3e8efc..1a97d8e69 100644 --- a/pkg/docker/linter/check/id20012.go +++ b/pkg/docker/linter/check/id20012.go @@ -3,10 +3,9 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -24,8 +23,8 @@ func init() { }, }, Names: []string{ - instruction.Entrypoint, - instruction.Cmd, + df.InstTypeEntrypoint, + df.InstTypeCmd, }, } diff --git a/pkg/docker/linter/check/id20014.go b/pkg/docker/linter/check/id20014.go index e0095f0a0..800c31cca 100644 --- a/pkg/docker/linter/check/id20014.go +++ b/pkg/docker/linter/check/id20014.go @@ -3,10 +3,9 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -40,7 +39,7 @@ func (c *NoWorkdirPath) Run(opts *Options, ctx *Context) (*Result, error) { } for _, stage := range ctx.Dockerfile.Stages { - if instructions, ok := stage.CurrentInstructionsByType[instruction.Workdir]; ok { + if instructions, ok := stage.CurrentInstructionsByType[df.InstTypeWorkdir]; ok { for _, inst := range instructions { if len(inst.ArgsRaw) == 0 { if !result.Hit { diff --git a/pkg/docker/linter/check/id20015.go b/pkg/docker/linter/check/id20015.go index 58f39b629..a02c13571 100644 --- a/pkg/docker/linter/check/id20015.go +++ b/pkg/docker/linter/check/id20015.go @@ -3,11 +3,10 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "strings" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -40,7 +39,7 @@ func (c *RelativeWorkdir) Run(opts *Options, ctx *Context) (*Result, error) { } for _, stage := range ctx.Dockerfile.Stages { - if instructions, ok := stage.CurrentInstructionsByType[instruction.Workdir]; ok { + if instructions, ok := stage.CurrentInstructionsByType[df.InstTypeWorkdir]; ok { for _, inst := range instructions { if len(inst.ArgsRaw) > 0 { workdirPath := inst.ArgsRaw diff --git a/pkg/docker/linter/check/id20017.go b/pkg/docker/linter/check/id20017.go index 62a0e2eac..f7b9cf23d 100644 --- a/pkg/docker/linter/check/id20017.go +++ b/pkg/docker/linter/check/id20017.go @@ -3,11 +3,10 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "strings" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -43,7 +42,7 @@ func (c *LastUserRoot) Run(opts *Options, ctx *Context) (*Result, error) { if lastStageIdx > -1 { stage := ctx.Dockerfile.Stages[lastStageIdx] - if instructions, ok := stage.CurrentInstructionsByType[instruction.User]; ok { + if instructions, ok := stage.CurrentInstructionsByType[df.InstTypeUser]; ok { lastUserIdx := len(instructions) - 1 if lastUserIdx > -1 { inst := instructions[lastUserIdx] diff --git a/pkg/docker/linter/check/id20018.go b/pkg/docker/linter/check/id20018.go index 65d824515..62216d281 100644 --- a/pkg/docker/linter/check/id20018.go +++ b/pkg/docker/linter/check/id20018.go @@ -3,12 +3,11 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "strings" "github.com/google/shlex" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -41,7 +40,7 @@ func (c *PyPipInstallLatest) Run(opts *Options, ctx *Context) (*Result, error) { } for _, stage := range ctx.Dockerfile.Stages { - if instructions, ok := stage.CurrentInstructionsByType[instruction.Run]; ok { + if instructions, ok := stage.CurrentInstructionsByType[df.InstTypeRun]; ok { for _, inst := range instructions { if len(inst.ArgsRaw) == 0 { continue diff --git a/pkg/docker/linter/check/id20019.go b/pkg/docker/linter/check/id20019.go index 9fcac5964..d7d462160 100644 --- a/pkg/docker/linter/check/id20019.go +++ b/pkg/docker/linter/check/id20019.go @@ -3,6 +3,7 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" log "github.com/sirupsen/logrus" @@ -43,9 +44,9 @@ func (c *UnnecessaryLayer) Run(opts *Options, ctx *Context) (*Result, error) { var prevInst *instruction.Field for _, inst := range stage.CurrentInstructions { - if inst.Name == instruction.Run && + if inst.Name == df.InstTypeRun && prevInst != nil && - prevInst.Name == instruction.Run { + prevInst.Name == df.InstTypeRun { //very primitive unnecessary layer that only checks the previous RUN instruction //should have a separate check with more advanced unnecessary layer detection if !result.Hit { diff --git a/pkg/docker/linter/check/id20020.go b/pkg/docker/linter/check/id20020.go index af9d68b73..7cc83d286 100644 --- a/pkg/docker/linter/check/id20020.go +++ b/pkg/docker/linter/check/id20020.go @@ -3,10 +3,9 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) const ( @@ -51,10 +50,10 @@ func (c *TooManyLayers) Run(opts *Options, ctx *Context) (*Result, error) { layerCount := 0 for _, inst := range stage.CurrentInstructions { switch inst.Name { - case instruction.Run, - instruction.Workdir, - instruction.Copy, - instruction.Add: + case df.InstTypeRun, + df.InstTypeWorkdir, + df.InstTypeCopy, + df.InstTypeAdd: layerCount++ } } diff --git a/pkg/docker/linter/check/id20021.go b/pkg/docker/linter/check/id20021.go index a8a97408b..d1906441b 100644 --- a/pkg/docker/linter/check/id20021.go +++ b/pkg/docker/linter/check/id20021.go @@ -3,12 +3,11 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "strings" "github.com/google/shlex" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -41,7 +40,7 @@ func (c *SeparateRemove) Run(opts *Options, ctx *Context) (*Result, error) { } for _, stage := range ctx.Dockerfile.Stages { - if instructions, ok := stage.CurrentInstructionsByType[instruction.Run]; ok { + if instructions, ok := stage.CurrentInstructionsByType[df.InstTypeRun]; ok { for _, inst := range instructions { if len(inst.ArgsRaw) == 0 { continue diff --git a/pkg/docker/linter/check/id20022.go b/pkg/docker/linter/check/id20022.go index 696dca163..29ed71df2 100644 --- a/pkg/docker/linter/check/id20022.go +++ b/pkg/docker/linter/check/id20022.go @@ -3,12 +3,11 @@ package check import ( "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "strings" "github.com/google/shlex" log "github.com/sirupsen/logrus" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) func init() { @@ -53,7 +52,7 @@ func (check *BadContainerCommands) Run(opts *Options, ctx *Context) (*Result, er } for _, stage := range ctx.Dockerfile.Stages { - if instructions, ok := stage.CurrentInstructionsByType[instruction.Run]; ok { + if instructions, ok := stage.CurrentInstructionsByType[df.InstTypeRun]; ok { for _, inst := range instructions { if len(inst.ArgsRaw) == 0 { continue diff --git a/pkg/docker/linter/linter_test.go b/pkg/docker/linter/linter_test.go new file mode 100644 index 000000000..df2cfc2ab --- /dev/null +++ b/pkg/docker/linter/linter_test.go @@ -0,0 +1,59 @@ +package linter + +import ( + "github.com/mintoolkit/mint/pkg/docker/dockerfile/parser" + "github.com/mintoolkit/mint/pkg/docker/linter/check" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestExecute(t *testing.T) { + tmpFile, err := os.CreateTemp(".", "Dockerfile") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + tmpFile.WriteString(`FROM scratch as final +MAINTAINER author@example.com +LABEL stage=unittest +ADD . . +COPY . . +ENV +ARG +EXPOSE +RUN +HEALTHCHECK +ONBUILD +SHELL +ENTRYPOINT ["/bin/sh"] +VOLUME +WORKDIR +USER +STOPSIGNAL +CMD ["/bin/sh", "sleep"] +`) + Dockerfile, err := parser.FromFile(tmpFile.Name()) + + require.NoError(t, err) + require.Equal(t, len(Dockerfile.AllInstructions), 18) + + options := Options{ + tmpFile.Name(), + Dockerfile, + true, + ".", + true, + nil, + CheckSelector{}, + map[string]*check.Options{}, + } + + report, err := Execute(options) + + require.NoError(t, err) + require.NotEqual(t, report, nil) + for id := range report.Hits { + require.NotEqual(t, id, "ID.20000") //verify that no invalid instructions are detected + require.NotEqual(t, id, "ID.20007") //verify that no unknown instructions are detected + } +} diff --git a/pkg/imagebuilder/imagebuilder.go b/pkg/imagebuilder/imagebuilder.go index 12eef188b..549504c55 100644 --- a/pkg/imagebuilder/imagebuilder.go +++ b/pkg/imagebuilder/imagebuilder.go @@ -3,12 +3,11 @@ package imagebuilder import ( "errors" "fmt" + df "github.com/mintoolkit/mint/pkg/docker/dockerfile" "io" "os" "strings" "time" - - "github.com/mintoolkit/mint/pkg/docker/instruction" ) var ( @@ -208,46 +207,46 @@ func SimpleBuildOptionsFromDockerfileData(data string, ignoreExeInstructions boo instName := strings.ToLower(parts[0]) switch instName { - case instruction.Entrypoint: + case df.InstTypeEntrypoint: //options.Entrypoint []string - case instruction.Cmd: + case df.InstTypeCmd: //options.Cmd []string - case instruction.Env: + case df.InstTypeEnv: //options.EnvVars []string - case instruction.Expose: + case df.InstTypeExpose: //options.ExposedPorts map[string]struct{} - case instruction.Label: + case df.InstTypeLabel: //options.Labels map[string]string - case instruction.User: + case df.InstTypeUser: //options.User = parts[1] options.ImageConfig.Config.User = parts[1] - case instruction.Volume: + case df.InstTypeVolume: //options.Volumes map[string]struct{} - case instruction.Workdir: + case df.InstTypeWorkdir: //options.WorkDir = parts[1] options.ImageConfig.Config.WorkingDir = parts[1] - case instruction.Add: + case df.InstTypeAdd: //support tar files (ignore other things, at leas, for now) //options.Layers []LayerDataInfo - case instruction.Copy: + case df.InstTypeCopy: //options.Layers []LayerDataInfo - case instruction.Maintainer: + case df.InstTypeMaintainer: //TBD - case instruction.Healthcheck: + case df.InstTypeHealthcheck: //TBD - case instruction.From: + case df.InstTypeFrom: //options.From string - case instruction.Arg: + case df.InstTypeArg: //TODO - case instruction.Run: + case df.InstTypeRun: if !ignoreExeInstructions { return nil, fmt.Errorf("RUN instructions are not supported") } - case instruction.Onbuild: + case df.InstTypeOnbuild: //IGNORE - case instruction.Shell: + case df.InstTypeShell: //IGNORE - case instruction.StopSignal: + case df.InstTypeStopSignal: //IGNORE } }