github.com/lazyboychen7/engine@v17.12.1-ce-rc2+incompatible/builder/dockerfile/instructions/parse.go (about) 1 package instructions 2 3 import ( 4 "fmt" 5 "regexp" 6 "sort" 7 "strconv" 8 "strings" 9 "time" 10 11 "github.com/docker/docker/api/types/container" 12 "github.com/docker/docker/api/types/strslice" 13 "github.com/docker/docker/builder/dockerfile/command" 14 "github.com/docker/docker/builder/dockerfile/parser" 15 "github.com/pkg/errors" 16 ) 17 18 type parseRequest struct { 19 command string 20 args []string 21 attributes map[string]bool 22 flags *BFlags 23 original string 24 } 25 26 func nodeArgs(node *parser.Node) []string { 27 result := []string{} 28 for ; node.Next != nil; node = node.Next { 29 arg := node.Next 30 if len(arg.Children) == 0 { 31 result = append(result, arg.Value) 32 } else if len(arg.Children) == 1 { 33 //sub command 34 result = append(result, arg.Children[0].Value) 35 result = append(result, nodeArgs(arg.Children[0])...) 36 } 37 } 38 return result 39 } 40 41 func newParseRequestFromNode(node *parser.Node) parseRequest { 42 return parseRequest{ 43 command: node.Value, 44 args: nodeArgs(node), 45 attributes: node.Attributes, 46 original: node.Original, 47 flags: NewBFlagsWithArgs(node.Flags), 48 } 49 } 50 51 // ParseInstruction converts an AST to a typed instruction (either a command or a build stage beginning when encountering a `FROM` statement) 52 func ParseInstruction(node *parser.Node) (interface{}, error) { 53 req := newParseRequestFromNode(node) 54 switch node.Value { 55 case command.Env: 56 return parseEnv(req) 57 case command.Maintainer: 58 return parseMaintainer(req) 59 case command.Label: 60 return parseLabel(req) 61 case command.Add: 62 return parseAdd(req) 63 case command.Copy: 64 return parseCopy(req) 65 case command.From: 66 return parseFrom(req) 67 case command.Onbuild: 68 return parseOnBuild(req) 69 case command.Workdir: 70 return parseWorkdir(req) 71 case command.Run: 72 return parseRun(req) 73 case command.Cmd: 74 return parseCmd(req) 75 case command.Healthcheck: 76 return parseHealthcheck(req) 77 case command.Entrypoint: 78 return parseEntrypoint(req) 79 case command.Expose: 80 return parseExpose(req) 81 case command.User: 82 return parseUser(req) 83 case command.Volume: 84 return parseVolume(req) 85 case command.StopSignal: 86 return parseStopSignal(req) 87 case command.Arg: 88 return parseArg(req) 89 case command.Shell: 90 return parseShell(req) 91 } 92 93 return nil, &UnknownInstruction{Instruction: node.Value, Line: node.StartLine} 94 } 95 96 // ParseCommand converts an AST to a typed Command 97 func ParseCommand(node *parser.Node) (Command, error) { 98 s, err := ParseInstruction(node) 99 if err != nil { 100 return nil, err 101 } 102 if c, ok := s.(Command); ok { 103 return c, nil 104 } 105 return nil, errors.Errorf("%T is not a command type", s) 106 } 107 108 // UnknownInstruction represents an error occurring when a command is unresolvable 109 type UnknownInstruction struct { 110 Line int 111 Instruction string 112 } 113 114 func (e *UnknownInstruction) Error() string { 115 return fmt.Sprintf("unknown instruction: %s", strings.ToUpper(e.Instruction)) 116 } 117 118 // IsUnknownInstruction checks if the error is an UnknownInstruction or a parseError containing an UnknownInstruction 119 func IsUnknownInstruction(err error) bool { 120 _, ok := err.(*UnknownInstruction) 121 if !ok { 122 var pe *parseError 123 if pe, ok = err.(*parseError); ok { 124 _, ok = pe.inner.(*UnknownInstruction) 125 } 126 } 127 return ok 128 } 129 130 type parseError struct { 131 inner error 132 node *parser.Node 133 } 134 135 func (e *parseError) Error() string { 136 return fmt.Sprintf("Dockerfile parse error line %d: %v", e.node.StartLine, e.inner.Error()) 137 } 138 139 // Parse a docker file into a collection of buildable stages 140 func Parse(ast *parser.Node) (stages []Stage, metaArgs []ArgCommand, err error) { 141 for _, n := range ast.Children { 142 cmd, err := ParseInstruction(n) 143 if err != nil { 144 return nil, nil, &parseError{inner: err, node: n} 145 } 146 if len(stages) == 0 { 147 // meta arg case 148 if a, isArg := cmd.(*ArgCommand); isArg { 149 metaArgs = append(metaArgs, *a) 150 continue 151 } 152 } 153 switch c := cmd.(type) { 154 case *Stage: 155 stages = append(stages, *c) 156 case Command: 157 stage, err := CurrentStage(stages) 158 if err != nil { 159 return nil, nil, err 160 } 161 stage.AddCommand(c) 162 default: 163 return nil, nil, errors.Errorf("%T is not a command type", cmd) 164 } 165 166 } 167 return stages, metaArgs, nil 168 } 169 170 func parseKvps(args []string, cmdName string) (KeyValuePairs, error) { 171 if len(args) == 0 { 172 return nil, errAtLeastOneArgument(cmdName) 173 } 174 if len(args)%2 != 0 { 175 // should never get here, but just in case 176 return nil, errTooManyArguments(cmdName) 177 } 178 var res KeyValuePairs 179 for j := 0; j < len(args); j += 2 { 180 if len(args[j]) == 0 { 181 return nil, errBlankCommandNames(cmdName) 182 } 183 name := args[j] 184 value := args[j+1] 185 res = append(res, KeyValuePair{Key: name, Value: value}) 186 } 187 return res, nil 188 } 189 190 func parseEnv(req parseRequest) (*EnvCommand, error) { 191 192 if err := req.flags.Parse(); err != nil { 193 return nil, err 194 } 195 envs, err := parseKvps(req.args, "ENV") 196 if err != nil { 197 return nil, err 198 } 199 return &EnvCommand{ 200 Env: envs, 201 withNameAndCode: newWithNameAndCode(req), 202 }, nil 203 } 204 205 func parseMaintainer(req parseRequest) (*MaintainerCommand, error) { 206 if len(req.args) != 1 { 207 return nil, errExactlyOneArgument("MAINTAINER") 208 } 209 210 if err := req.flags.Parse(); err != nil { 211 return nil, err 212 } 213 return &MaintainerCommand{ 214 Maintainer: req.args[0], 215 withNameAndCode: newWithNameAndCode(req), 216 }, nil 217 } 218 219 func parseLabel(req parseRequest) (*LabelCommand, error) { 220 221 if err := req.flags.Parse(); err != nil { 222 return nil, err 223 } 224 225 labels, err := parseKvps(req.args, "LABEL") 226 if err != nil { 227 return nil, err 228 } 229 230 return &LabelCommand{ 231 Labels: labels, 232 withNameAndCode: newWithNameAndCode(req), 233 }, nil 234 } 235 236 func parseAdd(req parseRequest) (*AddCommand, error) { 237 if len(req.args) < 2 { 238 return nil, errNoDestinationArgument("ADD") 239 } 240 flChown := req.flags.AddString("chown", "") 241 if err := req.flags.Parse(); err != nil { 242 return nil, err 243 } 244 return &AddCommand{ 245 SourcesAndDest: SourcesAndDest(req.args), 246 withNameAndCode: newWithNameAndCode(req), 247 Chown: flChown.Value, 248 }, nil 249 } 250 251 func parseCopy(req parseRequest) (*CopyCommand, error) { 252 if len(req.args) < 2 { 253 return nil, errNoDestinationArgument("COPY") 254 } 255 flChown := req.flags.AddString("chown", "") 256 flFrom := req.flags.AddString("from", "") 257 if err := req.flags.Parse(); err != nil { 258 return nil, err 259 } 260 return &CopyCommand{ 261 SourcesAndDest: SourcesAndDest(req.args), 262 From: flFrom.Value, 263 withNameAndCode: newWithNameAndCode(req), 264 Chown: flChown.Value, 265 }, nil 266 } 267 268 func parseFrom(req parseRequest) (*Stage, error) { 269 stageName, err := parseBuildStageName(req.args) 270 if err != nil { 271 return nil, err 272 } 273 274 if err := req.flags.Parse(); err != nil { 275 return nil, err 276 } 277 code := strings.TrimSpace(req.original) 278 279 return &Stage{ 280 BaseName: req.args[0], 281 Name: stageName, 282 SourceCode: code, 283 Commands: []Command{}, 284 }, nil 285 286 } 287 288 func parseBuildStageName(args []string) (string, error) { 289 stageName := "" 290 switch { 291 case len(args) == 3 && strings.EqualFold(args[1], "as"): 292 stageName = strings.ToLower(args[2]) 293 if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok { 294 return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", stageName) 295 } 296 case len(args) != 1: 297 return "", errors.New("FROM requires either one or three arguments") 298 } 299 300 return stageName, nil 301 } 302 303 func parseOnBuild(req parseRequest) (*OnbuildCommand, error) { 304 if len(req.args) == 0 { 305 return nil, errAtLeastOneArgument("ONBUILD") 306 } 307 if err := req.flags.Parse(); err != nil { 308 return nil, err 309 } 310 311 triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0])) 312 switch strings.ToUpper(triggerInstruction) { 313 case "ONBUILD": 314 return nil, errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") 315 case "MAINTAINER", "FROM": 316 return nil, fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction) 317 } 318 319 original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "") 320 return &OnbuildCommand{ 321 Expression: original, 322 withNameAndCode: newWithNameAndCode(req), 323 }, nil 324 325 } 326 327 func parseWorkdir(req parseRequest) (*WorkdirCommand, error) { 328 if len(req.args) != 1 { 329 return nil, errExactlyOneArgument("WORKDIR") 330 } 331 332 err := req.flags.Parse() 333 if err != nil { 334 return nil, err 335 } 336 return &WorkdirCommand{ 337 Path: req.args[0], 338 withNameAndCode: newWithNameAndCode(req), 339 }, nil 340 341 } 342 343 func parseShellDependentCommand(req parseRequest, emptyAsNil bool) ShellDependantCmdLine { 344 args := handleJSONArgs(req.args, req.attributes) 345 cmd := strslice.StrSlice(args) 346 if emptyAsNil && len(cmd) == 0 { 347 cmd = nil 348 } 349 return ShellDependantCmdLine{ 350 CmdLine: cmd, 351 PrependShell: !req.attributes["json"], 352 } 353 } 354 355 func parseRun(req parseRequest) (*RunCommand, error) { 356 357 if err := req.flags.Parse(); err != nil { 358 return nil, err 359 } 360 return &RunCommand{ 361 ShellDependantCmdLine: parseShellDependentCommand(req, false), 362 withNameAndCode: newWithNameAndCode(req), 363 }, nil 364 365 } 366 367 func parseCmd(req parseRequest) (*CmdCommand, error) { 368 if err := req.flags.Parse(); err != nil { 369 return nil, err 370 } 371 return &CmdCommand{ 372 ShellDependantCmdLine: parseShellDependentCommand(req, false), 373 withNameAndCode: newWithNameAndCode(req), 374 }, nil 375 376 } 377 378 func parseEntrypoint(req parseRequest) (*EntrypointCommand, error) { 379 if err := req.flags.Parse(); err != nil { 380 return nil, err 381 } 382 383 cmd := &EntrypointCommand{ 384 ShellDependantCmdLine: parseShellDependentCommand(req, true), 385 withNameAndCode: newWithNameAndCode(req), 386 } 387 388 return cmd, nil 389 } 390 391 // parseOptInterval(flag) is the duration of flag.Value, or 0 if 392 // empty. An error is reported if the value is given and less than minimum duration. 393 func parseOptInterval(f *Flag) (time.Duration, error) { 394 s := f.Value 395 if s == "" { 396 return 0, nil 397 } 398 d, err := time.ParseDuration(s) 399 if err != nil { 400 return 0, err 401 } 402 if d < container.MinimumDuration { 403 return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration) 404 } 405 return d, nil 406 } 407 func parseHealthcheck(req parseRequest) (*HealthCheckCommand, error) { 408 if len(req.args) == 0 { 409 return nil, errAtLeastOneArgument("HEALTHCHECK") 410 } 411 cmd := &HealthCheckCommand{ 412 withNameAndCode: newWithNameAndCode(req), 413 } 414 415 typ := strings.ToUpper(req.args[0]) 416 args := req.args[1:] 417 if typ == "NONE" { 418 if len(args) != 0 { 419 return nil, errors.New("HEALTHCHECK NONE takes no arguments") 420 } 421 test := strslice.StrSlice{typ} 422 cmd.Health = &container.HealthConfig{ 423 Test: test, 424 } 425 } else { 426 427 healthcheck := container.HealthConfig{} 428 429 flInterval := req.flags.AddString("interval", "") 430 flTimeout := req.flags.AddString("timeout", "") 431 flStartPeriod := req.flags.AddString("start-period", "") 432 flRetries := req.flags.AddString("retries", "") 433 434 if err := req.flags.Parse(); err != nil { 435 return nil, err 436 } 437 438 switch typ { 439 case "CMD": 440 cmdSlice := handleJSONArgs(args, req.attributes) 441 if len(cmdSlice) == 0 { 442 return nil, errors.New("Missing command after HEALTHCHECK CMD") 443 } 444 445 if !req.attributes["json"] { 446 typ = "CMD-SHELL" 447 } 448 449 healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...)) 450 default: 451 return nil, fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ) 452 } 453 454 interval, err := parseOptInterval(flInterval) 455 if err != nil { 456 return nil, err 457 } 458 healthcheck.Interval = interval 459 460 timeout, err := parseOptInterval(flTimeout) 461 if err != nil { 462 return nil, err 463 } 464 healthcheck.Timeout = timeout 465 466 startPeriod, err := parseOptInterval(flStartPeriod) 467 if err != nil { 468 return nil, err 469 } 470 healthcheck.StartPeriod = startPeriod 471 472 if flRetries.Value != "" { 473 retries, err := strconv.ParseInt(flRetries.Value, 10, 32) 474 if err != nil { 475 return nil, err 476 } 477 if retries < 1 { 478 return nil, fmt.Errorf("--retries must be at least 1 (not %d)", retries) 479 } 480 healthcheck.Retries = int(retries) 481 } else { 482 healthcheck.Retries = 0 483 } 484 485 cmd.Health = &healthcheck 486 } 487 return cmd, nil 488 } 489 490 func parseExpose(req parseRequest) (*ExposeCommand, error) { 491 portsTab := req.args 492 493 if len(req.args) == 0 { 494 return nil, errAtLeastOneArgument("EXPOSE") 495 } 496 497 if err := req.flags.Parse(); err != nil { 498 return nil, err 499 } 500 501 sort.Strings(portsTab) 502 return &ExposeCommand{ 503 Ports: portsTab, 504 withNameAndCode: newWithNameAndCode(req), 505 }, nil 506 } 507 508 func parseUser(req parseRequest) (*UserCommand, error) { 509 if len(req.args) != 1 { 510 return nil, errExactlyOneArgument("USER") 511 } 512 513 if err := req.flags.Parse(); err != nil { 514 return nil, err 515 } 516 return &UserCommand{ 517 User: req.args[0], 518 withNameAndCode: newWithNameAndCode(req), 519 }, nil 520 } 521 522 func parseVolume(req parseRequest) (*VolumeCommand, error) { 523 if len(req.args) == 0 { 524 return nil, errAtLeastOneArgument("VOLUME") 525 } 526 527 if err := req.flags.Parse(); err != nil { 528 return nil, err 529 } 530 531 cmd := &VolumeCommand{ 532 withNameAndCode: newWithNameAndCode(req), 533 } 534 535 for _, v := range req.args { 536 v = strings.TrimSpace(v) 537 if v == "" { 538 return nil, errors.New("VOLUME specified can not be an empty string") 539 } 540 cmd.Volumes = append(cmd.Volumes, v) 541 } 542 return cmd, nil 543 544 } 545 546 func parseStopSignal(req parseRequest) (*StopSignalCommand, error) { 547 if len(req.args) != 1 { 548 return nil, errExactlyOneArgument("STOPSIGNAL") 549 } 550 sig := req.args[0] 551 552 cmd := &StopSignalCommand{ 553 Signal: sig, 554 withNameAndCode: newWithNameAndCode(req), 555 } 556 return cmd, nil 557 558 } 559 560 func parseArg(req parseRequest) (*ArgCommand, error) { 561 if len(req.args) != 1 { 562 return nil, errExactlyOneArgument("ARG") 563 } 564 565 var ( 566 name string 567 newValue *string 568 ) 569 570 arg := req.args[0] 571 // 'arg' can just be a name or name-value pair. Note that this is different 572 // from 'env' that handles the split of name and value at the parser level. 573 // The reason for doing it differently for 'arg' is that we support just 574 // defining an arg and not assign it a value (while 'env' always expects a 575 // name-value pair). If possible, it will be good to harmonize the two. 576 if strings.Contains(arg, "=") { 577 parts := strings.SplitN(arg, "=", 2) 578 if len(parts[0]) == 0 { 579 return nil, errBlankCommandNames("ARG") 580 } 581 582 name = parts[0] 583 newValue = &parts[1] 584 } else { 585 name = arg 586 } 587 588 return &ArgCommand{ 589 Key: name, 590 Value: newValue, 591 withNameAndCode: newWithNameAndCode(req), 592 }, nil 593 } 594 595 func parseShell(req parseRequest) (*ShellCommand, error) { 596 if err := req.flags.Parse(); err != nil { 597 return nil, err 598 } 599 shellSlice := handleJSONArgs(req.args, req.attributes) 600 switch { 601 case len(shellSlice) == 0: 602 // SHELL [] 603 return nil, errAtLeastOneArgument("SHELL") 604 case req.attributes["json"]: 605 // SHELL ["powershell", "-command"] 606 607 return &ShellCommand{ 608 Shell: strslice.StrSlice(shellSlice), 609 withNameAndCode: newWithNameAndCode(req), 610 }, nil 611 default: 612 // SHELL powershell -command - not JSON 613 return nil, errNotJSON("SHELL", req.original) 614 } 615 } 616 617 func errAtLeastOneArgument(command string) error { 618 return errors.Errorf("%s requires at least one argument", command) 619 } 620 621 func errExactlyOneArgument(command string) error { 622 return errors.Errorf("%s requires exactly one argument", command) 623 } 624 625 func errNoDestinationArgument(command string) error { 626 return errors.Errorf("%s requires at least two arguments, but only one was provided. Destination could not be determined.", command) 627 } 628 629 func errBlankCommandNames(command string) error { 630 return errors.Errorf("%s names can not be blank", command) 631 } 632 633 func errTooManyArguments(command string) error { 634 return errors.Errorf("Bad input to %s, too many arguments", command) 635 }