123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- package instructions
- import (
- "fmt"
- "regexp"
- "sort"
- "strconv"
- "strings"
- "time"
- "github.com/docker/docker/api/types/container"
- "github.com/docker/docker/api/types/strslice"
- "github.com/docker/docker/builder/dockerfile/command"
- "github.com/docker/docker/builder/dockerfile/parser"
- "github.com/pkg/errors"
- )
- type parseRequest struct {
- command string
- args []string
- attributes map[string]bool
- flags *BFlags
- original string
- }
- func nodeArgs(node *parser.Node) []string {
- result := []string{}
- for ; node.Next != nil; node = node.Next {
- arg := node.Next
- if len(arg.Children) == 0 {
- result = append(result, arg.Value)
- } else if len(arg.Children) == 1 {
- //sub command
- result = append(result, arg.Children[0].Value)
- result = append(result, nodeArgs(arg.Children[0])...)
- }
- }
- return result
- }
- func newParseRequestFromNode(node *parser.Node) parseRequest {
- return parseRequest{
- command: node.Value,
- args: nodeArgs(node),
- attributes: node.Attributes,
- original: node.Original,
- flags: NewBFlagsWithArgs(node.Flags),
- }
- }
- // ParseInstruction converts an AST to a typed instruction (either a command or a build stage beginning when encountering a `FROM` statement)
- func ParseInstruction(node *parser.Node) (interface{}, error) {
- req := newParseRequestFromNode(node)
- switch node.Value {
- case command.Env:
- return parseEnv(req)
- case command.Maintainer:
- return parseMaintainer(req)
- case command.Label:
- return parseLabel(req)
- case command.Add:
- return parseAdd(req)
- case command.Copy:
- return parseCopy(req)
- case command.From:
- return parseFrom(req)
- case command.Onbuild:
- return parseOnBuild(req)
- case command.Workdir:
- return parseWorkdir(req)
- case command.Run:
- return parseRun(req)
- case command.Cmd:
- return parseCmd(req)
- case command.Healthcheck:
- return parseHealthcheck(req)
- case command.Entrypoint:
- return parseEntrypoint(req)
- case command.Expose:
- return parseExpose(req)
- case command.User:
- return parseUser(req)
- case command.Volume:
- return parseVolume(req)
- case command.StopSignal:
- return parseStopSignal(req)
- case command.Arg:
- return parseArg(req)
- case command.Shell:
- return parseShell(req)
- }
- return nil, &UnknownInstruction{Instruction: node.Value, Line: node.StartLine}
- }
- // ParseCommand converts an AST to a typed Command
- func ParseCommand(node *parser.Node) (Command, error) {
- s, err := ParseInstruction(node)
- if err != nil {
- return nil, err
- }
- if c, ok := s.(Command); ok {
- return c, nil
- }
- return nil, errors.Errorf("%T is not a command type", s)
- }
- // UnknownInstruction represents an error occuring when a command is unresolvable
- type UnknownInstruction struct {
- Line int
- Instruction string
- }
- func (e *UnknownInstruction) Error() string {
- return fmt.Sprintf("unknown instruction: %s", strings.ToUpper(e.Instruction))
- }
- // IsUnknownInstruction checks if the error is an UnknownInstruction or a parseError containing an UnknownInstruction
- func IsUnknownInstruction(err error) bool {
- _, ok := err.(*UnknownInstruction)
- if !ok {
- var pe *parseError
- if pe, ok = err.(*parseError); ok {
- _, ok = pe.inner.(*UnknownInstruction)
- }
- }
- return ok
- }
- type parseError struct {
- inner error
- node *parser.Node
- }
- func (e *parseError) Error() string {
- return fmt.Sprintf("Dockerfile parse error line %d: %v", e.node.StartLine, e.inner.Error())
- }
- // Parse a docker file into a collection of buildable stages
- func Parse(ast *parser.Node) (stages []Stage, metaArgs []ArgCommand, err error) {
- for _, n := range ast.Children {
- cmd, err := ParseInstruction(n)
- if err != nil {
- return nil, nil, &parseError{inner: err, node: n}
- }
- if len(stages) == 0 {
- // meta arg case
- if a, isArg := cmd.(*ArgCommand); isArg {
- metaArgs = append(metaArgs, *a)
- continue
- }
- }
- switch c := cmd.(type) {
- case *Stage:
- stages = append(stages, *c)
- case Command:
- stage, err := CurrentStage(stages)
- if err != nil {
- return nil, nil, err
- }
- stage.AddCommand(c)
- default:
- return nil, nil, errors.Errorf("%T is not a command type", cmd)
- }
- }
- return stages, metaArgs, nil
- }
- func parseKvps(args []string, cmdName string) (KeyValuePairs, error) {
- if len(args) == 0 {
- return nil, errAtLeastOneArgument(cmdName)
- }
- if len(args)%2 != 0 {
- // should never get here, but just in case
- return nil, errTooManyArguments(cmdName)
- }
- var res KeyValuePairs
- for j := 0; j < len(args); j += 2 {
- if len(args[j]) == 0 {
- return nil, errBlankCommandNames(cmdName)
- }
- name := args[j]
- value := args[j+1]
- res = append(res, KeyValuePair{Key: name, Value: value})
- }
- return res, nil
- }
- func parseEnv(req parseRequest) (*EnvCommand, error) {
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- envs, err := parseKvps(req.args, "ENV")
- if err != nil {
- return nil, err
- }
- return &EnvCommand{
- Env: envs,
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseMaintainer(req parseRequest) (*MaintainerCommand, error) {
- if len(req.args) != 1 {
- return nil, errExactlyOneArgument("MAINTAINER")
- }
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- return &MaintainerCommand{
- Maintainer: req.args[0],
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseLabel(req parseRequest) (*LabelCommand, error) {
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- labels, err := parseKvps(req.args, "LABEL")
- if err != nil {
- return nil, err
- }
- return &LabelCommand{
- Labels: labels,
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseAdd(req parseRequest) (*AddCommand, error) {
- if len(req.args) < 2 {
- return nil, errAtLeastTwoArguments("ADD")
- }
- flChown := req.flags.AddString("chown", "")
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- return &AddCommand{
- SourcesAndDest: SourcesAndDest(req.args),
- withNameAndCode: newWithNameAndCode(req),
- Chown: flChown.Value,
- }, nil
- }
- func parseCopy(req parseRequest) (*CopyCommand, error) {
- if len(req.args) < 2 {
- return nil, errAtLeastTwoArguments("COPY")
- }
- flChown := req.flags.AddString("chown", "")
- flFrom := req.flags.AddString("from", "")
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- return &CopyCommand{
- SourcesAndDest: SourcesAndDest(req.args),
- From: flFrom.Value,
- withNameAndCode: newWithNameAndCode(req),
- Chown: flChown.Value,
- }, nil
- }
- func parseFrom(req parseRequest) (*Stage, error) {
- stageName, err := parseBuildStageName(req.args)
- if err != nil {
- return nil, err
- }
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- code := strings.TrimSpace(req.original)
- return &Stage{
- BaseName: req.args[0],
- Name: stageName,
- SourceCode: code,
- Commands: []Command{},
- }, nil
- }
- func parseBuildStageName(args []string) (string, error) {
- stageName := ""
- switch {
- case len(args) == 3 && strings.EqualFold(args[1], "as"):
- stageName = strings.ToLower(args[2])
- if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok {
- return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", stageName)
- }
- case len(args) != 1:
- return "", errors.New("FROM requires either one or three arguments")
- }
- return stageName, nil
- }
- func parseOnBuild(req parseRequest) (*OnbuildCommand, error) {
- if len(req.args) == 0 {
- return nil, errAtLeastOneArgument("ONBUILD")
- }
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0]))
- switch strings.ToUpper(triggerInstruction) {
- case "ONBUILD":
- return nil, errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
- case "MAINTAINER", "FROM":
- return nil, fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
- }
- original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "")
- return &OnbuildCommand{
- Expression: original,
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseWorkdir(req parseRequest) (*WorkdirCommand, error) {
- if len(req.args) != 1 {
- return nil, errExactlyOneArgument("WORKDIR")
- }
- err := req.flags.Parse()
- if err != nil {
- return nil, err
- }
- return &WorkdirCommand{
- Path: req.args[0],
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseShellDependentCommand(req parseRequest, emptyAsNil bool) ShellDependantCmdLine {
- args := handleJSONArgs(req.args, req.attributes)
- cmd := strslice.StrSlice(args)
- if emptyAsNil && len(cmd) == 0 {
- cmd = nil
- }
- return ShellDependantCmdLine{
- CmdLine: cmd,
- PrependShell: !req.attributes["json"],
- }
- }
- func parseRun(req parseRequest) (*RunCommand, error) {
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- return &RunCommand{
- ShellDependantCmdLine: parseShellDependentCommand(req, false),
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseCmd(req parseRequest) (*CmdCommand, error) {
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- return &CmdCommand{
- ShellDependantCmdLine: parseShellDependentCommand(req, false),
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseEntrypoint(req parseRequest) (*EntrypointCommand, error) {
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- cmd := &EntrypointCommand{
- ShellDependantCmdLine: parseShellDependentCommand(req, true),
- withNameAndCode: newWithNameAndCode(req),
- }
- return cmd, nil
- }
- // parseOptInterval(flag) is the duration of flag.Value, or 0 if
- // empty. An error is reported if the value is given and less than minimum duration.
- func parseOptInterval(f *Flag) (time.Duration, error) {
- s := f.Value
- if s == "" {
- return 0, nil
- }
- d, err := time.ParseDuration(s)
- if err != nil {
- return 0, err
- }
- if d < container.MinimumDuration {
- return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration)
- }
- return d, nil
- }
- func parseHealthcheck(req parseRequest) (*HealthCheckCommand, error) {
- if len(req.args) == 0 {
- return nil, errAtLeastOneArgument("HEALTHCHECK")
- }
- cmd := &HealthCheckCommand{
- withNameAndCode: newWithNameAndCode(req),
- }
- typ := strings.ToUpper(req.args[0])
- args := req.args[1:]
- if typ == "NONE" {
- if len(args) != 0 {
- return nil, errors.New("HEALTHCHECK NONE takes no arguments")
- }
- test := strslice.StrSlice{typ}
- cmd.Health = &container.HealthConfig{
- Test: test,
- }
- } else {
- healthcheck := container.HealthConfig{}
- flInterval := req.flags.AddString("interval", "")
- flTimeout := req.flags.AddString("timeout", "")
- flStartPeriod := req.flags.AddString("start-period", "")
- flRetries := req.flags.AddString("retries", "")
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- switch typ {
- case "CMD":
- cmdSlice := handleJSONArgs(args, req.attributes)
- if len(cmdSlice) == 0 {
- return nil, errors.New("Missing command after HEALTHCHECK CMD")
- }
- if !req.attributes["json"] {
- typ = "CMD-SHELL"
- }
- healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...))
- default:
- return nil, fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ)
- }
- interval, err := parseOptInterval(flInterval)
- if err != nil {
- return nil, err
- }
- healthcheck.Interval = interval
- timeout, err := parseOptInterval(flTimeout)
- if err != nil {
- return nil, err
- }
- healthcheck.Timeout = timeout
- startPeriod, err := parseOptInterval(flStartPeriod)
- if err != nil {
- return nil, err
- }
- healthcheck.StartPeriod = startPeriod
- if flRetries.Value != "" {
- retries, err := strconv.ParseInt(flRetries.Value, 10, 32)
- if err != nil {
- return nil, err
- }
- if retries < 1 {
- return nil, fmt.Errorf("--retries must be at least 1 (not %d)", retries)
- }
- healthcheck.Retries = int(retries)
- } else {
- healthcheck.Retries = 0
- }
- cmd.Health = &healthcheck
- }
- return cmd, nil
- }
- func parseExpose(req parseRequest) (*ExposeCommand, error) {
- portsTab := req.args
- if len(req.args) == 0 {
- return nil, errAtLeastOneArgument("EXPOSE")
- }
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- sort.Strings(portsTab)
- return &ExposeCommand{
- Ports: portsTab,
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseUser(req parseRequest) (*UserCommand, error) {
- if len(req.args) != 1 {
- return nil, errExactlyOneArgument("USER")
- }
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- return &UserCommand{
- User: req.args[0],
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseVolume(req parseRequest) (*VolumeCommand, error) {
- if len(req.args) == 0 {
- return nil, errAtLeastOneArgument("VOLUME")
- }
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- cmd := &VolumeCommand{
- withNameAndCode: newWithNameAndCode(req),
- }
- for _, v := range req.args {
- v = strings.TrimSpace(v)
- if v == "" {
- return nil, errors.New("VOLUME specified can not be an empty string")
- }
- cmd.Volumes = append(cmd.Volumes, v)
- }
- return cmd, nil
- }
- func parseStopSignal(req parseRequest) (*StopSignalCommand, error) {
- if len(req.args) != 1 {
- return nil, errExactlyOneArgument("STOPSIGNAL")
- }
- sig := req.args[0]
- cmd := &StopSignalCommand{
- Signal: sig,
- withNameAndCode: newWithNameAndCode(req),
- }
- return cmd, nil
- }
- func parseArg(req parseRequest) (*ArgCommand, error) {
- if len(req.args) != 1 {
- return nil, errExactlyOneArgument("ARG")
- }
- var (
- name string
- newValue *string
- )
- arg := req.args[0]
- // 'arg' can just be a name or name-value pair. Note that this is different
- // from 'env' that handles the split of name and value at the parser level.
- // The reason for doing it differently for 'arg' is that we support just
- // defining an arg and not assign it a value (while 'env' always expects a
- // name-value pair). If possible, it will be good to harmonize the two.
- if strings.Contains(arg, "=") {
- parts := strings.SplitN(arg, "=", 2)
- if len(parts[0]) == 0 {
- return nil, errBlankCommandNames("ARG")
- }
- name = parts[0]
- newValue = &parts[1]
- } else {
- name = arg
- }
- return &ArgCommand{
- Key: name,
- Value: newValue,
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- }
- func parseShell(req parseRequest) (*ShellCommand, error) {
- if err := req.flags.Parse(); err != nil {
- return nil, err
- }
- shellSlice := handleJSONArgs(req.args, req.attributes)
- switch {
- case len(shellSlice) == 0:
- // SHELL []
- return nil, errAtLeastOneArgument("SHELL")
- case req.attributes["json"]:
- // SHELL ["powershell", "-command"]
- return &ShellCommand{
- Shell: strslice.StrSlice(shellSlice),
- withNameAndCode: newWithNameAndCode(req),
- }, nil
- default:
- // SHELL powershell -command - not JSON
- return nil, errNotJSON("SHELL", req.original)
- }
- }
- func errAtLeastOneArgument(command string) error {
- return errors.Errorf("%s requires at least one argument", command)
- }
- func errExactlyOneArgument(command string) error {
- return errors.Errorf("%s requires exactly one argument", command)
- }
- func errAtLeastTwoArguments(command string) error {
- return errors.Errorf("%s requires at least two arguments", command)
- }
- func errBlankCommandNames(command string) error {
- return errors.Errorf("%s names can not be blank", command)
- }
- func errTooManyArguments(command string) error {
- return errors.Errorf("Bad input to %s, too many arguments", command)
- }
|