b16ff9f859
By setting an entrypoint in the Dockerfile this allows one to run an image and only pass arguments.
420 lines
11 KiB
Go
420 lines
11 KiB
Go
package docker
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/dotcloud/docker/utils"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
type BuildFile interface {
|
|
Build(io.Reader) (string, error)
|
|
CmdFrom(string) error
|
|
CmdRun(string) error
|
|
}
|
|
|
|
type buildFile struct {
|
|
runtime *Runtime
|
|
builder *Builder
|
|
srv *Server
|
|
|
|
image string
|
|
maintainer string
|
|
config *Config
|
|
context string
|
|
|
|
tmpContainers map[string]struct{}
|
|
tmpImages map[string]struct{}
|
|
|
|
out io.Writer
|
|
}
|
|
|
|
func (b *buildFile) clearTmp(containers, images map[string]struct{}) {
|
|
for c := range containers {
|
|
tmp := b.runtime.Get(c)
|
|
b.runtime.Destroy(tmp)
|
|
utils.Debugf("Removing container %s", c)
|
|
}
|
|
for i := range images {
|
|
b.runtime.graph.Delete(i)
|
|
utils.Debugf("Removing image %s", i)
|
|
}
|
|
}
|
|
|
|
func (b *buildFile) CmdFrom(name string) error {
|
|
image, err := b.runtime.repositories.LookupImage(name)
|
|
if err != nil {
|
|
if b.runtime.graph.IsNotExist(err) {
|
|
|
|
var tag, remote string
|
|
if strings.Contains(name, ":") {
|
|
remoteParts := strings.Split(name, ":")
|
|
tag = remoteParts[1]
|
|
remote = remoteParts[0]
|
|
} else {
|
|
remote = name
|
|
}
|
|
|
|
if err := b.srv.ImagePull(remote, tag, "", b.out, utils.NewStreamFormatter(false), nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
image, err = b.runtime.repositories.LookupImage(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
b.image = image.ID
|
|
b.config = &Config{}
|
|
return nil
|
|
}
|
|
|
|
func (b *buildFile) CmdMaintainer(name string) error {
|
|
b.maintainer = name
|
|
return b.commit("", b.config.Cmd, fmt.Sprintf("MAINTAINER %s", name))
|
|
}
|
|
|
|
func (b *buildFile) CmdRun(args string) error {
|
|
if b.image == "" {
|
|
return fmt.Errorf("Please provide a source image with `from` prior to run")
|
|
}
|
|
config, _, _, err := ParseRun([]string{b.image, "/bin/sh", "-c", args}, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := b.config.Cmd
|
|
b.config.Cmd = nil
|
|
MergeConfig(b.config, config)
|
|
|
|
utils.Debugf("Command to be executed: %v", b.config.Cmd)
|
|
|
|
if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
|
|
return err
|
|
} else if cache != nil {
|
|
fmt.Fprintf(b.out, " ---> Using cache\n")
|
|
utils.Debugf("[BUILDER] Use cached version")
|
|
b.image = cache.ID
|
|
return nil
|
|
} else {
|
|
utils.Debugf("[BUILDER] Cache miss")
|
|
}
|
|
|
|
cid, err := b.run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := b.commit(cid, cmd, "run"); err != nil {
|
|
return err
|
|
}
|
|
b.config.Cmd = cmd
|
|
return nil
|
|
}
|
|
|
|
func (b *buildFile) CmdEnv(args string) error {
|
|
tmp := strings.SplitN(args, " ", 2)
|
|
if len(tmp) != 2 {
|
|
return fmt.Errorf("Invalid ENV format")
|
|
}
|
|
key := strings.Trim(tmp[0], " \t")
|
|
value := strings.Trim(tmp[1], " \t")
|
|
|
|
for i, elem := range b.config.Env {
|
|
if strings.HasPrefix(elem, key+"=") {
|
|
b.config.Env[i] = key + "=" + value
|
|
return nil
|
|
}
|
|
}
|
|
b.config.Env = append(b.config.Env, key+"="+value)
|
|
return b.commit("", b.config.Cmd, fmt.Sprintf("ENV %s=%s", key, value))
|
|
}
|
|
|
|
func (b *buildFile) CmdCmd(args string) error {
|
|
var cmd []string
|
|
if err := json.Unmarshal([]byte(args), &cmd); err != nil {
|
|
utils.Debugf("Error unmarshalling: %s, setting cmd to /bin/sh -c", err)
|
|
cmd = []string{"/bin/sh", "-c", args}
|
|
}
|
|
if err := b.commit("", cmd, fmt.Sprintf("CMD %v", cmd)); err != nil {
|
|
return err
|
|
}
|
|
b.config.Cmd = cmd
|
|
return nil
|
|
}
|
|
|
|
func (b *buildFile) CmdExpose(args string) error {
|
|
ports := strings.Split(args, " ")
|
|
b.config.PortSpecs = append(ports, b.config.PortSpecs...)
|
|
return b.commit("", b.config.Cmd, fmt.Sprintf("EXPOSE %v", ports))
|
|
}
|
|
|
|
func (b *buildFile) CmdInsert(args string) error {
|
|
return fmt.Errorf("INSERT has been deprecated. Please use ADD instead")
|
|
}
|
|
|
|
func (b *buildFile) CmdCopy(args string) error {
|
|
return fmt.Errorf("COPY has been deprecated. Please use ADD instead")
|
|
}
|
|
|
|
func (b *buildFile) CmdEntrypoint(args string) error {
|
|
if args == "" {
|
|
return fmt.Errorf("Entrypoint cannot be empty")
|
|
}
|
|
|
|
var entrypoint []string
|
|
if err := json.Unmarshal([]byte(args), &entrypoint); err != nil {
|
|
b.config.Entrypoint = []string{"/bin/sh", "-c", args}
|
|
} else {
|
|
b.config.Entrypoint = entrypoint
|
|
}
|
|
if err := b.commit("", b.config.Cmd, fmt.Sprintf("ENTRYPOINT %s", args)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *buildFile) addRemote(container *Container, orig, dest string) error {
|
|
file, err := utils.Download(orig, ioutil.Discard)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Body.Close()
|
|
|
|
return container.Inject(file.Body, dest)
|
|
}
|
|
|
|
func (b *buildFile) addContext(container *Container, orig, dest string) error {
|
|
origPath := path.Join(b.context, orig)
|
|
destPath := path.Join(container.RootfsPath(), dest)
|
|
// Preserve the trailing '/'
|
|
if dest[len(dest)-1] == '/' {
|
|
destPath = destPath + "/"
|
|
}
|
|
fi, err := os.Stat(origPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fi.IsDir() {
|
|
if err := CopyWithTar(origPath, destPath); err != nil {
|
|
return err
|
|
}
|
|
// First try to unpack the source as an archive
|
|
} else if err := UntarPath(origPath, destPath); err != nil {
|
|
utils.Debugf("Couldn't untar %s to %s: %s", origPath, destPath, err)
|
|
// If that fails, just copy it as a regular file
|
|
if err := os.MkdirAll(path.Dir(destPath), 0700); err != nil {
|
|
return err
|
|
}
|
|
if err := CopyWithTar(origPath, destPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *buildFile) CmdAdd(args string) error {
|
|
if b.context == "" {
|
|
return fmt.Errorf("No context given. Impossible to use ADD")
|
|
}
|
|
tmp := strings.SplitN(args, " ", 2)
|
|
if len(tmp) != 2 {
|
|
return fmt.Errorf("Invalid ADD format")
|
|
}
|
|
orig := strings.Trim(tmp[0], " \t")
|
|
dest := strings.Trim(tmp[1], " \t")
|
|
|
|
cmd := b.config.Cmd
|
|
b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)}
|
|
|
|
b.config.Image = b.image
|
|
// Create the container and start it
|
|
container, err := b.builder.Create(b.config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.tmpContainers[container.ID] = struct{}{}
|
|
|
|
if err := container.EnsureMounted(); err != nil {
|
|
return err
|
|
}
|
|
defer container.Unmount()
|
|
|
|
if utils.IsURL(orig) {
|
|
if err := b.addRemote(container, orig, dest); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := b.addContext(container, orig, dest); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := b.commit(container.ID, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil {
|
|
return err
|
|
}
|
|
b.config.Cmd = cmd
|
|
return nil
|
|
}
|
|
|
|
func (b *buildFile) run() (string, error) {
|
|
if b.image == "" {
|
|
return "", fmt.Errorf("Please provide a source image with `from` prior to run")
|
|
}
|
|
b.config.Image = b.image
|
|
|
|
// Create the container and start it
|
|
c, err := b.builder.Create(b.config)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
b.tmpContainers[c.ID] = struct{}{}
|
|
fmt.Fprintf(b.out, " ---> Running in %s\n", utils.TruncateID(c.ID))
|
|
|
|
//start the container
|
|
hostConfig := &HostConfig{}
|
|
if err := c.Start(hostConfig); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Wait for it to finish
|
|
if ret := c.Wait(); ret != 0 {
|
|
return "", fmt.Errorf("The command %v returned a non-zero code: %d", b.config.Cmd, ret)
|
|
}
|
|
|
|
return c.ID, nil
|
|
}
|
|
|
|
// Commit the container <id> with the autorun command <autoCmd>
|
|
func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
|
|
if b.image == "" {
|
|
return fmt.Errorf("Please provide a source image with `from` prior to commit")
|
|
}
|
|
b.config.Image = b.image
|
|
if id == "" {
|
|
cmd := b.config.Cmd
|
|
b.config.Cmd = []string{"/bin/sh", "-c", "#(nop) " + comment}
|
|
defer func(cmd []string) { b.config.Cmd = cmd }(cmd)
|
|
|
|
if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
|
|
return err
|
|
} else if cache != nil {
|
|
fmt.Fprintf(b.out, " ---> Using cache\n")
|
|
utils.Debugf("[BUILDER] Use cached version")
|
|
b.image = cache.ID
|
|
return nil
|
|
} else {
|
|
utils.Debugf("[BUILDER] Cache miss")
|
|
}
|
|
container, err := b.builder.Create(b.config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.tmpContainers[container.ID] = struct{}{}
|
|
fmt.Fprintf(b.out, " ---> Running in %s\n", utils.TruncateID(container.ID))
|
|
id = container.ID
|
|
if err := container.EnsureMounted(); err != nil {
|
|
return err
|
|
}
|
|
defer container.Unmount()
|
|
}
|
|
|
|
container := b.runtime.Get(id)
|
|
if container == nil {
|
|
return fmt.Errorf("An error occured while creating the container")
|
|
}
|
|
|
|
// Note: Actually copy the struct
|
|
autoConfig := *b.config
|
|
autoConfig.Cmd = autoCmd
|
|
// Commit the container
|
|
image, err := b.builder.Commit(container, "", "", "", b.maintainer, &autoConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.tmpImages[image.ID] = struct{}{}
|
|
b.image = image.ID
|
|
return nil
|
|
}
|
|
|
|
func (b *buildFile) Build(context io.Reader) (string, error) {
|
|
// FIXME: @creack any reason for using /tmp instead of ""?
|
|
// FIXME: @creack "name" is a terrible variable name
|
|
name, err := ioutil.TempDir("/tmp", "docker-build")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := Untar(context, name); err != nil {
|
|
return "", err
|
|
}
|
|
defer os.RemoveAll(name)
|
|
b.context = name
|
|
dockerfile, err := os.Open(path.Join(name, "Dockerfile"))
|
|
if err != nil {
|
|
return "", fmt.Errorf("Can't build a directory with no Dockerfile")
|
|
}
|
|
// FIXME: "file" is also a terrible variable name ;)
|
|
file := bufio.NewReader(dockerfile)
|
|
stepN := 0
|
|
for {
|
|
line, err := file.ReadString('\n')
|
|
if err != nil {
|
|
if err == io.EOF && line == "" {
|
|
break
|
|
} else if err != io.EOF {
|
|
return "", err
|
|
}
|
|
}
|
|
line = strings.Trim(strings.Replace(line, "\t", " ", -1), " \t\r\n")
|
|
// Skip comments and empty line
|
|
if len(line) == 0 || line[0] == '#' {
|
|
continue
|
|
}
|
|
tmp := strings.SplitN(line, " ", 2)
|
|
if len(tmp) != 2 {
|
|
return "", fmt.Errorf("Invalid Dockerfile format")
|
|
}
|
|
instruction := strings.ToLower(strings.Trim(tmp[0], " "))
|
|
arguments := strings.Trim(tmp[1], " ")
|
|
stepN += 1
|
|
// FIXME: only count known instructions as build steps
|
|
fmt.Fprintf(b.out, "Step %d : %s %s\n", stepN, strings.ToUpper(instruction), arguments)
|
|
|
|
method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:]))
|
|
if !exists {
|
|
fmt.Fprintf(b.out, "# Skipping unknown instruction %s\n", strings.ToUpper(instruction))
|
|
continue
|
|
}
|
|
ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface()
|
|
if ret != nil {
|
|
return "", ret.(error)
|
|
}
|
|
|
|
fmt.Fprintf(b.out, " ---> %v\n", utils.TruncateID(b.image))
|
|
}
|
|
if b.image != "" {
|
|
fmt.Fprintf(b.out, "Successfully built %s\n", utils.TruncateID(b.image))
|
|
return b.image, nil
|
|
}
|
|
return "", fmt.Errorf("An error occured during the build\n")
|
|
}
|
|
|
|
func NewBuildFile(srv *Server, out io.Writer) BuildFile {
|
|
return &buildFile{
|
|
builder: NewBuilder(srv.runtime),
|
|
runtime: srv.runtime,
|
|
srv: srv,
|
|
config: &Config{},
|
|
out: out,
|
|
tmpContainers: make(map[string]struct{}),
|
|
tmpImages: make(map[string]struct{}),
|
|
}
|
|
}
|