mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
ebcb7d6b40
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>
327 lines
10 KiB
Go
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)
|
|
}
|