package build import ( "bufio" "fmt" "io" "strings" ) // Instruction is one logical Dockerfile line: the keyword uppercased // (docker's own convention is upper case, but the syntax is // case-insensitive) and the rest of the line with continuations // joined. Line is the physical line the instruction started on, so an // error names the place the author sees. type Instruction struct { Keyword string Args string Line int } // support says what the builder does with an instruction. type support int const ( // supportTree changes the work tree: FROM, RUN, COPY. supportTree support = iota // supportMeta goes into image.json through store.ApplyChange. supportMeta // supportLater is a real Dockerfile instruction that a later stage // of RFC 0006 implements. supportLater // supportRefused will not be implemented, and says why. supportRefused ) // instructions is the whole instruction surface in one table: what is // supported now, what a later stage brings, and what is refused with // its reason. A keyword outside it is not a Dockerfile instruction. var instructions = map[string]struct { support support note string }{ "FROM": {supportTree, ""}, "RUN": {supportTree, ""}, "COPY": {supportTree, ""}, "ENTRYPOINT": {supportMeta, ""}, "CMD": {supportMeta, ""}, "ENV": {supportMeta, ""}, "WORKDIR": {supportMeta, ""}, "USER": {supportMeta, ""}, "VOLUME": {supportMeta, ""}, "LABEL": {supportMeta, ""}, "ARG": {supportLater, "ARG and ${} expansion are stage 2 of RFC 0006"}, "SHELL": {supportLater, "SHELL is stage 2 of RFC 0006; RUN uses /bin/sh -c"}, "ADD": {supportLater, "ADD is stage 2 of RFC 0006; COPY handles local files"}, "EXPOSE": {supportRefused, "the sandbox shares the host's network by contract, so there is no port to publish (RFC 0002)"}, "HEALTHCHECK": {supportRefused, "there is no daemon to poll a health check; werkdock runs no background probe"}, "STOPSIGNAL": {supportRefused, "stop sends SIGTERM and then SIGKILL after the timeout (RFC 0005); no per-image signal is read"}, "ONBUILD": {supportRefused, "an ONBUILD trigger needs a builder that re-reads it from the image config on a later FROM"}, "MAINTAINER": {supportRefused, "deprecated by docker itself; use LABEL maintainer=\"...\""}, } // Parse reads a Dockerfile into its instructions: `#` comment lines // and blank lines are dropped, a line ending in `\` continues on the // next one (comment lines inside a continuation are dropped too, as // docker does). Instructions a later stage brings and instructions // werkdock refuses both fail here, with the reason and the line — a // build that cannot be finished must not start by copying a 2 GiB // tree. func Parse(r io.Reader) ([]Instruction, error) { var result []Instruction var pending strings.Builder startLine := 0 flush := func() error { if startLine == 0 { return nil } instruction, err := parseLine(pending.String(), startLine) pending.Reset() startLine = 0 if err != nil { return err } result = append(result, instruction) return nil } scanner := bufio.NewScanner(r) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) for line := 1; scanner.Scan(); line++ { text := scanner.Text() trimmed := strings.TrimSpace(text) if strings.HasPrefix(trimmed, "#") { continue // a comment, inside a continuation as well } if startLine == 0 && trimmed == "" { continue } continues := strings.HasSuffix(trimmed, "\\") trimmed = strings.TrimSpace(strings.TrimSuffix(trimmed, "\\")) if startLine == 0 { startLine = line } else if trimmed != "" { pending.WriteString(" ") } pending.WriteString(trimmed) if !continues { if err := flush(); err != nil { return nil, err } } } if err := scanner.Err(); err != nil { return nil, err } // A file whose last line ends in a backslash: take what there is, // as docker does. if err := flush(); err != nil { return nil, err } if len(result) == 0 { return nil, fmt.Errorf("the Dockerfile holds no instruction") } if result[0].Keyword != "FROM" { return nil, fmt.Errorf("line %d: a Dockerfile begins with FROM, not %s", result[0].Line, result[0].Keyword) } return result, nil } func parseLine(text string, line int) (Instruction, error) { keyword, args, _ := strings.Cut(text, " ") keyword = strings.ToUpper(strings.TrimSpace(keyword)) args = strings.TrimSpace(args) known, ok := instructions[keyword] if !ok { return Instruction{}, fmt.Errorf("line %d: unknown instruction %s", line, keyword) } switch known.support { case supportLater: return Instruction{}, fmt.Errorf("line %d: %s is not supported yet: %s", line, keyword, known.note) case supportRefused: return Instruction{}, fmt.Errorf("line %d: %s is not supported: %s", line, keyword, known.note) } if args == "" { return Instruction{}, fmt.Errorf("line %d: %s needs an argument", line, keyword) } if keyword == "RUN" && strings.HasPrefix(args, "--") { // BuildKit's flags; as a shell command they would fail with a message that says nothing flag, _, _ := strings.Cut(strings.Fields(args)[0], "=") return Instruction{}, fmt.Errorf("line %d: RUN %s is not supported: %s", line, flag, runFlagNote(flag)) } return Instruction{Keyword: keyword, Args: args, Line: line}, nil } // runFlagNote is why a RUN flag of BuildKit is refused. func runFlagNote(flag string) string { if flag == "--mount" { return "a RUN step cannot mount anything (CapEff is 0 in the sandbox, RFC 0006); every mount stands in bwrap's argv" } return "it is BuildKit's, and werkdock build is no BuildKit (RFC 0006)" } // changesTree reports whether an instruction is one of the three that // touch the work tree; the others are metadata for image.json. func (i Instruction) changesTree() bool { return instructions[i.Keyword].support == supportTree }