moby--moby/builder/evaluator/evaluator.go

233 lines
7.4 KiB
Go
Raw Normal View History

// evaluator 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 resposibility 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 evaluator
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
"github.com/docker/docker/builder/parser"
"github.com/docker/docker/daemon"
"github.com/docker/docker/engine"
"github.com/docker/docker/nat"
"github.com/docker/docker/pkg/tarsum"
"github.com/docker/docker/registry"
"github.com/docker/docker/runconfig"
"github.com/docker/docker/utils"
)
var (
ErrDockerfileEmpty = errors.New("Dockerfile cannot be empty")
)
var evaluateTable map[string]func(*buildFile, []string) error
func init() {
evaluateTable = map[string]func(*buildFile, []string) error{
"env": env,
"maintainer": maintainer,
"add": add,
"copy": dispatchCopy, // copy() is a go builtin
"from": from,
"onbuild": onbuild,
"workdir": workdir,
"docker-version": nullDispatch, // we don't care about docker-version
"run": run,
"cmd": cmd,
"entrypoint": entrypoint,
"expose": expose,
"volume": volume,
"user": user,
"insert": insert,
}
}
type envMap map[string]string
type uniqueMap map[string]struct{}
// internal struct, used to maintain configuration of the Dockerfile's
// processing as it evaluates the parsing result.
type buildFile struct {
dockerfile *parser.Node // the syntax tree of the dockerfile
env envMap // map of environment variables
image string // image name for commit processing
config *runconfig.Config // runconfig for cmd, run, entrypoint etc.
options *BuildOpts // see below
maintainer string // maintainer name. could probably be removed.
cmdSet bool // indicates is CMD was set in current Dockerfile
context *tarsum.TarSum // the context is a tarball that is uploaded by the client
contextPath string // the path of the temporary directory the local context is unpacked to (server side)
// both of these are controlled by the Remove and ForceRemove options in BuildOpts
tmpContainers uniqueMap // a map of containers used for removes
tmpImages uniqueMap // a map of images used for removes
}
type BuildOpts struct {
Daemon *daemon.Daemon
Engine *engine.Engine
// effectively stdio for the run. Because it is not stdio, I said
// "Effectively". Do not use stdio anywhere in this package for any reason.
OutStream io.Writer
ErrStream io.Writer
Verbose bool
UtilizeCache bool
// controls how images and containers are handled between steps.
Remove bool
ForceRemove bool
AuthConfig *registry.AuthConfig
AuthConfigFile *registry.ConfigFile
// Deprecated, original writer used for ImagePull. To be removed.
OutOld io.Writer
StreamFormatter *utils.StreamFormatter
}
// Create a new builder.
func NewBuilder(opts *BuildOpts) (*buildFile, error) {
return &buildFile{
dockerfile: nil,
env: envMap{},
config: initRunConfig(),
options: opts,
tmpContainers: make(uniqueMap),
tmpImages: make(uniqueMap),
}, nil
}
// Run the builder with the context. This is the lynchpin of this package. This
// will (barring errors):
//
// * call readContext() which will set up the temporary directory and unpack
// the context into it.
// * read the dockerfile
// * parse the dockerfile
// * walk the parse tree and execute it by dispatching to handlers. If Remove
// or ForceRemove is set, additional cleanup around containers happens after
// processing.
// * Print a happy message and return the image ID.
//
func (b *buildFile) Run(context io.Reader) (string, error) {
if err := b.readContext(context); err != nil {
return "", err
}
filename := path.Join(b.contextPath, "Dockerfile")
if _, err := os.Stat(filename); os.IsNotExist(err) {
return "", fmt.Errorf("Cannot build a directory without a Dockerfile")
}
fileBytes, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
if len(fileBytes) == 0 {
return "", ErrDockerfileEmpty
}
ast, err := parser.Parse(bytes.NewReader(fileBytes))
if err != nil {
return "", err
}
b.dockerfile = ast
for i, n := range b.dockerfile.Children {
if err := b.dispatch(i, n); err != nil {
if b.options.ForceRemove {
b.clearTmp(b.tmpContainers)
}
return "", err
} else if b.options.Remove {
b.clearTmp(b.tmpContainers)
}
}
if b.image == "" {
return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?\n")
}
fmt.Fprintf(b.options.OutStream, "Successfully built %s\n", utils.TruncateID(b.image))
return b.image, nil
}
func initRunConfig() *runconfig.Config {
return &runconfig.Config{
PortSpecs: []string{},
// FIXME(erikh) this should be a type that lives in runconfig
ExposedPorts: map[nat.Port]struct{}{},
Env: []string{},
Cmd: []string{},
// FIXME(erikh) this should also be a type in runconfig
Volumes: map[string]struct{}{},
Entrypoint: []string{"/bin/sh", "-c"},
OnBuild: []string{},
}
}
// 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 buildFile 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 statmeent,
// 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 *buildFile) dispatch(stepN int, ast *parser.Node) error {
cmd := ast.Value
strs := []string{}
if cmd == "onbuild" {
fmt.Fprintf(b.options.OutStream, "%#v\n", ast.Next.Children[0].Value)
ast = ast.Next.Children[0]
strs = append(strs, ast.Value)
}
for ast.Next != nil {
ast = ast.Next
strs = append(strs, replaceEnv(b, ast.Value))
}
fmt.Fprintf(b.options.OutStream, "Step %d : %s %s\n", stepN, strings.ToUpper(cmd), strings.Join(strs, " "))
// XXX yes, we skip any cmds that are not valid; the parser should have
// picked these out already.
if f, ok := evaluateTable[cmd]; ok {
return f(b, strs)
}
return nil
}