1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00
moby--moby/builder/dockerfile/evaluator.go
Brian Goff ebcb7d6b40 Remove string checking in API error handling
Use strongly typed errors to set HTTP status codes.
Error interfaces are defined in the api/errors package and errors
returned from controllers are checked against these interfaces.

Errors can be wraeped in a pkg/errors.Causer, as long as somewhere in the
line of causes one of the interfaces is implemented. The special error
interfaces take precedence over Causer, meaning if both Causer and one
of the new error interfaces are implemented, the Causer is not
traversed.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2017-08-15 16:01:11 -04:00

327 lines
10 KiB
Go

// Package dockerfile is the evaluation step in the Dockerfile parse/evaluate pipeline.
//
// It incorporates a dispatch table based on the parser.Node values (see the
// parser package for more information) that are yielded from the parser itself.
// Calling newBuilder with the BuildOpts struct can be used to customize the
// experience for execution purposes only. Parsing is controlled in the parser
// package, and this division of responsibility should be respected.
//
// Please see the jump table targets for the actual invocations, most of which
// will call out to the functions in internals.go to deal with their tasks.
//
// ONBUILD is a special case, which is covered in the onbuild() func in
// dispatchers.go.
//
// The evaluator uses the concept of "steps", which are usually each processable
// line in the Dockerfile. Each step is numbered and certain actions are taken
// before and after each step, such as creating an image ID and removing temporary
// containers and images. Note that ONBUILD creates a kinda-sorta "sub run" which
// includes its own set of steps (usually only one of them).
package dockerfile
import (
"bytes"
"fmt"
"runtime"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/builder"
"github.com/docker/docker/builder/dockerfile/command"
"github.com/docker/docker/builder/dockerfile/parser"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/runconfig/opts"
"github.com/pkg/errors"
)
// Environment variable interpolation will happen on these statements only.
var replaceEnvAllowed = map[string]bool{
command.Env: true,
command.Label: true,
command.Add: true,
command.Copy: true,
command.Workdir: true,
command.Expose: true,
command.Volume: true,
command.User: true,
command.StopSignal: true,
command.Arg: true,
}
// Certain commands are allowed to have their args split into more
// words after env var replacements. Meaning:
// ENV foo="123 456"
// EXPOSE $foo
// should result in the same thing as:
// EXPOSE 123 456
// and not treat "123 456" as a single word.
// Note that: EXPOSE "$foo" and EXPOSE $foo are not the same thing.
// Quotes will cause it to still be treated as single word.
var allowWordExpansion = map[string]bool{
command.Expose: true,
}
type dispatchRequest struct {
builder *Builder // TODO: replace this with a smaller interface
args []string
attributes map[string]bool
flags *BFlags
original string
shlex *ShellLex
state *dispatchState
source builder.Source
}
func newDispatchRequestFromOptions(options dispatchOptions, builder *Builder, args []string) dispatchRequest {
return dispatchRequest{
builder: builder,
args: args,
attributes: options.node.Attributes,
original: options.node.Original,
flags: NewBFlagsWithArgs(options.node.Flags),
shlex: options.shlex,
state: options.state,
source: options.source,
}
}
type dispatcher func(dispatchRequest) error
var evaluateTable map[string]dispatcher
func init() {
evaluateTable = map[string]dispatcher{
command.Add: add,
command.Arg: arg,
command.Cmd: cmd,
command.Copy: dispatchCopy, // copy() is a go builtin
command.Entrypoint: entrypoint,
command.Env: env,
command.Expose: expose,
command.From: from,
command.Healthcheck: healthcheck,
command.Label: label,
command.Maintainer: maintainer,
command.Onbuild: onbuild,
command.Run: run,
command.Shell: shell,
command.StopSignal: stopSignal,
command.User: user,
command.Volume: volume,
command.Workdir: workdir,
}
}
func formatStep(stepN int, stepTotal int) string {
return fmt.Sprintf("%d/%d", stepN+1, stepTotal)
}
// This method is the entrypoint to all statement handling routines.
//
// Almost all nodes will have this structure:
// Child[Node, Node, Node] where Child is from parser.Node.Children and each
// node comes from parser.Node.Next. This forms a "line" with a statement and
// arguments and we process them in this normalized form by hitting
// evaluateTable with the leaf nodes of the command and the Builder object.
//
// ONBUILD is a special case; in this case the parser will emit:
// Child[Node, Child[Node, Node...]] where the first node is the literal
// "onbuild" and the child entrypoint is the command of the ONBUILD statement,
// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
// deal with that, at least until it becomes more of a general concern with new
// features.
func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
node := options.node
cmd := node.Value
upperCasedCmd := strings.ToUpper(cmd)
// To ensure the user is given a decent error message if the platform
// on which the daemon is running does not support a builder command.
if err := platformSupports(strings.ToLower(cmd)); err != nil {
buildsFailed.WithValues(metricsCommandNotSupportedError).Inc()
return nil, validationError{err}
}
msg := bytes.NewBufferString(fmt.Sprintf("Step %s : %s%s",
options.stepMsg, upperCasedCmd, formatFlags(node.Flags)))
args := []string{}
ast := node
if cmd == command.Onbuild {
var err error
ast, args, err = handleOnBuildNode(node, msg)
if err != nil {
return nil, validationError{err}
}
}
runConfigEnv := options.state.runConfig.Env
envs := append(runConfigEnv, b.buildArgs.FilterAllowed(runConfigEnv)...)
processFunc := createProcessWordFunc(options.shlex, cmd, envs)
words, err := getDispatchArgsFromNode(ast, processFunc, msg)
if err != nil {
buildsFailed.WithValues(metricsErrorProcessingCommandsError).Inc()
return nil, validationError{err}
}
args = append(args, words...)
fmt.Fprintln(b.Stdout, msg.String())
f, ok := evaluateTable[cmd]
if !ok {
buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
return nil, validationError{errors.Errorf("unknown instruction: %s", upperCasedCmd)}
}
options.state.updateRunConfig()
err = f(newDispatchRequestFromOptions(options, b, args))
return options.state, err
}
type dispatchOptions struct {
state *dispatchState
stepMsg string
node *parser.Node
shlex *ShellLex
source builder.Source
}
// dispatchState is a data object which is modified by dispatchers
type dispatchState struct {
runConfig *container.Config
maintainer string
cmdSet bool
imageID string
baseImage builder.Image
stageName string
}
func newDispatchState() *dispatchState {
return &dispatchState{runConfig: &container.Config{}}
}
func (s *dispatchState) updateRunConfig() {
s.runConfig.Image = s.imageID
}
// hasFromImage returns true if the builder has processed a `FROM <image>` line
func (s *dispatchState) hasFromImage() bool {
return s.imageID != "" || (s.baseImage != nil && s.baseImage.ImageID() == "")
}
func (s *dispatchState) isCurrentStage(target string) bool {
if target == "" {
return false
}
return strings.EqualFold(s.stageName, target)
}
func (s *dispatchState) beginStage(stageName string, image builder.Image) {
s.stageName = stageName
s.imageID = image.ImageID()
if image.RunConfig() != nil {
s.runConfig = image.RunConfig()
} else {
s.runConfig = &container.Config{}
}
s.baseImage = image
s.setDefaultPath()
}
// Add the default PATH to runConfig.ENV if one exists for the platform and there
// is no PATH set. Note that Windows containers on Windows won't have one as it's set by HCS
func (s *dispatchState) setDefaultPath() {
// TODO @jhowardmsft LCOW Support - This will need revisiting later
platform := runtime.GOOS
if system.LCOWSupported() {
platform = "linux"
}
if system.DefaultPathEnv(platform) == "" {
return
}
envMap := opts.ConvertKVStringsToMap(s.runConfig.Env)
if _, ok := envMap["PATH"]; !ok {
s.runConfig.Env = append(s.runConfig.Env, "PATH="+system.DefaultPathEnv(platform))
}
}
func handleOnBuildNode(ast *parser.Node, msg *bytes.Buffer) (*parser.Node, []string, error) {
if ast.Next == nil {
return nil, nil, validationError{errors.New("ONBUILD requires at least one argument")}
}
ast = ast.Next.Children[0]
msg.WriteString(" " + ast.Value + formatFlags(ast.Flags))
return ast, []string{ast.Value}, nil
}
func formatFlags(flags []string) string {
if len(flags) > 0 {
return " " + strings.Join(flags, " ")
}
return ""
}
func getDispatchArgsFromNode(ast *parser.Node, processFunc processWordFunc, msg *bytes.Buffer) ([]string, error) {
args := []string{}
for i := 0; ast.Next != nil; i++ {
ast = ast.Next
words, err := processFunc(ast.Value)
if err != nil {
return nil, err
}
args = append(args, words...)
msg.WriteString(" " + ast.Value)
}
return args, nil
}
type processWordFunc func(string) ([]string, error)
func createProcessWordFunc(shlex *ShellLex, cmd string, envs []string) processWordFunc {
switch {
case !replaceEnvAllowed[cmd]:
return func(word string) ([]string, error) {
return []string{word}, nil
}
case allowWordExpansion[cmd]:
return func(word string) ([]string, error) {
return shlex.ProcessWords(word, envs)
}
default:
return func(word string) ([]string, error) {
word, err := shlex.ProcessWord(word, envs)
return []string{word}, err
}
}
}
// checkDispatch does a simple check for syntax errors of the Dockerfile.
// Because some of the instructions can only be validated through runtime,
// arg, env, etc., this syntax check will not be complete and could not replace
// the runtime check. Instead, this function is only a helper that allows
// user to find out the obvious error in Dockerfile earlier on.
func checkDispatch(ast *parser.Node) error {
cmd := ast.Value
upperCasedCmd := strings.ToUpper(cmd)
// To ensure the user is given a decent error message if the platform
// on which the daemon is running does not support a builder command.
if err := platformSupports(strings.ToLower(cmd)); err != nil {
return err
}
// The instruction itself is ONBUILD, we will make sure it follows with at
// least one argument
if upperCasedCmd == "ONBUILD" {
if ast.Next == nil {
buildsFailed.WithValues(metricsMissingOnbuildArgumentsError).Inc()
return errors.New("ONBUILD requires at least one argument")
}
}
if _, ok := evaluateTable[cmd]; ok {
return nil
}
buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
return errors.Errorf("unknown instruction: %s", upperCasedCmd)
}