Abstract builder and implement server-side dockerfile builder
This patch creates interfaces in builder/ for building Docker images. It is a first step in a series of patches to remove the daemon dependency on builder and later allow a client-side Dockerfile builder as well as potential builder plugins. It is needed because we cannot remove the /build API endpoint, so we need to keep the server-side Dockerfile builder, but we also want to reuse the same Dockerfile parser and evaluator for both server-side and client-side. builder/dockerfile/ and api/server/builder.go contain implementations of those interfaces as a refactoring of the current code. Signed-off-by: Tibor Vass <tibor@docker.com>
This commit is contained in:
parent
f41230b93a
commit
e0ef11a4c2
26 changed files with 1723 additions and 1343 deletions
|
@ -12,7 +12,6 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
@ -131,13 +130,19 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
|
|||
return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err)
|
||||
}
|
||||
|
||||
var includes = []string{"."}
|
||||
|
||||
excludes, err := utils.ReadDockerIgnore(path.Join(contextDir, ".dockerignore"))
|
||||
if err != nil {
|
||||
f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
var excludes []string
|
||||
if err == nil {
|
||||
excludes, err = utils.ReadDockerIgnore(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := utils.ValidateContextDirectory(contextDir, excludes); err != nil {
|
||||
return fmt.Errorf("Error checking context: '%s'.", err)
|
||||
}
|
||||
|
@ -149,6 +154,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
|
|||
// removed. The deamon will remove them for us, if needed, after it
|
||||
// parses the Dockerfile. Ignore errors here, as they will have been
|
||||
// caught by ValidateContextDirectory above.
|
||||
var includes = []string{"."}
|
||||
keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
|
||||
keepThem2, _ := fileutils.Matches(relDockerfile, excludes)
|
||||
if keepThem1 || keepThem2 {
|
||||
|
|
|
@ -12,13 +12,18 @@ import (
|
|||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/builder"
|
||||
"github.com/docker/docker/builder/dockerfile"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/daemon/daemonbuilder"
|
||||
"github.com/docker/docker/graph"
|
||||
"github.com/docker/docker/graph/tags"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/ulimit"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/utils"
|
||||
"golang.org/x/net/context"
|
||||
|
@ -56,13 +61,18 @@ func (s *router) postCommit(ctx context.Context, w http.ResponseWriter, r *http.
|
|||
Config: c,
|
||||
}
|
||||
|
||||
imgID, err := dockerfile.Commit(cname, s.daemon, commitCfg)
|
||||
container, err := s.daemon.Get(cname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imgID, err := dockerfile.Commit(container, s.daemon, commitCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return httputils.WriteJSON(w, http.StatusCreated, &types.ContainerCommitResponse{
|
||||
ID: imgID,
|
||||
ID: string(imgID),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -125,7 +135,7 @@ func (s *router) postImagesCreate(ctx context.Context, w http.ResponseWriter, r
|
|||
// generated from the download to be available to the output
|
||||
// stream processing below
|
||||
var newConfig *runconfig.Config
|
||||
newConfig, err = dockerfile.BuildFromConfig(s.daemon, &runconfig.Config{}, r.Form["changes"])
|
||||
newConfig, err = dockerfile.BuildFromConfig(&runconfig.Config{}, r.Form["changes"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -269,7 +279,7 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||
var (
|
||||
authConfigs = map[string]cliconfig.AuthConfig{}
|
||||
authConfigsEncoded = r.Header.Get("X-Registry-Config")
|
||||
buildConfig = dockerfile.NewBuildConfig()
|
||||
buildConfig = &dockerfile.Config{}
|
||||
)
|
||||
|
||||
if authConfigsEncoded != "" {
|
||||
|
@ -284,6 +294,21 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
version := httputils.VersionFromContext(ctx)
|
||||
output := ioutils.NewWriteFlusher(w)
|
||||
sf := streamformatter.NewJSONStreamFormatter()
|
||||
errf := func(err error) error {
|
||||
// Do not write the error in the http output if it's still empty.
|
||||
// This prevents from writing a 200(OK) when there is an interal error.
|
||||
if !output.Flushed() {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(sf.FormatError(errors.New(utils.GetErrorMessage(err))))
|
||||
if err != nil {
|
||||
logrus.Warnf("could not write error response: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if httputils.BoolValue(r, "forcerm") && version.GreaterThanOrEqualTo("1.12") {
|
||||
buildConfig.Remove = true
|
||||
} else if r.FormValue("rm") == "" && version.GreaterThanOrEqualTo("1.12") {
|
||||
|
@ -295,17 +320,22 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||
buildConfig.Pull = true
|
||||
}
|
||||
|
||||
output := ioutils.NewWriteFlusher(w)
|
||||
buildConfig.Stdout = output
|
||||
buildConfig.Context = r.Body
|
||||
repoName, tag := parsers.ParseRepositoryTag(r.FormValue("t"))
|
||||
if repoName != "" {
|
||||
if err := registry.ValidateRepositoryName(repoName); err != nil {
|
||||
return errf(err)
|
||||
}
|
||||
if len(tag) > 0 {
|
||||
if err := tags.ValidateTagName(tag); err != nil {
|
||||
return errf(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildConfig.RemoteURL = r.FormValue("remote")
|
||||
buildConfig.DockerfileName = r.FormValue("dockerfile")
|
||||
buildConfig.RepoName = r.FormValue("t")
|
||||
buildConfig.SuppressOutput = httputils.BoolValue(r, "q")
|
||||
buildConfig.NoCache = httputils.BoolValue(r, "nocache")
|
||||
buildConfig.Verbose = !httputils.BoolValue(r, "q")
|
||||
buildConfig.UseCache = !httputils.BoolValue(r, "nocache")
|
||||
buildConfig.ForceRemove = httputils.BoolValue(r, "forcerm")
|
||||
buildConfig.AuthConfigs = authConfigs
|
||||
buildConfig.MemorySwap = httputils.Int64ValueOrZero(r, "memswap")
|
||||
buildConfig.Memory = httputils.Int64ValueOrZero(r, "memory")
|
||||
buildConfig.CPUShares = httputils.Int64ValueOrZero(r, "cpushares")
|
||||
|
@ -319,7 +349,7 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||
ulimitsJSON := r.FormValue("ulimits")
|
||||
if ulimitsJSON != "" {
|
||||
if err := json.NewDecoder(strings.NewReader(ulimitsJSON)).Decode(&buildUlimits); err != nil {
|
||||
return err
|
||||
return errf(err)
|
||||
}
|
||||
buildConfig.Ulimits = buildUlimits
|
||||
}
|
||||
|
@ -328,12 +358,50 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||
buildArgsJSON := r.FormValue("buildargs")
|
||||
if buildArgsJSON != "" {
|
||||
if err := json.NewDecoder(strings.NewReader(buildArgsJSON)).Decode(&buildArgs); err != nil {
|
||||
return err
|
||||
return errf(err)
|
||||
}
|
||||
buildConfig.BuildArgs = buildArgs
|
||||
}
|
||||
buildConfig.BuildArgs = buildArgs
|
||||
|
||||
// Job cancellation. Note: not all job types support this.
|
||||
remoteURL := r.FormValue("remote")
|
||||
|
||||
// Currently, only used if context is from a remote url.
|
||||
// The field `In` is set by DetectContextFromRemoteURL.
|
||||
// Look at code in DetectContextFromRemoteURL for more information.
|
||||
pReader := &progressreader.Config{
|
||||
// TODO: make progressreader streamformatter-agnostic
|
||||
Out: output,
|
||||
Formatter: sf,
|
||||
Size: r.ContentLength,
|
||||
NewLines: true,
|
||||
ID: "Downloading context",
|
||||
Action: remoteURL,
|
||||
}
|
||||
|
||||
var (
|
||||
context builder.ModifiableContext
|
||||
dockerfileName string
|
||||
err error
|
||||
)
|
||||
context, dockerfileName, err = daemonbuilder.DetectContextFromRemoteURL(r.Body, remoteURL, pReader)
|
||||
if err != nil {
|
||||
return errf(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := context.Close(); err != nil {
|
||||
logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
docker := daemonbuilder.Docker{s.daemon, output, authConfigs}
|
||||
|
||||
b, err := dockerfile.NewBuilder(buildConfig, docker, builder.DockerIgnoreContext{context}, nil)
|
||||
if err != nil {
|
||||
return errf(err)
|
||||
}
|
||||
b.Stdout = &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf}
|
||||
b.Stderr = &streamformatter.StderrFormatter{Writer: output, StreamFormatter: sf}
|
||||
|
||||
if closeNotifier, ok := w.(http.CloseNotifier); ok {
|
||||
finished := make(chan struct{})
|
||||
defer close(finished)
|
||||
|
@ -342,20 +410,26 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||
case <-finished:
|
||||
case <-closeNotifier.CloseNotify():
|
||||
logrus.Infof("Client disconnected, cancelling job: build")
|
||||
buildConfig.Cancel()
|
||||
b.Cancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if err := dockerfile.Build(s.daemon, buildConfig); err != nil {
|
||||
// Do not write the error in the http output if it's still empty.
|
||||
// This prevents from writing a 200(OK) when there is an interal error.
|
||||
if !output.Flushed() {
|
||||
return err
|
||||
}
|
||||
sf := streamformatter.NewJSONStreamFormatter()
|
||||
w.Write(sf.FormatError(errors.New(utils.GetErrorMessage(err))))
|
||||
if len(dockerfileName) > 0 {
|
||||
b.DockerfileName = dockerfileName
|
||||
}
|
||||
|
||||
imgID, err := b.Build()
|
||||
if err != nil {
|
||||
return errf(err)
|
||||
}
|
||||
|
||||
if repoName != "" {
|
||||
if err := s.daemon.Repositories().Tag(repoName, tag, string(imgID), true); err != nil {
|
||||
return errf(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
139
builder/builder.go
Normal file
139
builder/builder.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
// Package builder defines interfaces for any Docker builder to implement.
|
||||
//
|
||||
// Historically, only server-side Dockerfile interpreters existed.
|
||||
// This package allows for other implementations of Docker builders.
|
||||
package builder
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
// TODO: remove dependency on daemon
|
||||
"github.com/docker/docker/daemon"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/runconfig"
|
||||
)
|
||||
|
||||
// Builder abstracts a Docker builder whose only purpose is to build a Docker image referenced by an imageID.
|
||||
type Builder interface {
|
||||
// Build builds a Docker image referenced by an imageID string.
|
||||
//
|
||||
// Note: Tagging an image should not be done by a Builder, it should instead be done
|
||||
// by the caller.
|
||||
//
|
||||
// TODO: make this return a reference instead of string
|
||||
Build() (imageID string)
|
||||
}
|
||||
|
||||
// Context represents a file system tree.
|
||||
type Context interface {
|
||||
// Close allows to signal that the filesystem tree won't be used anymore.
|
||||
// For Context implementations using a temporary directory, it is recommended to
|
||||
// delete the temporary directory in Close().
|
||||
Close() error
|
||||
// Stat returns an entry corresponding to path if any.
|
||||
// It is recommended to return an error if path was not found.
|
||||
Stat(path string) (FileInfo, error)
|
||||
// Open opens path from the context and returns a readable stream of it.
|
||||
Open(path string) (io.ReadCloser, error)
|
||||
// Walk walks the tree of the context with the function passed to it.
|
||||
Walk(root string, walkFn WalkFunc) error
|
||||
}
|
||||
|
||||
// WalkFunc is the type of the function called for each file or directory visited by Context.Walk().
|
||||
type WalkFunc func(path string, fi FileInfo, err error) error
|
||||
|
||||
// ModifiableContext represents a modifiable Context.
|
||||
// TODO: remove this interface once we can get rid of Remove()
|
||||
type ModifiableContext interface {
|
||||
Context
|
||||
// Remove deletes the entry specified by `path`.
|
||||
// It is usual for directory entries to delete all its subentries.
|
||||
Remove(path string) error
|
||||
}
|
||||
|
||||
// FileInfo extends os.FileInfo to allow retrieving an absolute path to the file.
|
||||
// TODO: remove this interface once pkg/archive exposes a walk function that Context can use.
|
||||
type FileInfo interface {
|
||||
os.FileInfo
|
||||
Path() string
|
||||
}
|
||||
|
||||
// PathFileInfo is a convenience struct that implements the FileInfo interface.
|
||||
type PathFileInfo struct {
|
||||
os.FileInfo
|
||||
// FilePath holds the absolute path to the file.
|
||||
FilePath string
|
||||
}
|
||||
|
||||
// Path returns the absolute path to the file.
|
||||
func (fi PathFileInfo) Path() string {
|
||||
return fi.FilePath
|
||||
}
|
||||
|
||||
// Hashed defines an extra method intended for implementations of os.FileInfo.
|
||||
type Hashed interface {
|
||||
// Hash returns the hash of a file.
|
||||
Hash() string
|
||||
SetHash(string)
|
||||
}
|
||||
|
||||
// HashedFileInfo is a convenient struct that augments FileInfo with a field.
|
||||
type HashedFileInfo struct {
|
||||
FileInfo
|
||||
// FileHash represents the hash of a file.
|
||||
FileHash string
|
||||
}
|
||||
|
||||
// Hash returns the hash of a file.
|
||||
func (fi HashedFileInfo) Hash() string {
|
||||
return fi.FileHash
|
||||
}
|
||||
|
||||
// SetHash sets the hash of a file.
|
||||
func (fi *HashedFileInfo) SetHash(h string) {
|
||||
fi.FileHash = h
|
||||
}
|
||||
|
||||
// Docker abstracts calls to a Docker Daemon.
|
||||
type Docker interface {
|
||||
// TODO: use digest reference instead of name
|
||||
|
||||
// LookupImage looks up a Docker image referenced by `name`.
|
||||
LookupImage(name string) (*image.Image, error)
|
||||
// Pull tells Docker to pull image referenced by `name`.
|
||||
Pull(name string) (*image.Image, error)
|
||||
|
||||
// TODO: move daemon.Container to its own package
|
||||
|
||||
// Container looks up a Docker container referenced by `id`.
|
||||
Container(id string) (*daemon.Container, error)
|
||||
// Create creates a new Docker container and returns potential warnings
|
||||
// TODO: put warnings in the error
|
||||
Create(*runconfig.Config, *runconfig.HostConfig) (*daemon.Container, []string, error)
|
||||
// Remove removes a container specified by `id`.
|
||||
Remove(id string, cfg *daemon.ContainerRmConfig) error
|
||||
// Commit creates a new Docker image from an existing Docker container.
|
||||
Commit(*daemon.Container, *daemon.ContainerCommitConfig) (*image.Image, error)
|
||||
// Copy copies/extracts a source FileInfo to a destination path inside a container
|
||||
// specified by a container object.
|
||||
// TODO: make an Extract method instead of passing `decompress`
|
||||
// TODO: do not pass a FileInfo, instead refactor the archive package to export a Walk function that can be used
|
||||
// with Context.Walk
|
||||
Copy(c *daemon.Container, destPath string, src FileInfo, decompress bool) error
|
||||
|
||||
// Retain retains an image avoiding it to be removed or overwritten until a corresponding Release() call.
|
||||
// TODO: remove
|
||||
Retain(sessionID, imgID string)
|
||||
// Release releases a list of images that were retained for the time of a build.
|
||||
// TODO: remove
|
||||
Release(sessionID string, activeImages []string)
|
||||
}
|
||||
|
||||
// ImageCache abstracts an image cache store.
|
||||
// (parent image, child runconfig) -> child image
|
||||
type ImageCache interface {
|
||||
// GetCachedImage returns a reference to a cached image whose parent equals `parent`
|
||||
// and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error.
|
||||
GetCachedImage(parentID string, cfg *runconfig.Config) (imageID string, err error)
|
||||
}
|
292
builder/dockerfile/builder.go
Normal file
292
builder/dockerfile/builder.go
Normal file
|
@ -0,0 +1,292 @@
|
|||
package dockerfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/builder"
|
||||
"github.com/docker/docker/builder/dockerfile/parser"
|
||||
"github.com/docker/docker/daemon"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/ulimit"
|
||||
"github.com/docker/docker/runconfig"
|
||||
)
|
||||
|
||||
var validCommitCommands = map[string]bool{
|
||||
"cmd": true,
|
||||
"entrypoint": true,
|
||||
"env": true,
|
||||
"expose": true,
|
||||
"label": true,
|
||||
"onbuild": true,
|
||||
"user": true,
|
||||
"volume": true,
|
||||
"workdir": true,
|
||||
}
|
||||
|
||||
// BuiltinAllowedBuildArgs is list of built-in allowed build args
|
||||
var BuiltinAllowedBuildArgs = map[string]bool{
|
||||
"HTTP_PROXY": true,
|
||||
"http_proxy": true,
|
||||
"HTTPS_PROXY": true,
|
||||
"https_proxy": true,
|
||||
"FTP_PROXY": true,
|
||||
"ftp_proxy": true,
|
||||
"NO_PROXY": true,
|
||||
"no_proxy": true,
|
||||
}
|
||||
|
||||
// Config constitutes the configuration for a Dockerfile builder.
|
||||
type Config struct {
|
||||
// only used if Dockerfile has to be extracted from Context
|
||||
DockerfileName string
|
||||
|
||||
Verbose bool
|
||||
UseCache bool
|
||||
Remove bool
|
||||
ForceRemove bool
|
||||
Pull bool
|
||||
BuildArgs map[string]string // build-time args received in build context for expansion/substitution and commands in 'run'.
|
||||
|
||||
// resource constraints
|
||||
// TODO: factor out to be reused with Run ?
|
||||
|
||||
Memory int64
|
||||
MemorySwap int64
|
||||
CPUShares int64
|
||||
CPUPeriod int64
|
||||
CPUQuota int64
|
||||
CPUSetCpus string
|
||||
CPUSetMems string
|
||||
CgroupParent string
|
||||
Ulimits []*ulimit.Ulimit
|
||||
}
|
||||
|
||||
// Builder is a Dockerfile builder
|
||||
// It implements the builder.Builder interface.
|
||||
type Builder struct {
|
||||
*Config
|
||||
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
docker builder.Docker
|
||||
context builder.Context
|
||||
|
||||
dockerfile *parser.Node
|
||||
runConfig *runconfig.Config // runconfig for cmd, run, entrypoint etc.
|
||||
flags *BFlags
|
||||
tmpContainers map[string]struct{}
|
||||
image string // imageID
|
||||
noBaseImage bool
|
||||
maintainer string
|
||||
cmdSet bool
|
||||
disableCommit bool
|
||||
cacheBusted bool
|
||||
cancelled chan struct{}
|
||||
cancelOnce sync.Once
|
||||
allowedBuildArgs map[string]bool // list of build-time args that are allowed for expansion/substitution and passing to commands in 'run'.
|
||||
|
||||
// TODO: remove once docker.Commit can receive a tag
|
||||
id string
|
||||
activeImages []string
|
||||
}
|
||||
|
||||
// NewBuilder creates a new Dockerfile builder from an optional dockerfile and a Config.
|
||||
// If dockerfile is nil, the Dockerfile specified by Config.DockerfileName,
|
||||
// will be read from the Context passed to Build().
|
||||
func NewBuilder(config *Config, docker builder.Docker, context builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) {
|
||||
if config == nil {
|
||||
config = new(Config)
|
||||
}
|
||||
if config.BuildArgs == nil {
|
||||
config.BuildArgs = make(map[string]string)
|
||||
}
|
||||
b = &Builder{
|
||||
Config: config,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
docker: docker,
|
||||
context: context,
|
||||
runConfig: new(runconfig.Config),
|
||||
tmpContainers: map[string]struct{}{},
|
||||
cancelled: make(chan struct{}),
|
||||
id: stringid.GenerateNonCryptoID(),
|
||||
allowedBuildArgs: make(map[string]bool),
|
||||
}
|
||||
if dockerfile != nil {
|
||||
b.dockerfile, err = parser.Parse(dockerfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Build runs the Dockerfile builder from a context and a docker object that allows to make calls
|
||||
// to Docker.
|
||||
//
|
||||
// This will (barring errors):
|
||||
//
|
||||
// * read the dockerfile from context
|
||||
// * parse the dockerfile if not already parsed
|
||||
// * walk the AST 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.
|
||||
// * NOT tag the image, that is responsibility of the caller.
|
||||
//
|
||||
func (b *Builder) Build() (string, error) {
|
||||
// TODO: remove once b.docker.Commit can take a tag parameter.
|
||||
defer func() {
|
||||
b.docker.Release(b.id, b.activeImages)
|
||||
}()
|
||||
|
||||
// If Dockerfile was not parsed yet, extract it from the Context
|
||||
if b.dockerfile == nil {
|
||||
if err := b.readDockerfile(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
var shortImgID string
|
||||
for i, n := range b.dockerfile.Children {
|
||||
select {
|
||||
case <-b.cancelled:
|
||||
logrus.Debug("Builder: build cancelled!")
|
||||
fmt.Fprintf(b.Stdout, "Build cancelled")
|
||||
return "", fmt.Errorf("Build cancelled")
|
||||
default:
|
||||
// Not cancelled yet, keep going...
|
||||
}
|
||||
if err := b.dispatch(i, n); err != nil {
|
||||
if b.ForceRemove {
|
||||
b.clearTmp()
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
shortImgID = stringid.TruncateID(b.image)
|
||||
fmt.Fprintf(b.Stdout, " ---> %s\n", shortImgID)
|
||||
if b.Remove {
|
||||
b.clearTmp()
|
||||
}
|
||||
}
|
||||
|
||||
// check if there are any leftover build-args that were passed but not
|
||||
// consumed during build. Return an error, if there are any.
|
||||
leftoverArgs := []string{}
|
||||
for arg := range b.BuildArgs {
|
||||
if !b.isBuildArgAllowed(arg) {
|
||||
leftoverArgs = append(leftoverArgs, arg)
|
||||
}
|
||||
}
|
||||
if len(leftoverArgs) > 0 {
|
||||
return "", fmt.Errorf("One or more build-args %v were not consumed, failing build.", leftoverArgs)
|
||||
}
|
||||
|
||||
if b.image == "" {
|
||||
return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?")
|
||||
}
|
||||
|
||||
fmt.Fprintf(b.Stdout, "Successfully built %s\n", shortImgID)
|
||||
return b.image, nil
|
||||
}
|
||||
|
||||
// Cancel cancels an ongoing Dockerfile build.
|
||||
func (b *Builder) Cancel() {
|
||||
b.cancelOnce.Do(func() {
|
||||
close(b.cancelled)
|
||||
})
|
||||
}
|
||||
|
||||
// CommitConfig contains build configs for commit operation
|
||||
type CommitConfig struct {
|
||||
Pause bool
|
||||
Repo string
|
||||
Tag string
|
||||
Author string
|
||||
Comment string
|
||||
Changes []string
|
||||
Config *runconfig.Config
|
||||
}
|
||||
|
||||
// BuildFromConfig will do build directly from parameter 'changes', which comes
|
||||
// from Dockerfile entries, it will:
|
||||
// - call parse.Parse() to get AST root from Dockerfile entries
|
||||
// - do build by calling builder.dispatch() to call all entries' handling routines
|
||||
// TODO: remove?
|
||||
func BuildFromConfig(config *runconfig.Config, changes []string) (*runconfig.Config, error) {
|
||||
ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that the commands are valid
|
||||
for _, n := range ast.Children {
|
||||
if !validCommitCommands[n.Value] {
|
||||
return nil, fmt.Errorf("%s is not a valid change command", n.Value)
|
||||
}
|
||||
}
|
||||
|
||||
b, err := NewBuilder(nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.runConfig = config
|
||||
b.Stdout = ioutil.Discard
|
||||
b.Stderr = ioutil.Discard
|
||||
b.disableCommit = true
|
||||
|
||||
for i, n := range ast.Children {
|
||||
if err := b.dispatch(i, n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return b.runConfig, nil
|
||||
}
|
||||
|
||||
// Commit will create a new image from a container's changes
|
||||
// TODO: remove daemon, make Commit a method on *Builder ?
|
||||
func Commit(container *daemon.Container, d *daemon.Daemon, c *CommitConfig) (string, error) {
|
||||
// It is not possible to commit a running container on Windows
|
||||
if runtime.GOOS == "windows" && container.IsRunning() {
|
||||
return "", fmt.Errorf("Windows does not support commit of a running container")
|
||||
}
|
||||
|
||||
if c.Config == nil {
|
||||
c.Config = &runconfig.Config{}
|
||||
}
|
||||
|
||||
newConfig, err := BuildFromConfig(c.Config, c.Changes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := runconfig.Merge(newConfig, container.Config); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
commitCfg := &daemon.ContainerCommitConfig{
|
||||
Pause: c.Pause,
|
||||
Repo: c.Repo,
|
||||
Tag: c.Tag,
|
||||
Author: c.Author,
|
||||
Comment: c.Comment,
|
||||
Config: newConfig,
|
||||
}
|
||||
|
||||
img, err := d.Commit(container, commitCfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return img.ID, nil
|
||||
}
|
|
@ -19,6 +19,7 @@ import (
|
|||
|
||||
"github.com/Sirupsen/logrus"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/image"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/nat"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
|
@ -34,7 +35,7 @@ const (
|
|||
)
|
||||
|
||||
// dispatch with no layer / parsing. This is effectively not a command.
|
||||
func nullDispatch(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func nullDispatch(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -43,7 +44,7 @@ func nullDispatch(b *builder, args []string, attributes map[string]bool, origina
|
|||
// Sets the environment variable foo to bar, also makes interpolation
|
||||
// in the dockerfile available from the next statement on via ${foo}.
|
||||
//
|
||||
func env(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func env(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) == 0 {
|
||||
return derr.ErrorCodeAtLeastOneArg.WithArgs("ENV")
|
||||
}
|
||||
|
@ -53,7 +54,7 @@ func env(b *builder, args []string, attributes map[string]bool, original string)
|
|||
return derr.ErrorCodeTooManyArgs.WithArgs("ENV")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -62,10 +63,10 @@ func env(b *builder, args []string, attributes map[string]bool, original string)
|
|||
// context of a builder command. Will remove once we actually add
|
||||
// a builder command to something!
|
||||
/*
|
||||
flBool1 := b.BuilderFlags.AddBool("bool1", false)
|
||||
flStr1 := b.BuilderFlags.AddString("str1", "HI")
|
||||
flBool1 := b.flags.AddBool("bool1", false)
|
||||
flStr1 := b.flags.AddString("str1", "HI")
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -82,44 +83,44 @@ func env(b *builder, args []string, attributes map[string]bool, original string)
|
|||
commitStr += " " + newVar
|
||||
|
||||
gotOne := false
|
||||
for i, envVar := range b.Config.Env {
|
||||
for i, envVar := range b.runConfig.Env {
|
||||
envParts := strings.SplitN(envVar, "=", 2)
|
||||
if envParts[0] == args[j] {
|
||||
b.Config.Env[i] = newVar
|
||||
b.runConfig.Env[i] = newVar
|
||||
gotOne = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !gotOne {
|
||||
b.Config.Env = append(b.Config.Env, newVar)
|
||||
b.runConfig.Env = append(b.runConfig.Env, newVar)
|
||||
}
|
||||
j++
|
||||
}
|
||||
|
||||
return b.commit("", b.Config.Cmd, commitStr)
|
||||
return b.commit("", b.runConfig.Cmd, commitStr)
|
||||
}
|
||||
|
||||
// MAINTAINER some text <maybe@an.email.address>
|
||||
//
|
||||
// Sets the maintainer metadata.
|
||||
func maintainer(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func maintainer(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) != 1 {
|
||||
return derr.ErrorCodeExactlyOneArg.WithArgs("MAINTAINER")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.maintainer = args[0]
|
||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer))
|
||||
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer))
|
||||
}
|
||||
|
||||
// LABEL some json data describing the image
|
||||
//
|
||||
// Sets the Label variable foo to bar,
|
||||
//
|
||||
func label(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func label(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) == 0 {
|
||||
return derr.ErrorCodeAtLeastOneArg.WithArgs("LABEL")
|
||||
}
|
||||
|
@ -128,14 +129,14 @@ func label(b *builder, args []string, attributes map[string]bool, original strin
|
|||
return derr.ErrorCodeTooManyArgs.WithArgs("LABEL")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commitStr := "LABEL"
|
||||
|
||||
if b.Config.Labels == nil {
|
||||
b.Config.Labels = map[string]string{}
|
||||
if b.runConfig.Labels == nil {
|
||||
b.runConfig.Labels = map[string]string{}
|
||||
}
|
||||
|
||||
for j := 0; j < len(args); j++ {
|
||||
|
@ -144,10 +145,10 @@ func label(b *builder, args []string, attributes map[string]bool, original strin
|
|||
newVar := args[j] + "=" + args[j+1] + ""
|
||||
commitStr += " " + newVar
|
||||
|
||||
b.Config.Labels[args[j]] = args[j+1]
|
||||
b.runConfig.Labels[args[j]] = args[j+1]
|
||||
j++
|
||||
}
|
||||
return b.commit("", b.Config.Cmd, commitStr)
|
||||
return b.commit("", b.runConfig.Cmd, commitStr)
|
||||
}
|
||||
|
||||
// ADD foo /path
|
||||
|
@ -155,12 +156,12 @@ func label(b *builder, args []string, attributes map[string]bool, original strin
|
|||
// Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
|
||||
// exist here. If you do not wish to have this automatic handling, use COPY.
|
||||
//
|
||||
func add(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func add(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) < 2 {
|
||||
return derr.ErrorCodeAtLeastTwoArgs.WithArgs("ADD")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -171,12 +172,12 @@ func add(b *builder, args []string, attributes map[string]bool, original string)
|
|||
//
|
||||
// Same as 'ADD' but without the tar and remote url handling.
|
||||
//
|
||||
func dispatchCopy(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func dispatchCopy(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) < 2 {
|
||||
return derr.ErrorCodeAtLeastTwoArgs.WithArgs("COPY")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -187,12 +188,12 @@ func dispatchCopy(b *builder, args []string, attributes map[string]bool, origina
|
|||
//
|
||||
// This sets the image the dockerfile will build on top of.
|
||||
//
|
||||
func from(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func from(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) != 1 {
|
||||
return derr.ErrorCodeExactlyOneArg.WithArgs("FROM")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -208,25 +209,21 @@ func from(b *builder, args []string, attributes map[string]bool, original string
|
|||
return nil
|
||||
}
|
||||
|
||||
image, err := b.Daemon.Repositories().LookupImage(name)
|
||||
if b.Pull {
|
||||
image, err = b.pullImage(name)
|
||||
var (
|
||||
image *image.Image
|
||||
err error
|
||||
)
|
||||
// TODO: don't use `name`, instead resolve it to a digest
|
||||
if !b.Pull {
|
||||
image, err = b.docker.LookupImage(name)
|
||||
// TODO: shouldn't we error out if error is different from "not found" ?
|
||||
}
|
||||
if image == nil {
|
||||
image, err = b.docker.Pull(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if b.Daemon.Graph().IsNotExist(err, name) {
|
||||
image, err = b.pullImage(name)
|
||||
}
|
||||
|
||||
// note that the top level err will still be !nil here if IsNotExist is
|
||||
// not the error. This approach just simplifies the logic a bit.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return b.processImageFrom(image)
|
||||
}
|
||||
|
||||
|
@ -239,12 +236,12 @@ func from(b *builder, args []string, attributes map[string]bool, original string
|
|||
// special cases. search for 'OnBuild' in internals.go for additional special
|
||||
// cases.
|
||||
//
|
||||
func onbuild(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func onbuild(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) == 0 {
|
||||
return derr.ErrorCodeAtLeastOneArg.WithArgs("ONBUILD")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -258,20 +255,20 @@ func onbuild(b *builder, args []string, attributes map[string]bool, original str
|
|||
|
||||
original = regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(original, "")
|
||||
|
||||
b.Config.OnBuild = append(b.Config.OnBuild, original)
|
||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("ONBUILD %s", original))
|
||||
b.runConfig.OnBuild = append(b.runConfig.OnBuild, original)
|
||||
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ONBUILD %s", original))
|
||||
}
|
||||
|
||||
// WORKDIR /tmp
|
||||
//
|
||||
// Set the working directory for future RUN/CMD/etc statements.
|
||||
//
|
||||
func workdir(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func workdir(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) != 1 {
|
||||
return derr.ErrorCodeExactlyOneArg.WithArgs("WORKDIR")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -280,13 +277,13 @@ func workdir(b *builder, args []string, attributes map[string]bool, original str
|
|||
workdir := filepath.FromSlash(args[0])
|
||||
|
||||
if !system.IsAbs(workdir) {
|
||||
current := filepath.FromSlash(b.Config.WorkingDir)
|
||||
current := filepath.FromSlash(b.runConfig.WorkingDir)
|
||||
workdir = filepath.Join(string(os.PathSeparator), current, workdir)
|
||||
}
|
||||
|
||||
b.Config.WorkingDir = workdir
|
||||
b.runConfig.WorkingDir = workdir
|
||||
|
||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("WORKDIR %v", workdir))
|
||||
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("WORKDIR %v", workdir))
|
||||
}
|
||||
|
||||
// RUN some command yo
|
||||
|
@ -299,12 +296,12 @@ func workdir(b *builder, args []string, attributes map[string]bool, original str
|
|||
// RUN echo hi # cmd /S /C echo hi (Windows)
|
||||
// RUN [ "echo", "hi" ] # echo hi
|
||||
//
|
||||
func run(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func run(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if b.image == "" && !b.noBaseImage {
|
||||
return derr.ErrorCodeMissingFrom
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -328,13 +325,13 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
|
|||
}
|
||||
|
||||
// stash the cmd
|
||||
cmd := b.Config.Cmd
|
||||
runconfig.Merge(b.Config, config)
|
||||
cmd := b.runConfig.Cmd
|
||||
runconfig.Merge(b.runConfig, config)
|
||||
// stash the config environment
|
||||
env := b.Config.Env
|
||||
env := b.runConfig.Env
|
||||
|
||||
defer func(cmd *stringutils.StrSlice) { b.Config.Cmd = cmd }(cmd)
|
||||
defer func(env []string) { b.Config.Env = env }(env)
|
||||
defer func(cmd *stringutils.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
|
||||
defer func(env []string) { b.runConfig.Env = env }(env)
|
||||
|
||||
// derive the net build-time environment for this run. We let config
|
||||
// environment override the build time environment.
|
||||
|
@ -350,8 +347,8 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
|
|||
// of RUN, without leaking it to the final image. It also aids cache
|
||||
// lookup for same image built with same build time environment.
|
||||
cmdBuildEnv := []string{}
|
||||
configEnv := runconfig.ConvertKVStringsToMap(b.Config.Env)
|
||||
for key, val := range b.buildArgs {
|
||||
configEnv := runconfig.ConvertKVStringsToMap(b.runConfig.Env)
|
||||
for key, val := range b.BuildArgs {
|
||||
if !b.isBuildArgAllowed(key) {
|
||||
// skip build-args that are not in allowed list, meaning they have
|
||||
// not been defined by an "ARG" Dockerfile command yet.
|
||||
|
@ -379,7 +376,7 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
|
|||
saveCmd = stringutils.NewStrSlice(append(tmpEnv, saveCmd.Slice()...)...)
|
||||
}
|
||||
|
||||
b.Config.Cmd = saveCmd
|
||||
b.runConfig.Cmd = saveCmd
|
||||
hit, err := b.probeCache()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -389,11 +386,11 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
|
|||
}
|
||||
|
||||
// set Cmd manually, this is special case only for Dockerfiles
|
||||
b.Config.Cmd = config.Cmd
|
||||
b.runConfig.Cmd = config.Cmd
|
||||
// set build-time environment for 'run'.
|
||||
b.Config.Env = append(b.Config.Env, cmdBuildEnv...)
|
||||
b.runConfig.Env = append(b.runConfig.Env, cmdBuildEnv...)
|
||||
|
||||
logrus.Debugf("[BUILDER] Command to be executed: %v", b.Config.Cmd)
|
||||
logrus.Debugf("[BUILDER] Command to be executed: %v", b.runConfig.Cmd)
|
||||
|
||||
c, err := b.create()
|
||||
if err != nil {
|
||||
|
@ -413,8 +410,8 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
|
|||
// revert to original config environment and set the command string to
|
||||
// have the build-time env vars in it (if any) so that future cache look-ups
|
||||
// properly match it.
|
||||
b.Config.Env = env
|
||||
b.Config.Cmd = saveCmd
|
||||
b.runConfig.Env = env
|
||||
b.runConfig.Cmd = saveCmd
|
||||
if err := b.commit(c.ID, cmd, "run"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -427,8 +424,8 @@ func run(b *builder, args []string, attributes map[string]bool, original string)
|
|||
// Set the default command to run in the container (which may be empty).
|
||||
// Argument handling is the same as RUN.
|
||||
//
|
||||
func cmd(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
func cmd(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -442,9 +439,9 @@ func cmd(b *builder, args []string, attributes map[string]bool, original string)
|
|||
}
|
||||
}
|
||||
|
||||
b.Config.Cmd = stringutils.NewStrSlice(cmdSlice...)
|
||||
b.runConfig.Cmd = stringutils.NewStrSlice(cmdSlice...)
|
||||
|
||||
if err := b.commit("", b.Config.Cmd, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
|
||||
if err := b.commit("", b.runConfig.Cmd, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -460,11 +457,11 @@ func cmd(b *builder, args []string, attributes map[string]bool, original string)
|
|||
// Set the entrypoint (which defaults to sh -c on linux, or cmd /S /C on Windows) to
|
||||
// /usr/sbin/nginx. Will accept the CMD as the arguments to /usr/sbin/nginx.
|
||||
//
|
||||
// Handles command processing similar to CMD and RUN, only b.Config.Entrypoint
|
||||
// Handles command processing similar to CMD and RUN, only b.runConfig.Entrypoint
|
||||
// is initialized at NewBuilder time instead of through argument parsing.
|
||||
//
|
||||
func entrypoint(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
func entrypoint(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -473,26 +470,26 @@ func entrypoint(b *builder, args []string, attributes map[string]bool, original
|
|||
switch {
|
||||
case attributes["json"]:
|
||||
// ENTRYPOINT ["echo", "hi"]
|
||||
b.Config.Entrypoint = stringutils.NewStrSlice(parsed...)
|
||||
b.runConfig.Entrypoint = stringutils.NewStrSlice(parsed...)
|
||||
case len(parsed) == 0:
|
||||
// ENTRYPOINT []
|
||||
b.Config.Entrypoint = nil
|
||||
b.runConfig.Entrypoint = nil
|
||||
default:
|
||||
// ENTRYPOINT echo hi
|
||||
if runtime.GOOS != "windows" {
|
||||
b.Config.Entrypoint = stringutils.NewStrSlice("/bin/sh", "-c", parsed[0])
|
||||
b.runConfig.Entrypoint = stringutils.NewStrSlice("/bin/sh", "-c", parsed[0])
|
||||
} else {
|
||||
b.Config.Entrypoint = stringutils.NewStrSlice("cmd", "/S", "/C", parsed[0])
|
||||
b.runConfig.Entrypoint = stringutils.NewStrSlice("cmd", "/S /C", parsed[0])
|
||||
}
|
||||
}
|
||||
|
||||
// when setting the entrypoint if a CMD was not explicitly set then
|
||||
// set the command to nil
|
||||
if !b.cmdSet {
|
||||
b.Config.Cmd = nil
|
||||
b.runConfig.Cmd = nil
|
||||
}
|
||||
|
||||
if err := b.commit("", b.Config.Cmd, fmt.Sprintf("ENTRYPOINT %q", b.Config.Entrypoint)); err != nil {
|
||||
if err := b.commit("", b.runConfig.Cmd, fmt.Sprintf("ENTRYPOINT %q", b.runConfig.Entrypoint)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -502,21 +499,21 @@ func entrypoint(b *builder, args []string, attributes map[string]bool, original
|
|||
// EXPOSE 6667/tcp 7000/tcp
|
||||
//
|
||||
// Expose ports for links and port mappings. This all ends up in
|
||||
// b.Config.ExposedPorts for runconfig.
|
||||
// b.runConfig.ExposedPorts for runconfig.
|
||||
//
|
||||
func expose(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func expose(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
portsTab := args
|
||||
|
||||
if len(args) == 0 {
|
||||
return derr.ErrorCodeAtLeastOneArg.WithArgs("EXPOSE")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.Config.ExposedPorts == nil {
|
||||
b.Config.ExposedPorts = make(nat.PortSet)
|
||||
if b.runConfig.ExposedPorts == nil {
|
||||
b.runConfig.ExposedPorts = make(nat.PortSet)
|
||||
}
|
||||
|
||||
ports, _, err := nat.ParsePortSpecs(portsTab)
|
||||
|
@ -530,14 +527,14 @@ func expose(b *builder, args []string, attributes map[string]bool, original stri
|
|||
portList := make([]string, len(ports))
|
||||
var i int
|
||||
for port := range ports {
|
||||
if _, exists := b.Config.ExposedPorts[port]; !exists {
|
||||
b.Config.ExposedPorts[port] = struct{}{}
|
||||
if _, exists := b.runConfig.ExposedPorts[port]; !exists {
|
||||
b.runConfig.ExposedPorts[port] = struct{}{}
|
||||
}
|
||||
portList[i] = string(port)
|
||||
i++
|
||||
}
|
||||
sort.Strings(portList)
|
||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("EXPOSE %s", strings.Join(portList, " ")))
|
||||
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("EXPOSE %s", strings.Join(portList, " ")))
|
||||
}
|
||||
|
||||
// USER foo
|
||||
|
@ -545,43 +542,43 @@ func expose(b *builder, args []string, attributes map[string]bool, original stri
|
|||
// Set the user to 'foo' for future commands and when running the
|
||||
// ENTRYPOINT/CMD at container run time.
|
||||
//
|
||||
func user(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func user(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) != 1 {
|
||||
return derr.ErrorCodeExactlyOneArg.WithArgs("USER")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Config.User = args[0]
|
||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("USER %v", args))
|
||||
b.runConfig.User = args[0]
|
||||
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("USER %v", args))
|
||||
}
|
||||
|
||||
// VOLUME /foo
|
||||
//
|
||||
// Expose the volume /foo for use. Will also accept the JSON array form.
|
||||
//
|
||||
func volume(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func volume(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) == 0 {
|
||||
return derr.ErrorCodeAtLeastOneArg.WithArgs("VOLUME")
|
||||
}
|
||||
|
||||
if err := b.BuilderFlags.Parse(); err != nil {
|
||||
if err := b.flags.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.Config.Volumes == nil {
|
||||
b.Config.Volumes = map[string]struct{}{}
|
||||
if b.runConfig.Volumes == nil {
|
||||
b.runConfig.Volumes = map[string]struct{}{}
|
||||
}
|
||||
for _, v := range args {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return derr.ErrorCodeVolumeEmpty
|
||||
}
|
||||
b.Config.Volumes[v] = struct{}{}
|
||||
b.runConfig.Volumes[v] = struct{}{}
|
||||
}
|
||||
if err := b.commit("", b.Config.Cmd, fmt.Sprintf("VOLUME %v", args)); err != nil {
|
||||
if err := b.commit("", b.runConfig.Cmd, fmt.Sprintf("VOLUME %v", args)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -590,7 +587,7 @@ func volume(b *builder, args []string, attributes map[string]bool, original stri
|
|||
// STOPSIGNAL signal
|
||||
//
|
||||
// Set the signal that will be used to kill the container.
|
||||
func stopSignal(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func stopSignal(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("STOPSIGNAL requires exactly one argument")
|
||||
}
|
||||
|
@ -601,8 +598,8 @@ func stopSignal(b *builder, args []string, attributes map[string]bool, original
|
|||
return err
|
||||
}
|
||||
|
||||
b.Config.StopSignal = sig
|
||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("STOPSIGNAL %v", args))
|
||||
b.runConfig.StopSignal = sig
|
||||
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("STOPSIGNAL %v", args))
|
||||
}
|
||||
|
||||
// ARG name[=value]
|
||||
|
@ -610,7 +607,7 @@ func stopSignal(b *builder, args []string, attributes map[string]bool, original
|
|||
// Adds the variable foo to the trusted list of variables that can be passed
|
||||
// to builder using the --build-arg flag for expansion/subsitution or passing to 'run'.
|
||||
// Dockerfile author may optionally set a default value of this variable.
|
||||
func arg(b *builder, args []string, attributes map[string]bool, original string) error {
|
||||
func arg(b *Builder, args []string, attributes map[string]bool, original string) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("ARG requires exactly one argument definition")
|
||||
}
|
||||
|
@ -642,9 +639,9 @@ func arg(b *builder, args []string, attributes map[string]bool, original string)
|
|||
// If there is a default value associated with this arg then add it to the
|
||||
// b.buildArgs if one is not already passed to the builder. The args passed
|
||||
// to builder override the defaut value of 'arg'.
|
||||
if _, ok := b.buildArgs[name]; !ok && hasDefault {
|
||||
b.buildArgs[name] = value
|
||||
if _, ok := b.BuildArgs[name]; !ok && hasDefault {
|
||||
b.BuildArgs[name] = value
|
||||
}
|
||||
|
||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("ARG %s", arg))
|
||||
return b.commit("", b.runConfig.Cmd, fmt.Sprintf("ARG %s", arg))
|
||||
}
|
||||
|
|
|
@ -21,26 +21,11 @@ package dockerfile
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/builder/dockerfile/command"
|
||||
"github.com/docker/docker/builder/dockerfile/parser"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/daemon"
|
||||
"github.com/docker/docker/pkg/fileutils"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/symlink"
|
||||
"github.com/docker/docker/pkg/tarsum"
|
||||
"github.com/docker/docker/pkg/ulimit"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
// Environment variable interpolation will happen on these statements only.
|
||||
|
@ -57,10 +42,10 @@ var replaceEnvAllowed = map[string]struct{}{
|
|||
command.Arg: {},
|
||||
}
|
||||
|
||||
var evaluateTable map[string]func(*builder, []string, map[string]bool, string) error
|
||||
var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) error
|
||||
|
||||
func init() {
|
||||
evaluateTable = map[string]func(*builder, []string, map[string]bool, string) error{
|
||||
evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
|
||||
command.Env: env,
|
||||
command.Label: label,
|
||||
command.Maintainer: maintainer,
|
||||
|
@ -80,223 +65,6 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
// builder is an internal struct, used to maintain configuration of the Dockerfile's
|
||||
// processing as it evaluates the parsing result.
|
||||
type builder struct {
|
||||
Daemon *daemon.Daemon
|
||||
|
||||
// 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
|
||||
cacheBusted bool
|
||||
|
||||
// controls how images and containers are handled between steps.
|
||||
Remove bool
|
||||
ForceRemove bool
|
||||
Pull bool
|
||||
|
||||
// set this to true if we want the builder to not commit between steps.
|
||||
// This is useful when we only want to use the evaluator table to generate
|
||||
// the final configs of the Dockerfile but dont want the layers
|
||||
disableCommit bool
|
||||
|
||||
// Registry server auth configs used to pull images when handling `FROM`.
|
||||
AuthConfigs map[string]cliconfig.AuthConfig
|
||||
|
||||
// Deprecated, original writer used for ImagePull. To be removed.
|
||||
OutOld io.Writer
|
||||
StreamFormatter *streamformatter.StreamFormatter
|
||||
|
||||
Config *runconfig.Config // runconfig for cmd, run, entrypoint etc.
|
||||
|
||||
buildArgs map[string]string // build-time args received in build context for expansion/substitution and commands in 'run'.
|
||||
allowedBuildArgs map[string]bool // list of build-time args that are allowed for expansion/substitution and passing to commands in 'run'.
|
||||
|
||||
// both of these are controlled by the Remove and ForceRemove options in BuildOpts
|
||||
TmpContainers map[string]struct{} // a map of containers used for removes
|
||||
|
||||
dockerfileName string // name of Dockerfile
|
||||
dockerfile *parser.Node // the syntax tree of the dockerfile
|
||||
image string // image name for commit processing
|
||||
maintainer string // maintainer name. could probably be removed.
|
||||
cmdSet bool // indicates is CMD was set in current Dockerfile
|
||||
BuilderFlags *BFlags // current cmd's BuilderFlags - temporary
|
||||
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)
|
||||
noBaseImage bool // indicates that this build does not start from any base image, but is being built from an empty file system.
|
||||
|
||||
// Set resource restrictions for build containers
|
||||
cpuSetCpus string
|
||||
cpuSetMems string
|
||||
cpuShares int64
|
||||
cpuPeriod int64
|
||||
cpuQuota int64
|
||||
cgroupParent string
|
||||
memory int64
|
||||
memorySwap int64
|
||||
ulimits []*ulimit.Ulimit
|
||||
|
||||
cancelled <-chan struct{} // When closed, job was cancelled.
|
||||
|
||||
activeImages []string
|
||||
id string // Used to hold reference images
|
||||
}
|
||||
|
||||
// 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 *builder) Run(context io.Reader) (string, error) {
|
||||
if err := b.readContext(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(b.contextPath); err != nil {
|
||||
logrus.Debugf("[BUILDER] failed to remove temporary context: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := b.readDockerfile(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// some initializations that would not have been supplied by the caller.
|
||||
b.Config = &runconfig.Config{}
|
||||
|
||||
b.TmpContainers = map[string]struct{}{}
|
||||
|
||||
for i, n := range b.dockerfile.Children {
|
||||
select {
|
||||
case <-b.cancelled:
|
||||
logrus.Debug("Builder: build cancelled!")
|
||||
fmt.Fprintf(b.OutStream, "Build cancelled")
|
||||
return "", fmt.Errorf("Build cancelled")
|
||||
default:
|
||||
// Not cancelled yet, keep going...
|
||||
}
|
||||
if err := b.dispatch(i, n); err != nil {
|
||||
if b.ForceRemove {
|
||||
b.clearTmp()
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintf(b.OutStream, " ---> %s\n", stringid.TruncateID(b.image))
|
||||
if b.Remove {
|
||||
b.clearTmp()
|
||||
}
|
||||
}
|
||||
|
||||
// check if there are any leftover build-args that were passed but not
|
||||
// consumed during build. Return an error, if there are any.
|
||||
leftoverArgs := []string{}
|
||||
for arg := range b.buildArgs {
|
||||
if !b.isBuildArgAllowed(arg) {
|
||||
leftoverArgs = append(leftoverArgs, arg)
|
||||
}
|
||||
}
|
||||
if len(leftoverArgs) > 0 {
|
||||
return "", fmt.Errorf("One or more build-args %v were not consumed, failing build.", leftoverArgs)
|
||||
}
|
||||
|
||||
if b.image == "" {
|
||||
return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?")
|
||||
}
|
||||
|
||||
fmt.Fprintf(b.OutStream, "Successfully built %s\n", stringid.TruncateID(b.image))
|
||||
return b.image, nil
|
||||
}
|
||||
|
||||
// Reads a Dockerfile from the current context. It assumes that the
|
||||
// 'filename' is a relative path from the root of the context
|
||||
func (b *builder) readDockerfile() error {
|
||||
// If no -f was specified then look for 'Dockerfile'. If we can't find
|
||||
// that then look for 'dockerfile'. If neither are found then default
|
||||
// back to 'Dockerfile' and use that in the error message.
|
||||
if b.dockerfileName == "" {
|
||||
b.dockerfileName = api.DefaultDockerfileName
|
||||
tmpFN := filepath.Join(b.contextPath, api.DefaultDockerfileName)
|
||||
if _, err := os.Lstat(tmpFN); err != nil {
|
||||
tmpFN = filepath.Join(b.contextPath, strings.ToLower(api.DefaultDockerfileName))
|
||||
if _, err := os.Lstat(tmpFN); err == nil {
|
||||
b.dockerfileName = strings.ToLower(api.DefaultDockerfileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
origFile := b.dockerfileName
|
||||
|
||||
filename, err := symlink.FollowSymlinkInScope(filepath.Join(b.contextPath, origFile), b.contextPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("The Dockerfile (%s) must be within the build context", origFile)
|
||||
}
|
||||
|
||||
fi, err := os.Lstat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("Cannot locate specified Dockerfile: %s", origFile)
|
||||
}
|
||||
if fi.Size() == 0 {
|
||||
return fmt.Errorf("The Dockerfile (%s) cannot be empty", origFile)
|
||||
}
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.dockerfile, err = parser.Parse(f)
|
||||
f.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// After the Dockerfile has been parsed, we need to check the .dockerignore
|
||||
// file for either "Dockerfile" or ".dockerignore", and if either are
|
||||
// present then erase them from the build context. These files should never
|
||||
// have been sent from the client but we did send them to make sure that
|
||||
// we had the Dockerfile to actually parse, and then we also need the
|
||||
// .dockerignore file to know whether either file should be removed.
|
||||
// Note that this assumes the Dockerfile has been read into memory and
|
||||
// is now safe to be removed.
|
||||
|
||||
excludes, _ := utils.ReadDockerIgnore(filepath.Join(b.contextPath, ".dockerignore"))
|
||||
if rm, _ := fileutils.Matches(".dockerignore", excludes); rm == true {
|
||||
os.Remove(filepath.Join(b.contextPath, ".dockerignore"))
|
||||
b.context.(tarsum.BuilderContext).Remove(".dockerignore")
|
||||
}
|
||||
if rm, _ := fileutils.Matches(b.dockerfileName, excludes); rm == true {
|
||||
os.Remove(filepath.Join(b.contextPath, b.dockerfileName))
|
||||
b.context.(tarsum.BuilderContext).Remove(b.dockerfileName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// determine if build arg is part of built-in args or user
|
||||
// defined args in Dockerfile at any point in time.
|
||||
func (b *builder) isBuildArgAllowed(arg string) bool {
|
||||
if _, ok := BuiltinAllowedBuildArgs[arg]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := b.allowedBuildArgs[arg]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// This method is the entrypoint to all statement handling routines.
|
||||
//
|
||||
// Almost all nodes will have this structure:
|
||||
|
@ -311,8 +79,9 @@ func (b *builder) isBuildArgAllowed(arg string) bool {
|
|||
// 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(stepN int, ast *parser.Node) error {
|
||||
func (b *Builder) dispatch(stepN int, 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.
|
||||
|
@ -324,7 +93,7 @@ func (b *builder) dispatch(stepN int, ast *parser.Node) error {
|
|||
original := ast.Original
|
||||
flags := ast.Flags
|
||||
strs := []string{}
|
||||
msg := fmt.Sprintf("Step %d : %s", stepN+1, strings.ToUpper(cmd))
|
||||
msg := fmt.Sprintf("Step %d : %s", stepN+1, upperCasedCmd)
|
||||
|
||||
if len(ast.Flags) > 0 {
|
||||
msg += " " + strings.Join(ast.Flags, " ")
|
||||
|
@ -368,8 +137,8 @@ func (b *builder) dispatch(stepN int, ast *parser.Node) error {
|
|||
// stop on the first occurrence of a variable name and not notice
|
||||
// a subsequent one. So, putting the buildArgs list after the Config.Env
|
||||
// list, in 'envs', is safe.
|
||||
envs := b.Config.Env
|
||||
for key, val := range b.buildArgs {
|
||||
envs := b.runConfig.Env
|
||||
for key, val := range b.BuildArgs {
|
||||
if !b.isBuildArgAllowed(key) {
|
||||
// skip build-args that are not in allowed list, meaning they have
|
||||
// not been defined by an "ARG" Dockerfile command yet.
|
||||
|
@ -397,17 +166,17 @@ func (b *builder) dispatch(stepN int, ast *parser.Node) error {
|
|||
}
|
||||
|
||||
msg += " " + strings.Join(msgList, " ")
|
||||
fmt.Fprintln(b.OutStream, msg)
|
||||
fmt.Fprintln(b.Stdout, msg)
|
||||
|
||||
// XXX yes, we skip any cmds that are not valid; the parser should have
|
||||
// picked these out already.
|
||||
if f, ok := evaluateTable[cmd]; ok {
|
||||
b.BuilderFlags = NewBFlags()
|
||||
b.BuilderFlags.Args = flags
|
||||
b.flags = NewBFlags()
|
||||
b.flags.Args = flags
|
||||
return f(b, strList, attrs, original)
|
||||
}
|
||||
|
||||
return fmt.Errorf("Unknown instruction: %s", strings.ToUpper(cmd))
|
||||
return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
|
||||
}
|
||||
|
||||
// platformSupports is a short-term function to give users a quality error
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,17 +1,12 @@
|
|||
// +build freebsd linux
|
||||
// +build !windows
|
||||
|
||||
package dockerfile
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func getTempDir(dir, prefix string) (string, error) {
|
||||
return ioutil.TempDir(dir, prefix)
|
||||
}
|
||||
|
||||
func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
|
||||
// If the destination didn't already exist, or the destination isn't a
|
||||
// directory, then we should Lchown the destination. Otherwise, we shouldn't
|
||||
|
|
|
@ -2,20 +2,6 @@
|
|||
|
||||
package dockerfile
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/docker/docker/pkg/longpath"
|
||||
)
|
||||
|
||||
func getTempDir(dir, prefix string) (string, error) {
|
||||
tempDir, err := ioutil.TempDir(dir, prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return longpath.AddPrefix(tempDir), nil
|
||||
}
|
||||
|
||||
func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
|
||||
// chown is not supported on Windows
|
||||
return nil
|
||||
|
|
|
@ -1,376 +0,0 @@
|
|||
package dockerfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/builder/dockerfile/parser"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/daemon"
|
||||
"github.com/docker/docker/graph/tags"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/ulimit"
|
||||
"github.com/docker/docker/pkg/urlutil"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
// When downloading remote contexts, limit the amount (in bytes)
|
||||
// to be read from the response body in order to detect its Content-Type
|
||||
const maxPreambleLength = 100
|
||||
|
||||
// whitelist of commands allowed for a commit/import
|
||||
var validCommitCommands = map[string]bool{
|
||||
"cmd": true,
|
||||
"entrypoint": true,
|
||||
"env": true,
|
||||
"expose": true,
|
||||
"label": true,
|
||||
"onbuild": true,
|
||||
"user": true,
|
||||
"volume": true,
|
||||
"workdir": true,
|
||||
}
|
||||
|
||||
// BuiltinAllowedBuildArgs is list of built-in allowed build args
|
||||
var BuiltinAllowedBuildArgs = map[string]bool{
|
||||
"HTTP_PROXY": true,
|
||||
"http_proxy": true,
|
||||
"HTTPS_PROXY": true,
|
||||
"https_proxy": true,
|
||||
"FTP_PROXY": true,
|
||||
"ftp_proxy": true,
|
||||
"NO_PROXY": true,
|
||||
"no_proxy": true,
|
||||
}
|
||||
|
||||
// Config contains all configs for a build job
|
||||
type Config struct {
|
||||
DockerfileName string
|
||||
RemoteURL string
|
||||
RepoName string
|
||||
SuppressOutput bool
|
||||
NoCache bool
|
||||
Remove bool
|
||||
ForceRemove bool
|
||||
Pull bool
|
||||
Memory int64
|
||||
MemorySwap int64
|
||||
CPUShares int64
|
||||
CPUPeriod int64
|
||||
CPUQuota int64
|
||||
CPUSetCpus string
|
||||
CPUSetMems string
|
||||
CgroupParent string
|
||||
Ulimits []*ulimit.Ulimit
|
||||
AuthConfigs map[string]cliconfig.AuthConfig
|
||||
BuildArgs map[string]string
|
||||
|
||||
Stdout io.Writer
|
||||
Context io.ReadCloser
|
||||
// When closed, the job has been cancelled.
|
||||
// Note: not all jobs implement cancellation.
|
||||
// See Job.Cancel() and Job.WaitCancelled()
|
||||
cancelled chan struct{}
|
||||
cancelOnce sync.Once
|
||||
}
|
||||
|
||||
// Cancel signals the build job to cancel
|
||||
func (b *Config) Cancel() {
|
||||
b.cancelOnce.Do(func() {
|
||||
close(b.cancelled)
|
||||
})
|
||||
}
|
||||
|
||||
// WaitCancelled returns a channel which is closed ("never blocks") when
|
||||
// the job is cancelled.
|
||||
func (b *Config) WaitCancelled() <-chan struct{} {
|
||||
return b.cancelled
|
||||
}
|
||||
|
||||
// NewBuildConfig returns a new Config struct
|
||||
func NewBuildConfig() *Config {
|
||||
return &Config{
|
||||
AuthConfigs: map[string]cliconfig.AuthConfig{},
|
||||
cancelled: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Build is the main interface of the package, it gathers the Builder
|
||||
// struct and calls builder.Run() to do all the real build job.
|
||||
func Build(d *daemon.Daemon, buildConfig *Config) error {
|
||||
var (
|
||||
repoName string
|
||||
tag string
|
||||
context io.ReadCloser
|
||||
)
|
||||
sf := streamformatter.NewJSONStreamFormatter()
|
||||
|
||||
repoName, tag = parsers.ParseRepositoryTag(buildConfig.RepoName)
|
||||
if repoName != "" {
|
||||
if err := registry.ValidateRepositoryName(repoName); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tag) > 0 {
|
||||
if err := tags.ValidateTagName(tag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if buildConfig.RemoteURL == "" {
|
||||
context = ioutil.NopCloser(buildConfig.Context)
|
||||
} else if urlutil.IsGitURL(buildConfig.RemoteURL) {
|
||||
root, err := utils.GitClone(buildConfig.RemoteURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
c, err := archive.Tar(root, archive.Uncompressed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
context = c
|
||||
} else if urlutil.IsURL(buildConfig.RemoteURL) {
|
||||
f, err := httputils.Download(buildConfig.RemoteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error downloading remote context %s: %v", buildConfig.RemoteURL, err)
|
||||
}
|
||||
defer f.Body.Close()
|
||||
ct := f.Header.Get("Content-Type")
|
||||
clen := f.ContentLength
|
||||
contentType, bodyReader, err := inspectResponse(ct, f.Body, clen)
|
||||
|
||||
defer bodyReader.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error detecting content type for remote %s: %v", buildConfig.RemoteURL, err)
|
||||
}
|
||||
if contentType == httputils.MimeTypes.TextPlain {
|
||||
dockerFile, err := ioutil.ReadAll(bodyReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// When we're downloading just a Dockerfile put it in
|
||||
// the default name - don't allow the client to move/specify it
|
||||
buildConfig.DockerfileName = api.DefaultDockerfileName
|
||||
|
||||
c, err := archive.Generate(buildConfig.DockerfileName, string(dockerFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
context = c
|
||||
} else {
|
||||
// Pass through - this is a pre-packaged context, presumably
|
||||
// with a Dockerfile with the right name inside it.
|
||||
prCfg := progressreader.Config{
|
||||
In: bodyReader,
|
||||
Out: buildConfig.Stdout,
|
||||
Formatter: sf,
|
||||
Size: clen,
|
||||
NewLines: true,
|
||||
ID: "Downloading context",
|
||||
Action: buildConfig.RemoteURL,
|
||||
}
|
||||
context = progressreader.New(prCfg)
|
||||
}
|
||||
}
|
||||
|
||||
defer context.Close()
|
||||
|
||||
builder := &builder{
|
||||
Daemon: d,
|
||||
OutStream: &streamformatter.StdoutFormatter{
|
||||
Writer: buildConfig.Stdout,
|
||||
StreamFormatter: sf,
|
||||
},
|
||||
ErrStream: &streamformatter.StderrFormatter{
|
||||
Writer: buildConfig.Stdout,
|
||||
StreamFormatter: sf,
|
||||
},
|
||||
Verbose: !buildConfig.SuppressOutput,
|
||||
UtilizeCache: !buildConfig.NoCache,
|
||||
Remove: buildConfig.Remove,
|
||||
ForceRemove: buildConfig.ForceRemove,
|
||||
Pull: buildConfig.Pull,
|
||||
OutOld: buildConfig.Stdout,
|
||||
StreamFormatter: sf,
|
||||
AuthConfigs: buildConfig.AuthConfigs,
|
||||
dockerfileName: buildConfig.DockerfileName,
|
||||
cpuShares: buildConfig.CPUShares,
|
||||
cpuPeriod: buildConfig.CPUPeriod,
|
||||
cpuQuota: buildConfig.CPUQuota,
|
||||
cpuSetCpus: buildConfig.CPUSetCpus,
|
||||
cpuSetMems: buildConfig.CPUSetMems,
|
||||
cgroupParent: buildConfig.CgroupParent,
|
||||
memory: buildConfig.Memory,
|
||||
memorySwap: buildConfig.MemorySwap,
|
||||
ulimits: buildConfig.Ulimits,
|
||||
cancelled: buildConfig.WaitCancelled(),
|
||||
id: stringid.GenerateRandomID(),
|
||||
buildArgs: buildConfig.BuildArgs,
|
||||
allowedBuildArgs: make(map[string]bool),
|
||||
}
|
||||
|
||||
defer func() {
|
||||
builder.Daemon.Graph().Release(builder.id, builder.activeImages...)
|
||||
}()
|
||||
|
||||
id, err := builder.Run(context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if repoName != "" {
|
||||
return d.Repositories().Tag(repoName, tag, id, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildFromConfig will do build directly from parameter 'changes', which comes
|
||||
// from Dockerfile entries, it will:
|
||||
//
|
||||
// - call parse.Parse() to get AST root from Dockerfile entries
|
||||
// - do build by calling builder.dispatch() to call all entries' handling routines
|
||||
func BuildFromConfig(d *daemon.Daemon, c *runconfig.Config, changes []string) (*runconfig.Config, error) {
|
||||
ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that the commands are valid
|
||||
for _, n := range ast.Children {
|
||||
if !validCommitCommands[n.Value] {
|
||||
return nil, fmt.Errorf("%s is not a valid change command", n.Value)
|
||||
}
|
||||
}
|
||||
|
||||
builder := &builder{
|
||||
Daemon: d,
|
||||
Config: c,
|
||||
OutStream: ioutil.Discard,
|
||||
ErrStream: ioutil.Discard,
|
||||
disableCommit: true,
|
||||
}
|
||||
|
||||
for i, n := range ast.Children {
|
||||
if err := builder.dispatch(i, n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Config, nil
|
||||
}
|
||||
|
||||
// CommitConfig contains build configs for commit operation
|
||||
type CommitConfig struct {
|
||||
Pause bool
|
||||
Repo string
|
||||
Tag string
|
||||
Author string
|
||||
Comment string
|
||||
Changes []string
|
||||
Config *runconfig.Config
|
||||
}
|
||||
|
||||
// Commit will create a new image from a container's changes
|
||||
func Commit(name string, d *daemon.Daemon, c *CommitConfig) (string, error) {
|
||||
container, err := d.Get(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// It is not possible to commit a running container on Windows
|
||||
if runtime.GOOS == "windows" && container.IsRunning() {
|
||||
return "", fmt.Errorf("Windows does not support commit of a running container")
|
||||
}
|
||||
|
||||
if c.Config == nil {
|
||||
c.Config = &runconfig.Config{}
|
||||
}
|
||||
|
||||
newConfig, err := BuildFromConfig(d, c.Config, c.Changes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := runconfig.Merge(newConfig, container.Config); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
commitCfg := &daemon.ContainerCommitConfig{
|
||||
Pause: c.Pause,
|
||||
Repo: c.Repo,
|
||||
Tag: c.Tag,
|
||||
Author: c.Author,
|
||||
Comment: c.Comment,
|
||||
Config: newConfig,
|
||||
}
|
||||
|
||||
img, err := d.Commit(container, commitCfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return img.ID, nil
|
||||
}
|
||||
|
||||
// inspectResponse looks into the http response data at r to determine whether its
|
||||
// content-type is on the list of acceptable content types for remote build contexts.
|
||||
// This function returns:
|
||||
// - a string representation of the detected content-type
|
||||
// - an io.Reader for the response body
|
||||
// - an error value which will be non-nil either when something goes wrong while
|
||||
// reading bytes from r or when the detected content-type is not acceptable.
|
||||
func inspectResponse(ct string, r io.ReadCloser, clen int64) (string, io.ReadCloser, error) {
|
||||
plen := clen
|
||||
if plen <= 0 || plen > maxPreambleLength {
|
||||
plen = maxPreambleLength
|
||||
}
|
||||
|
||||
preamble := make([]byte, plen, plen)
|
||||
rlen, err := r.Read(preamble)
|
||||
if rlen == 0 {
|
||||
return ct, r, errors.New("Empty response")
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
return ct, r, err
|
||||
}
|
||||
|
||||
preambleR := bytes.NewReader(preamble)
|
||||
bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r))
|
||||
// Some web servers will use application/octet-stream as the default
|
||||
// content type for files without an extension (e.g. 'Dockerfile')
|
||||
// so if we receive this value we better check for text content
|
||||
contentType := ct
|
||||
if len(ct) == 0 || ct == httputils.MimeTypes.OctetStream {
|
||||
contentType, _, err = httputils.DetectContentType(preamble)
|
||||
if err != nil {
|
||||
return contentType, bodyReader, err
|
||||
}
|
||||
}
|
||||
|
||||
contentType = selectAcceptableMIME(contentType)
|
||||
var cterr error
|
||||
if len(contentType) == 0 {
|
||||
cterr = fmt.Errorf("unsupported Content-Type %q", ct)
|
||||
contentType = ct
|
||||
}
|
||||
|
||||
return contentType, bodyReader, cterr
|
||||
}
|
|
@ -1,17 +1,6 @@
|
|||
package dockerfile
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))`
|
||||
|
||||
var mimeRe = regexp.MustCompile(acceptableRemoteMIME)
|
||||
|
||||
func selectAcceptableMIME(ct string) string {
|
||||
return mimeRe.FindString(ct)
|
||||
}
|
||||
import "strings"
|
||||
|
||||
func handleJSONArgs(args []string, attributes map[string]bool) []string {
|
||||
if len(args) == 0 {
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
package dockerfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSelectAcceptableMIME(t *testing.T) {
|
||||
validMimeStrings := []string{
|
||||
"application/x-bzip2",
|
||||
"application/bzip2",
|
||||
"application/gzip",
|
||||
"application/x-gzip",
|
||||
"application/x-xz",
|
||||
"application/xz",
|
||||
"application/tar",
|
||||
"application/x-tar",
|
||||
"application/octet-stream",
|
||||
"text/plain",
|
||||
}
|
||||
|
||||
invalidMimeStrings := []string{
|
||||
"",
|
||||
"application/octet",
|
||||
"application/json",
|
||||
}
|
||||
|
||||
for _, m := range invalidMimeStrings {
|
||||
if len(selectAcceptableMIME(m)) > 0 {
|
||||
err := fmt.Errorf("Should not have accepted %q", m)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range validMimeStrings {
|
||||
if str := selectAcceptableMIME(m); str == "" {
|
||||
err := fmt.Errorf("Should have accepted %q", m)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
47
builder/dockerignore.go
Normal file
47
builder/dockerignore.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/docker/pkg/fileutils"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
// DockerIgnoreContext wraps a ModifiableContext to add a method
|
||||
// for handling the .dockerignore file at the root of the context.
|
||||
type DockerIgnoreContext struct {
|
||||
ModifiableContext
|
||||
}
|
||||
|
||||
// Process reads the .dockerignore file at the root of the embedded context.
|
||||
// If .dockerignore does not exist in the context, then nil is returned.
|
||||
//
|
||||
// It can take a list of files to be removed after .dockerignore is removed.
|
||||
// This is used for server-side implementations of builders that need to send
|
||||
// the .dockerignore file as well as the special files specified in filesToRemove,
|
||||
// but expect them to be excluded from the context after they were processed.
|
||||
//
|
||||
// For example, server-side Dockerfile builders are expected to pass in the name
|
||||
// of the Dockerfile to be removed after it was parsed.
|
||||
//
|
||||
// TODO: Don't require a ModifiableContext (use Context instead) and don't remove
|
||||
// files, instead handle a list of files to be excluded from the context.
|
||||
func (c DockerIgnoreContext) Process(filesToRemove []string) error {
|
||||
dockerignore, err := c.Open(".dockerignore")
|
||||
// Note that a missing .dockerignore file isn't treated as an error
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
excludes, _ := utils.ReadDockerIgnore(dockerignore)
|
||||
filesToRemove = append([]string{".dockerignore"}, filesToRemove...)
|
||||
for _, fileToRemove := range filesToRemove {
|
||||
rm, _ := fileutils.Matches(fileToRemove, excludes)
|
||||
if rm {
|
||||
c.Remove(fileToRemove)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
28
builder/git.go
Normal file
28
builder/git.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
// MakeGitContext returns a Context from gitURL that is cloned in a temporary directory.
|
||||
func MakeGitContext(gitURL string) (ModifiableContext, error) {
|
||||
root, err := utils.GitClone(gitURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := archive.Tar(root, archive.Uncompressed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// TODO: print errors?
|
||||
c.Close()
|
||||
os.RemoveAll(root)
|
||||
}()
|
||||
return MakeTarSumContext(c)
|
||||
}
|
115
builder/remote.go
Normal file
115
builder/remote.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
)
|
||||
|
||||
// When downloading remote contexts, limit the amount (in bytes)
|
||||
// to be read from the response body in order to detect its Content-Type
|
||||
const maxPreambleLength = 100
|
||||
|
||||
const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))`
|
||||
|
||||
var mimeRe = regexp.MustCompile(acceptableRemoteMIME)
|
||||
|
||||
// MakeRemoteContext downloads a context from remoteURL and returns it.
|
||||
//
|
||||
// If contentTypeHandlers is non-nil, then the Content-Type header is read along with a maximum of
|
||||
// maxPreambleLength bytes from the body to help detecting the MIME type.
|
||||
// Look at acceptableRemoteMIME for more details.
|
||||
//
|
||||
// If a match is found, then the body is sent to the contentType handler and a (potentially compressed) tar stream is expected
|
||||
// to be returned. If no match is found, it is assumed the body is a tar stream (compressed or not).
|
||||
// In either case, an (assumed) tar stream is passed to MakeTarSumContext whose result is returned.
|
||||
func MakeRemoteContext(remoteURL string, contentTypeHandlers map[string]func(io.ReadCloser) (io.ReadCloser, error)) (ModifiableContext, error) {
|
||||
f, err := httputils.Download(remoteURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error downloading remote context %s: %v", remoteURL, err)
|
||||
}
|
||||
defer f.Body.Close()
|
||||
|
||||
var contextReader io.ReadCloser
|
||||
if contentTypeHandlers != nil {
|
||||
contentType := f.Header.Get("Content-Type")
|
||||
clen := f.ContentLength
|
||||
|
||||
contentType, contextReader, err = inspectResponse(contentType, f.Body, clen)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error detecting content type for remote %s: %v", remoteURL, err)
|
||||
}
|
||||
defer contextReader.Close()
|
||||
|
||||
// This loop tries to find a content-type handler for the detected content-type.
|
||||
// If it could not find one from the caller-supplied map, it tries the empty content-type `""`
|
||||
// which is interpreted as a fallback handler (usually used for raw tar contexts).
|
||||
for _, ct := range []string{contentType, ""} {
|
||||
if fn, ok := contentTypeHandlers[ct]; ok {
|
||||
defer contextReader.Close()
|
||||
if contextReader, err = fn(contextReader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass through - this is a pre-packaged context, presumably
|
||||
// with a Dockerfile with the right name inside it.
|
||||
return MakeTarSumContext(contextReader)
|
||||
}
|
||||
|
||||
// inspectResponse looks into the http response data at r to determine whether its
|
||||
// content-type is on the list of acceptable content types for remote build contexts.
|
||||
// This function returns:
|
||||
// - a string representation of the detected content-type
|
||||
// - an io.Reader for the response body
|
||||
// - an error value which will be non-nil either when something goes wrong while
|
||||
// reading bytes from r or when the detected content-type is not acceptable.
|
||||
func inspectResponse(ct string, r io.ReadCloser, clen int64) (string, io.ReadCloser, error) {
|
||||
plen := clen
|
||||
if plen <= 0 || plen > maxPreambleLength {
|
||||
plen = maxPreambleLength
|
||||
}
|
||||
|
||||
preamble := make([]byte, plen, plen)
|
||||
rlen, err := r.Read(preamble)
|
||||
if rlen == 0 {
|
||||
return ct, r, errors.New("Empty response")
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
return ct, r, err
|
||||
}
|
||||
|
||||
preambleR := bytes.NewReader(preamble)
|
||||
bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r))
|
||||
// Some web servers will use application/octet-stream as the default
|
||||
// content type for files without an extension (e.g. 'Dockerfile')
|
||||
// so if we receive this value we better check for text content
|
||||
contentType := ct
|
||||
if len(ct) == 0 || ct == httputils.MimeTypes.OctetStream {
|
||||
contentType, _, err = httputils.DetectContentType(preamble)
|
||||
if err != nil {
|
||||
return contentType, bodyReader, err
|
||||
}
|
||||
}
|
||||
|
||||
contentType = selectAcceptableMIME(contentType)
|
||||
var cterr error
|
||||
if len(contentType) == 0 {
|
||||
cterr = fmt.Errorf("unsupported Content-Type %q", ct)
|
||||
contentType = ct
|
||||
}
|
||||
|
||||
return contentType, bodyReader, cterr
|
||||
}
|
||||
|
||||
func selectAcceptableMIME(ct string) string {
|
||||
return mimeRe.FindString(ct)
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
package dockerfile
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
@ -9,6 +10,41 @@ import (
|
|||
var textPlainDockerfile = "FROM busybox"
|
||||
var binaryContext = []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00} //xz magic
|
||||
|
||||
func TestSelectAcceptableMIME(t *testing.T) {
|
||||
validMimeStrings := []string{
|
||||
"application/x-bzip2",
|
||||
"application/bzip2",
|
||||
"application/gzip",
|
||||
"application/x-gzip",
|
||||
"application/x-xz",
|
||||
"application/xz",
|
||||
"application/tar",
|
||||
"application/x-tar",
|
||||
"application/octet-stream",
|
||||
"text/plain",
|
||||
}
|
||||
|
||||
invalidMimeStrings := []string{
|
||||
"",
|
||||
"application/octet",
|
||||
"application/json",
|
||||
}
|
||||
|
||||
for _, m := range invalidMimeStrings {
|
||||
if len(selectAcceptableMIME(m)) > 0 {
|
||||
err := fmt.Errorf("Should not have accepted %q", m)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range validMimeStrings {
|
||||
if str := selectAcceptableMIME(m); str == "" {
|
||||
err := fmt.Errorf("Should have accepted %q", m)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectEmptyResponse(t *testing.T) {
|
||||
ct := "application/octet-stream"
|
||||
br := ioutil.NopCloser(bytes.NewReader([]byte("")))
|
165
builder/tarsum.go
Normal file
165
builder/tarsum.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/chrootarchive"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/symlink"
|
||||
"github.com/docker/docker/pkg/tarsum"
|
||||
)
|
||||
|
||||
type tarSumContext struct {
|
||||
root string
|
||||
sums tarsum.FileInfoSums
|
||||
}
|
||||
|
||||
func (c *tarSumContext) Close() error {
|
||||
return os.RemoveAll(c.root)
|
||||
}
|
||||
|
||||
func convertPathError(err error, cleanpath string) error {
|
||||
if err, ok := err.(*os.PathError); ok {
|
||||
err.Path = cleanpath
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *tarSumContext) Open(path string) (io.ReadCloser, error) {
|
||||
cleanpath, fullpath, err := c.normalize(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := os.Open(fullpath)
|
||||
if err != nil {
|
||||
return nil, convertPathError(err, cleanpath)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (c *tarSumContext) Stat(path string) (fi FileInfo, err error) {
|
||||
cleanpath, fullpath, err := c.normalize(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st, err := os.Lstat(fullpath)
|
||||
if err != nil {
|
||||
return nil, convertPathError(err, cleanpath)
|
||||
}
|
||||
|
||||
fi = PathFileInfo{st, fullpath}
|
||||
// we set sum to path by default for the case where GetFile returns nil.
|
||||
// The usual case is if cleanpath is empty.
|
||||
sum := path
|
||||
if tsInfo := c.sums.GetFile(cleanpath); tsInfo != nil {
|
||||
sum = tsInfo.Sum()
|
||||
}
|
||||
fi = &HashedFileInfo{fi, sum}
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
// MakeTarSumContext returns a build Context from a tar stream.
|
||||
//
|
||||
// It extracts the tar stream to a temporary folder that is deleted as soon as
|
||||
// the Context is closed.
|
||||
// As the extraction happens, a tarsum is calculated for every file, and the set of
|
||||
// all those sums then becomes the source of truth for all operations on this Context.
|
||||
//
|
||||
// Closing tarStream has to be done by the caller.
|
||||
func MakeTarSumContext(tarStream io.Reader) (ModifiableContext, error) {
|
||||
root, err := ioutils.TempDir("", "docker-builder")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tsc := &tarSumContext{root: root}
|
||||
|
||||
// Make sure we clean-up upon error. In the happy case the caller
|
||||
// is expected to manage the clean-up
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tsc.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
decompressedStream, err := archive.DecompressStream(tarStream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sum, err := tarsum.NewTarSum(decompressedStream, true, tarsum.Version1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := chrootarchive.Untar(sum, root, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tsc.sums = sum.GetSums()
|
||||
|
||||
return tsc, nil
|
||||
}
|
||||
|
||||
func (c *tarSumContext) normalize(path string) (cleanpath, fullpath string, err error) {
|
||||
cleanpath = filepath.Clean(string(os.PathSeparator) + path)[1:]
|
||||
fullpath, err = symlink.FollowSymlinkInScope(filepath.Join(c.root, path), c.root)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullpath)
|
||||
}
|
||||
_, err = os.Stat(fullpath)
|
||||
if err != nil {
|
||||
return "", "", convertPathError(err, path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *tarSumContext) Walk(root string, walkFn WalkFunc) error {
|
||||
for _, tsInfo := range c.sums {
|
||||
path := tsInfo.Name()
|
||||
path, fullpath, err := c.normalize(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Any file in the context that starts with the given path will be
|
||||
// picked up and its hashcode used. However, we'll exclude the
|
||||
// root dir itself. We do this for a coupel of reasons:
|
||||
// 1 - ADD/COPY will not copy the dir itself, just its children
|
||||
// so there's no reason to include it in the hash calc
|
||||
// 2 - the metadata on the dir will change when any child file
|
||||
// changes. This will lead to a miss in the cache check if that
|
||||
// child file is in the .dockerignore list.
|
||||
if rel, err := filepath.Rel(root, path); err != nil {
|
||||
return err
|
||||
} else if rel == "." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := os.Lstat(fullpath)
|
||||
if err != nil {
|
||||
return convertPathError(err, path)
|
||||
}
|
||||
// TODO check context breakout?
|
||||
fi := &HashedFileInfo{PathFileInfo{info, fullpath}, tsInfo.Sum()}
|
||||
if err := walkFn(path, fi, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *tarSumContext) Remove(path string) error {
|
||||
_, fullpath, err := c.normalize(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(fullpath)
|
||||
}
|
|
@ -27,7 +27,7 @@ func (daemon *Daemon) ContainerCreate(name string, config *runconfig.Config, hos
|
|||
|
||||
daemon.adaptContainerSettings(hostConfig, adjustCPUShares)
|
||||
|
||||
container, buildWarnings, err := daemon.Create(config, hostConfig, name)
|
||||
container, err := daemon.Create(config, hostConfig, name)
|
||||
if err != nil {
|
||||
if daemon.Graph().IsNotExist(err, config.Image) {
|
||||
if strings.Contains(config.Image, "@") {
|
||||
|
@ -42,16 +42,13 @@ func (daemon *Daemon) ContainerCreate(name string, config *runconfig.Config, hos
|
|||
return types.ContainerCreateResponse{"", warnings}, err
|
||||
}
|
||||
|
||||
warnings = append(warnings, buildWarnings...)
|
||||
|
||||
return types.ContainerCreateResponse{container.ID, warnings}, nil
|
||||
}
|
||||
|
||||
// Create creates a new container from the given configuration with a given name.
|
||||
func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.HostConfig, name string) (retC *Container, retS []string, retErr error) {
|
||||
func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.HostConfig, name string) (retC *Container, retErr error) {
|
||||
var (
|
||||
container *Container
|
||||
warnings []string
|
||||
img *image.Image
|
||||
imgID string
|
||||
err error
|
||||
|
@ -60,16 +57,16 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos
|
|||
if config.Image != "" {
|
||||
img, err = daemon.repositories.LookupImage(config.Image)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if err = daemon.graph.CheckDepth(img); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
imgID = img.ID
|
||||
}
|
||||
|
||||
if err := daemon.mergeAndVerifyConfig(config, img); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hostConfig == nil {
|
||||
|
@ -78,11 +75,11 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos
|
|||
if hostConfig.SecurityOpt == nil {
|
||||
hostConfig.SecurityOpt, err = daemon.generateSecurityOpt(hostConfig.IpcMode, hostConfig.PidMode)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if container, err = daemon.newContainer(name, config, imgID); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
|
@ -93,13 +90,13 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos
|
|||
}()
|
||||
|
||||
if err := daemon.Register(container); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if err := daemon.createRootfs(container); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if err := daemon.setHostConfig(container, hostConfig); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
|
@ -109,20 +106,20 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos
|
|||
}
|
||||
}()
|
||||
if err := container.Mount(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
defer container.Unmount()
|
||||
|
||||
if err := createContainerPlatformSpecificSettings(container, config, hostConfig, img); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := container.toDiskLocking(); err != nil {
|
||||
logrus.Errorf("Error saving new container to disk: %v", err)
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
container.logEvent("create")
|
||||
return container, warnings, nil
|
||||
return container, nil
|
||||
}
|
||||
|
||||
func (daemon *Daemon) generateSecurityOpt(ipcMode runconfig.IpcMode, pidMode runconfig.PidMode) ([]string, error) {
|
||||
|
|
238
daemon/daemonbuilder/builder.go
Normal file
238
daemon/daemonbuilder/builder.go
Normal file
|
@ -0,0 +1,238 @@
|
|||
package daemonbuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/builder"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/daemon"
|
||||
"github.com/docker/docker/graph"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/chrootarchive"
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/docker/docker/pkg/urlutil"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
)
|
||||
|
||||
// Docker implements builder.Docker for the docker Daemon object.
|
||||
type Docker struct {
|
||||
Daemon *daemon.Daemon
|
||||
OutOld io.Writer
|
||||
AuthConfigs map[string]cliconfig.AuthConfig
|
||||
}
|
||||
|
||||
// ensure Docker implements builder.Docker
|
||||
var _ builder.Docker = Docker{}
|
||||
|
||||
// LookupImage looks up a Docker image referenced by `name`.
|
||||
func (d Docker) LookupImage(name string) (*image.Image, error) {
|
||||
return d.Daemon.Repositories().LookupImage(name)
|
||||
}
|
||||
|
||||
// Pull tells Docker to pull image referenced by `name`.
|
||||
func (d Docker) Pull(name string) (*image.Image, error) {
|
||||
remote, tag := parsers.ParseRepositoryTag(name)
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
|
||||
pullRegistryAuth := &cliconfig.AuthConfig{}
|
||||
if len(d.AuthConfigs) > 0 {
|
||||
// The request came with a full auth config file, we prefer to use that
|
||||
repoInfo, err := d.Daemon.RegistryService.ResolveRepository(remote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolvedConfig := registry.ResolveAuthConfig(
|
||||
&cliconfig.ConfigFile{AuthConfigs: d.AuthConfigs},
|
||||
repoInfo.Index,
|
||||
)
|
||||
pullRegistryAuth = &resolvedConfig
|
||||
}
|
||||
|
||||
imagePullConfig := &graph.ImagePullConfig{
|
||||
AuthConfig: pullRegistryAuth,
|
||||
OutStream: ioutils.NopWriteCloser(d.OutOld),
|
||||
}
|
||||
|
||||
if err := d.Daemon.Repositories().Pull(remote, tag, imagePullConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d.Daemon.Repositories().LookupImage(name)
|
||||
}
|
||||
|
||||
// Container looks up a Docker container referenced by `id`.
|
||||
func (d Docker) Container(id string) (*daemon.Container, error) {
|
||||
return d.Daemon.Get(id)
|
||||
}
|
||||
|
||||
// Create creates a new Docker container and returns potential warnings
|
||||
func (d Docker) Create(cfg *runconfig.Config, hostCfg *runconfig.HostConfig) (*daemon.Container, []string, error) {
|
||||
ccr, err := d.Daemon.ContainerCreate("", cfg, hostCfg, true)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
container, err := d.Daemon.Get(ccr.ID)
|
||||
if err != nil {
|
||||
return nil, ccr.Warnings, err
|
||||
}
|
||||
return container, ccr.Warnings, container.Mount()
|
||||
}
|
||||
|
||||
// Remove removes a container specified by `id`.
|
||||
func (d Docker) Remove(id string, cfg *daemon.ContainerRmConfig) error {
|
||||
return d.Daemon.ContainerRm(id, cfg)
|
||||
}
|
||||
|
||||
// Commit creates a new Docker image from an existing Docker container.
|
||||
func (d Docker) Commit(c *daemon.Container, cfg *daemon.ContainerCommitConfig) (*image.Image, error) {
|
||||
return d.Daemon.Commit(c, cfg)
|
||||
}
|
||||
|
||||
// Retain retains an image avoiding it to be removed or overwritten until a corresponding Release() call.
|
||||
func (d Docker) Retain(sessionID, imgID string) {
|
||||
d.Daemon.Graph().Retain(sessionID, imgID)
|
||||
}
|
||||
|
||||
// Release releases a list of images that were retained for the time of a build.
|
||||
func (d Docker) Release(sessionID string, activeImages []string) {
|
||||
d.Daemon.Graph().Release(sessionID, activeImages...)
|
||||
}
|
||||
|
||||
// Copy copies/extracts a source FileInfo to a destination path inside a container
|
||||
// specified by a container object.
|
||||
// TODO: make sure callers don't unnecessarily convert destPath with filepath.FromSlash (Copy does it already).
|
||||
// Copy should take in abstract paths (with slashes) and the implementation should convert it to OS-specific paths.
|
||||
func (d Docker) Copy(c *daemon.Container, destPath string, src builder.FileInfo, decompress bool) error {
|
||||
srcPath := src.Path()
|
||||
destExists := true
|
||||
|
||||
// Work in daemon-local OS specific file paths
|
||||
destPath = filepath.FromSlash(destPath)
|
||||
|
||||
dest, err := c.GetResourcePath(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve the trailing slash
|
||||
// TODO: why are we appending another path separator if there was already one?
|
||||
if strings.HasSuffix(destPath, string(os.PathSeparator)) || destPath == "." {
|
||||
dest += string(os.PathSeparator)
|
||||
}
|
||||
|
||||
destPath = dest
|
||||
|
||||
destStat, err := os.Stat(destPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logrus.Errorf("Error performing os.Stat on %s. %s", destPath, err)
|
||||
return err
|
||||
}
|
||||
destExists = false
|
||||
}
|
||||
|
||||
if src.IsDir() {
|
||||
// copy as directory
|
||||
if err := chrootarchive.CopyWithTar(srcPath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return fixPermissions(srcPath, destPath, 0, 0, destExists)
|
||||
}
|
||||
if decompress {
|
||||
// Only try to untar if it is a file and that we've been told to decompress (when ADD-ing a remote file)
|
||||
|
||||
// First try to unpack the source as an archive
|
||||
// to support the untar feature we need to clean up the path a little bit
|
||||
// because tar is very forgiving. First we need to strip off the archive's
|
||||
// filename from the path but this is only added if it does not end in slash
|
||||
tarDest := destPath
|
||||
if strings.HasSuffix(tarDest, string(os.PathSeparator)) {
|
||||
tarDest = filepath.Dir(destPath)
|
||||
}
|
||||
|
||||
// try to successfully untar the orig
|
||||
if err := chrootarchive.UntarPath(srcPath, tarDest); err == nil {
|
||||
return nil
|
||||
} else if err != io.EOF {
|
||||
logrus.Debugf("Couldn't untar to %s: %v", tarDest, err)
|
||||
}
|
||||
}
|
||||
|
||||
// only needed for fixPermissions, but might as well put it before CopyFileWithTar
|
||||
if destExists && destStat.IsDir() {
|
||||
destPath = filepath.Join(destPath, filepath.Base(srcPath))
|
||||
}
|
||||
|
||||
if err := system.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := chrootarchive.CopyFileWithTar(srcPath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fixPermissions(srcPath, destPath, 0, 0, destExists)
|
||||
}
|
||||
|
||||
// GetCachedImage returns a reference to a cached image whose parent equals `parent`
|
||||
// and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error.
|
||||
func (d Docker) GetCachedImage(imgID string, cfg *runconfig.Config) (string, error) {
|
||||
cache, err := d.Daemon.ImageGetCached(string(imgID), cfg)
|
||||
if cache == nil || err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cache.ID, nil
|
||||
}
|
||||
|
||||
// Following is specific to builder contexts
|
||||
|
||||
// DetectContextFromRemoteURL returns a context and in certain cases the name of the dockerfile to be used
|
||||
// irrespective of user input.
|
||||
// progressReader is only used if remoteURL is actually a URL (not empty, and not a Git endpoint).
|
||||
func DetectContextFromRemoteURL(r io.ReadCloser, remoteURL string, progressReader *progressreader.Config) (context builder.ModifiableContext, dockerfileName string, err error) {
|
||||
switch {
|
||||
case remoteURL == "":
|
||||
context, err = builder.MakeTarSumContext(r)
|
||||
case urlutil.IsGitURL(remoteURL):
|
||||
context, err = builder.MakeGitContext(remoteURL)
|
||||
case urlutil.IsURL(remoteURL):
|
||||
context, err = builder.MakeRemoteContext(remoteURL, map[string]func(io.ReadCloser) (io.ReadCloser, error){
|
||||
httputils.MimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) {
|
||||
dockerfile, err := ioutil.ReadAll(rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// dockerfileName is set to signal that the remote was interpreted as a single Dockerfile, in which case the caller
|
||||
// should use dockerfileName as the new name for the Dockerfile, irrespective of any other user input.
|
||||
dockerfileName = api.DefaultDockerfileName
|
||||
|
||||
// TODO: return a context without tarsum
|
||||
return archive.Generate(dockerfileName, string(dockerfile))
|
||||
},
|
||||
// fallback handler (tar context)
|
||||
"": func(rc io.ReadCloser) (io.ReadCloser, error) {
|
||||
progressReader.In = rc
|
||||
return progressReader, nil
|
||||
},
|
||||
})
|
||||
default:
|
||||
err = fmt.Errorf("remoteURL (%s) could not be recognized as URL", remoteURL)
|
||||
}
|
||||
return
|
||||
}
|
40
daemon/daemonbuilder/builder_unix.go
Normal file
40
daemon/daemonbuilder/builder_unix.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
// +build freebsd linux
|
||||
|
||||
package daemonbuilder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
|
||||
// If the destination didn't already exist, or the destination isn't a
|
||||
// directory, then we should Lchown the destination. Otherwise, we shouldn't
|
||||
// Lchown the destination.
|
||||
destStat, err := os.Stat(destination)
|
||||
if err != nil {
|
||||
// This should *never* be reached, because the destination must've already
|
||||
// been created while untar-ing the context.
|
||||
return err
|
||||
}
|
||||
doChownDestination := !destExisted || !destStat.IsDir()
|
||||
|
||||
// We Walk on the source rather than on the destination because we don't
|
||||
// want to change permissions on things we haven't created or modified.
|
||||
return filepath.Walk(source, func(fullpath string, info os.FileInfo, err error) error {
|
||||
// Do not alter the walk root iff. it existed before, as it doesn't fall under
|
||||
// the domain of "things we should chown".
|
||||
if !doChownDestination && (source == fullpath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Path is prefixed by source: substitute with destination instead.
|
||||
cleaned, err := filepath.Rel(source, fullpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullpath = filepath.Join(destination, cleaned)
|
||||
return os.Lchown(fullpath, uid, gid)
|
||||
})
|
||||
}
|
8
daemon/daemonbuilder/builder_windows.go
Normal file
8
daemon/daemonbuilder/builder_windows.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
// +build windows
|
||||
|
||||
package daemonbuilder
|
||||
|
||||
func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
|
||||
// chown is not supported on Windows
|
||||
return nil
|
||||
}
|
|
@ -39,7 +39,7 @@ func (s *DockerSuite) TestBuildApiDockerfilePath(c *check.C) {
|
|||
c.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(out), "must be within the build context") {
|
||||
if !strings.Contains(string(out), "Forbidden path outside the build context") {
|
||||
c.Fatalf("Didn't complain about leaving build context: %s", out)
|
||||
}
|
||||
}
|
||||
|
|
10
pkg/ioutils/temp_unix.go
Normal file
10
pkg/ioutils/temp_unix.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
// +build !windows
|
||||
|
||||
package ioutils
|
||||
|
||||
import "io/ioutil"
|
||||
|
||||
// TempDir on Unix systems is equivalent to ioutil.TempDir.
|
||||
func TempDir(dir, prefix string) (string, error) {
|
||||
return ioutil.TempDir(dir, prefix)
|
||||
}
|
18
pkg/ioutils/temp_windows.go
Normal file
18
pkg/ioutils/temp_windows.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
// +build windows
|
||||
|
||||
package ioutils
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/docker/docker/pkg/longpath"
|
||||
)
|
||||
|
||||
// TempDir is the equivalent of ioutil.TempDir, except that the result is in Windows longpath format.
|
||||
func TempDir(dir, prefix string) (string, error) {
|
||||
tempDir, err := ioutil.TempDir(dir, prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return longpath.AddPrefix(tempDir), nil
|
||||
}
|
|
@ -247,17 +247,11 @@ func ValidateContextDirectory(srcPath string, excludes []string) error {
|
|||
// ReadDockerIgnore reads a .dockerignore file and returns the list of file patterns
|
||||
// to ignore. Note this will trim whitespace from each line as well
|
||||
// as use GO's "clean" func to get the shortest/cleanest path for each.
|
||||
func ReadDockerIgnore(path string) ([]string, error) {
|
||||
// Note that a missing .dockerignore file isn't treated as an error
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("Error reading '%s': %v", path, err)
|
||||
}
|
||||
func ReadDockerIgnore(reader io.ReadCloser) ([]string, error) {
|
||||
if reader == nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
var excludes []string
|
||||
|
||||
|
@ -269,8 +263,8 @@ func ReadDockerIgnore(path string) ([]string, error) {
|
|||
pattern = filepath.Clean(pattern)
|
||||
excludes = append(excludes, pattern)
|
||||
}
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("Error reading '%s': %v", path, err)
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("Error reading .dockerignore: %v", err)
|
||||
}
|
||||
return excludes, nil
|
||||
}
|
||||
|
|
|
@ -63,24 +63,27 @@ func TestReadDockerIgnore(t *testing.T) {
|
|||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
diName := filepath.Join(tmpDir, ".dockerignore")
|
||||
|
||||
di, err := ReadDockerIgnore(diName)
|
||||
di, err := ReadDockerIgnore(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected not to have error, got %s", err)
|
||||
t.Fatalf("Expected not to have error, got %v", err)
|
||||
}
|
||||
|
||||
if diLen := len(di); diLen != 0 {
|
||||
t.Fatalf("Expected to have zero dockerignore entry, got %d", diLen)
|
||||
}
|
||||
|
||||
diName := filepath.Join(tmpDir, ".dockerignore")
|
||||
content := fmt.Sprintf("test1\n/test2\n/a/file/here\n\nlastfile")
|
||||
err = ioutil.WriteFile(diName, []byte(content), 0777)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
di, err = ReadDockerIgnore(diName)
|
||||
diFd, err := os.Open(diName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
di, err = ReadDockerIgnore(diFd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue