2015-03-24 23:57:23 -04:00
package client
import (
2015-07-21 17:47:44 -04:00
"archive/tar"
2015-03-24 23:57:23 -04:00
"bufio"
2015-10-27 20:29:21 -04:00
"bytes"
2015-03-24 23:57:23 -04:00
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
2015-07-21 17:47:44 -04:00
"regexp"
2015-03-24 23:57:23 -04:00
"runtime"
"strings"
"github.com/docker/docker/api"
2015-12-14 05:15:00 -05:00
"github.com/docker/docker/builder/dockerignore"
2015-05-05 00:18:28 -04:00
Cli "github.com/docker/docker/cli"
2015-07-22 22:26:06 -04:00
"github.com/docker/docker/opts"
2015-03-24 23:57:23 -04:00
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
2015-12-14 05:17:51 -05:00
"github.com/docker/docker/pkg/gitutils"
2015-06-04 09:30:14 -04:00
"github.com/docker/docker/pkg/httputils"
2016-01-12 16:18:24 -05:00
"github.com/docker/docker/pkg/ioutils"
2015-03-17 22:18:41 -04:00
"github.com/docker/docker/pkg/jsonmessage"
2015-03-24 23:57:23 -04:00
flag "github.com/docker/docker/pkg/mflag"
2015-11-13 19:59:01 -05:00
"github.com/docker/docker/pkg/progress"
2015-03-17 22:18:41 -04:00
"github.com/docker/docker/pkg/streamformatter"
2015-03-24 23:57:23 -04:00
"github.com/docker/docker/pkg/urlutil"
2015-12-04 16:55:15 -05:00
"github.com/docker/docker/reference"
2015-12-21 15:06:46 -05:00
runconfigopts "github.com/docker/docker/runconfig/opts"
2016-01-04 19:05:26 -05:00
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/container"
2015-12-16 12:26:49 -05:00
"github.com/docker/go-units"
2015-03-24 23:57:23 -04:00
)
2016-01-12 16:18:24 -05:00
type translatorFunc func ( reference . NamedTagged ) ( reference . Canonical , error )
2015-03-25 13:34:41 -04:00
// CmdBuild builds a new image from the source code at a given path.
//
// If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN.
//
// Usage: docker build [OPTIONS] PATH | URL | -
2015-03-24 23:57:23 -04:00
func ( cli * DockerCli ) CmdBuild ( args ... string ) error {
2015-10-08 08:46:21 -04:00
cmd := Cli . Subcmd ( "build" , [ ] string { "PATH | URL | -" } , Cli . DockerCommands [ "build" ] . Description , true )
2015-08-30 09:48:03 -04:00
flTags := opts . NewListOpts ( validateTag )
cmd . Var ( & flTags , [ ] string { "t" , "-tag" } , "Name and optionally a tag in the 'name:tag' format" )
2015-10-27 20:29:21 -04:00
suppressOutput := cmd . Bool ( [ ] string { "q" , "-quiet" } , false , "Suppress the build output and print image ID on success" )
2015-11-09 09:37:24 -05:00
noCache := cmd . Bool ( [ ] string { "-no-cache" } , false , "Do not use cache when building the image" )
rm := cmd . Bool ( [ ] string { "-rm" } , true , "Remove intermediate containers after a successful build" )
2015-03-24 23:57:23 -04:00
forceRm := cmd . Bool ( [ ] string { "-force-rm" } , false , "Always remove intermediate containers" )
pull := cmd . Bool ( [ ] string { "-pull" } , false , "Always attempt to pull a newer version of the image" )
dockerfileName := cmd . String ( [ ] string { "f" , "-file" } , "" , "Name of the Dockerfile (Default is 'PATH/Dockerfile')" )
flMemoryString := cmd . String ( [ ] string { "m" , "-memory" } , "" , "Memory limit" )
2015-12-29 20:23:35 -05:00
flMemorySwap := cmd . String ( [ ] string { "-memory-swap" } , "" , "Swap limit equal to memory plus swap: '-1' to enable unlimited swap" )
2015-09-09 02:30:56 -04:00
flShmSize := cmd . String ( [ ] string { "-shm-size" } , "" , "Size of /dev/shm, default value is 64MB" )
2015-10-16 15:05:18 -04:00
flCPUShares := cmd . Int64 ( [ ] string { "#c" , "-cpu-shares" } , 0 , "CPU shares (relative weight)" )
2015-07-20 20:55:30 -04:00
flCPUPeriod := cmd . Int64 ( [ ] string { "-cpu-period" } , 0 , "Limit the CPU CFS (Completely Fair Scheduler) period" )
flCPUQuota := cmd . Int64 ( [ ] string { "-cpu-quota" } , 0 , "Limit the CPU CFS (Completely Fair Scheduler) quota" )
2015-03-25 22:31:29 -04:00
flCPUSetCpus := cmd . String ( [ ] string { "-cpuset-cpus" } , "" , "CPUs in which to allow execution (0-3, 0,1)" )
2015-04-13 22:00:48 -04:00
flCPUSetMems := cmd . String ( [ ] string { "-cpuset-mems" } , "" , "MEMs in which to allow execution (0-3, 0,1)" )
2015-03-26 19:14:31 -04:00
flCgroupParent := cmd . String ( [ ] string { "-cgroup-parent" } , "" , "Optional parent cgroup for the container" )
2015-12-29 11:28:21 -05:00
flBuildArg := opts . NewListOpts ( runconfigopts . ValidateEnv )
2014-11-14 13:59:14 -05:00
cmd . Var ( & flBuildArg , [ ] string { "-build-arg" } , "Set build-time variables" )
2015-10-29 20:58:00 -04:00
isolation := cmd . String ( [ ] string { "-isolation" } , "" , "Container isolation level" )
2015-07-22 22:26:06 -04:00
2015-12-21 15:06:46 -05:00
ulimits := make ( map [ string ] * units . Ulimit )
flUlimits := runconfigopts . NewUlimitOpt ( & ulimits )
2015-07-22 22:26:06 -04:00
cmd . Var ( flUlimits , [ ] string { "-ulimit" } , "Ulimit options" )
2015-03-24 23:57:23 -04:00
cmd . Require ( flag . Exact , 1 )
2015-07-03 05:26:09 -04:00
2015-07-20 17:53:52 -04:00
// For trusted pull on "FROM <image>" instruction.
addTrustedFlags ( cmd , true )
2015-03-28 21:22:46 -04:00
cmd . ParseFlags ( args , true )
2015-03-24 23:57:23 -04:00
var (
2016-01-12 17:38:55 -05:00
context io . ReadCloser
err error
2015-03-24 23:57:23 -04:00
)
2015-07-20 17:53:52 -04:00
specifiedContext := cmd . Arg ( 0 )
2015-03-24 23:57:23 -04:00
2015-07-20 17:53:52 -04:00
var (
contextDir string
tempDir string
relDockerfile string
2015-10-27 20:29:21 -04:00
progBuff io . Writer
buildBuff io . Writer
2015-07-20 17:53:52 -04:00
)
2015-03-24 23:57:23 -04:00
2015-10-27 20:29:21 -04:00
progBuff = cli . out
buildBuff = cli . out
if * suppressOutput {
progBuff = bytes . NewBuffer ( nil )
buildBuff = bytes . NewBuffer ( nil )
}
2015-07-20 17:53:52 -04:00
switch {
case specifiedContext == "-" :
2016-01-12 16:18:24 -05:00
context , relDockerfile , err = getContextFromReader ( cli . in , * dockerfileName )
2016-01-16 05:59:13 -05:00
case urlutil . IsGitURL ( specifiedContext ) :
2015-07-20 17:53:52 -04:00
tempDir , relDockerfile , err = getContextFromGitURL ( specifiedContext , * dockerfileName )
case urlutil . IsURL ( specifiedContext ) :
2016-01-12 16:18:24 -05:00
context , relDockerfile , err = getContextFromURL ( progBuff , specifiedContext , * dockerfileName )
2015-07-20 17:53:52 -04:00
default :
contextDir , relDockerfile , err = getContextFromLocalDir ( specifiedContext , * dockerfileName )
}
2015-03-24 23:57:23 -04:00
2015-07-20 17:53:52 -04:00
if err != nil {
2015-10-27 20:29:21 -04:00
if * suppressOutput && urlutil . IsURL ( specifiedContext ) {
fmt . Fprintln ( cli . err , progBuff )
}
2015-07-20 17:53:52 -04:00
return fmt . Errorf ( "unable to prepare context: %s" , err )
}
2015-03-24 23:57:23 -04:00
2015-07-20 17:53:52 -04:00
if tempDir != "" {
defer os . RemoveAll ( tempDir )
contextDir = tempDir
}
2015-03-24 23:57:23 -04:00
2016-01-12 16:18:24 -05:00
if context == nil {
// And canonicalize dockerfile name to a platform-independent one
relDockerfile , err = archive . CanonicalTarNameForPath ( relDockerfile )
2015-09-06 13:26:40 -04:00
if err != nil {
2016-01-12 16:18:24 -05:00
return fmt . Errorf ( "cannot canonicalize dockerfile path %s: %v" , relDockerfile , err )
}
f , err := os . Open ( filepath . Join ( contextDir , ".dockerignore" ) )
if err != nil && ! os . IsNotExist ( err ) {
2015-09-06 13:26:40 -04:00
return err
}
2016-01-12 16:18:24 -05:00
var excludes [ ] string
if err == nil {
excludes , err = dockerignore . ReadAll ( f )
if err != nil {
return err
}
}
2015-03-24 23:57:23 -04:00
2016-01-12 16:18:24 -05:00
if err := validateContextDirectory ( contextDir , excludes ) ; err != nil {
return fmt . Errorf ( "Error checking context: '%s'." , err )
}
2015-07-20 17:53:52 -04:00
2016-01-12 16:18:24 -05:00
// If .dockerignore mentions .dockerignore or the Dockerfile
// then make sure we send both files over to the daemon
// because Dockerfile is, obviously, needed no matter what, and
// .dockerignore is needed to know if either one needs to be
// removed. The daemon 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 {
includes = append ( includes , ".dockerignore" , relDockerfile )
}
2015-03-24 23:57:23 -04:00
2016-01-12 16:18:24 -05:00
context , err = archive . TarWithOptions ( contextDir , & archive . TarOptions {
Compression : archive . Uncompressed ,
ExcludePatterns : excludes ,
IncludeFiles : includes ,
} )
2016-01-05 13:32:43 -05:00
if err != nil {
2016-01-12 16:18:24 -05:00
return err
2016-01-05 13:32:43 -05:00
}
2016-01-12 16:18:24 -05:00
}
2016-01-05 13:32:43 -05:00
2016-01-12 16:18:24 -05:00
var resolvedTags [ ] * resolvedTag
if isTrusted ( ) {
2016-01-05 13:32:43 -05:00
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
// Dockerfile which uses trusted pulls.
2016-01-12 16:18:24 -05:00
context = replaceDockerfileTarWrapper ( context , relDockerfile , cli . trustedReference , & resolvedTags )
2016-01-05 13:32:43 -05:00
}
2015-07-21 17:47:44 -04:00
2015-03-24 23:57:23 -04:00
// Setup an upload progress bar
2015-10-27 20:29:21 -04:00
progressOutput := streamformatter . NewStreamFormatter ( ) . NewProgressOutput ( progBuff , true )
2015-11-13 19:59:01 -05:00
var body io . Reader = progress . NewProgressReader ( context , progressOutput , 0 , "" , "Sending build context to Docker daemon" )
2015-03-24 23:57:23 -04:00
var memory int64
if * flMemoryString != "" {
parsedMemory , err := units . RAMInBytes ( * flMemoryString )
if err != nil {
return err
}
memory = parsedMemory
}
var memorySwap int64
if * flMemorySwap != "" {
if * flMemorySwap == "-1" {
memorySwap = - 1
} else {
parsedMemorySwap , err := units . RAMInBytes ( * flMemorySwap )
if err != nil {
return err
}
memorySwap = parsedMemorySwap
}
}
2015-12-29 15:49:17 -05:00
var shmSize int64
if * flShmSize != "" {
shmSize , err = units . RAMInBytes ( * flShmSize )
if err != nil {
return err
}
}
2015-12-04 17:02:06 -05:00
options := types . ImageBuildOptions {
2015-12-03 18:27:01 -05:00
Context : body ,
Memory : memory ,
MemorySwap : memorySwap ,
Tags : flTags . GetAll ( ) ,
SuppressOutput : * suppressOutput ,
NoCache : * noCache ,
Remove : * rm ,
ForceRemove : * forceRm ,
PullParent : * pull ,
2015-12-29 15:49:17 -05:00
IsolationLevel : container . IsolationLevel ( * isolation ) ,
2015-12-03 18:27:01 -05:00
CPUSetCPUs : * flCPUSetCpus ,
CPUSetMems : * flCPUSetMems ,
CPUShares : * flCPUShares ,
CPUQuota : * flCPUQuota ,
CPUPeriod : * flCPUPeriod ,
CgroupParent : * flCgroupParent ,
Dockerfile : relDockerfile ,
2015-12-29 15:49:17 -05:00
ShmSize : shmSize ,
2015-12-03 18:27:01 -05:00
Ulimits : flUlimits . GetList ( ) ,
2015-12-29 15:49:17 -05:00
BuildArgs : runconfigopts . ConvertKVStringsToMap ( flBuildArg . GetAll ( ) ) ,
2015-12-03 18:27:01 -05:00
AuthConfigs : cli . configFile . AuthConfigs ,
2015-07-22 22:26:06 -04:00
}
2015-12-03 18:27:01 -05:00
response , err := cli . client . ImageBuild ( options )
2014-11-14 13:59:14 -05:00
if err != nil {
return err
}
2015-12-21 18:02:44 -05:00
err = jsonmessage . DisplayJSONMessagesStream ( response . Body , buildBuff , cli . outFd , cli . isTerminalOut , nil )
2015-03-24 23:57:23 -04:00
if err != nil {
2015-12-03 18:27:01 -05:00
if jerr , ok := err . ( * jsonmessage . JSONError ) ; ok {
// If no error code is set, default to 1
if jerr . Code == 0 {
jerr . Code = 1
2015-06-04 09:30:14 -04:00
}
2015-10-27 20:29:21 -04:00
if * suppressOutput {
fmt . Fprintf ( cli . err , "%s%s" , progBuff , buildBuff )
}
2015-12-03 18:27:01 -05:00
return Cli . StatusError { Status : jerr . Message , StatusCode : jerr . Code }
2015-06-04 09:30:14 -04:00
}
}
2016-01-06 13:26:37 -05:00
// Windows: show error message about modified file permissions if the
// daemon isn't running Windows.
if response . OSType != "windows" && runtime . GOOS == "windows" {
2015-12-03 18:27:01 -05:00
fmt . Fprintln ( cli . err , ` SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories. ` )
2015-07-24 19:35:11 -04:00
}
2015-10-27 20:29:21 -04:00
// Everything worked so if -q was provided the output from the daemon
// should be just the image ID and we'll print that to stdout.
if * suppressOutput {
fmt . Fprintf ( cli . out , "%s" , buildBuff )
}
2016-01-05 13:32:43 -05:00
if isTrusted ( ) {
// Since the build was successful, now we must tag any of the resolved
// images from the above Dockerfile rewrite.
for _ , resolved := range resolvedTags {
if err := cli . tagTrusted ( resolved . digestRef , resolved . tagRef ) ; err != nil {
return err
}
2015-07-24 19:35:11 -04:00
}
}
return nil
2015-03-24 23:57:23 -04:00
}
2015-07-20 17:53:52 -04:00
2015-12-21 17:00:22 -05:00
// validateContextDirectory checks if all the contents of the directory
// can be read and returns an error if some files can't be read
// symlinks which point to non-existing files don't trigger an error
func validateContextDirectory ( srcPath string , excludes [ ] string ) error {
contextRoot , err := getContextRoot ( srcPath )
if err != nil {
return err
}
return filepath . Walk ( contextRoot , func ( filePath string , f os . FileInfo , err error ) error {
// skip this directory/file if it's not in the path, it won't get added to the context
if relFilePath , err := filepath . Rel ( contextRoot , filePath ) ; err != nil {
return err
} else if skip , err := fileutils . Matches ( relFilePath , excludes ) ; err != nil {
return err
} else if skip {
if f . IsDir ( ) {
return filepath . SkipDir
}
return nil
}
if err != nil {
if os . IsPermission ( err ) {
return fmt . Errorf ( "can't stat '%s'" , filePath )
}
if os . IsNotExist ( err ) {
return nil
}
return err
}
// skip checking if symlinks point to non-existing files, such symlinks can be useful
// also skip named pipes, because they hanging on open
if f . Mode ( ) & ( os . ModeSymlink | os . ModeNamedPipe ) != 0 {
return nil
}
if ! f . IsDir ( ) {
currentFile , err := os . Open ( filePath )
if err != nil && os . IsPermission ( err ) {
return fmt . Errorf ( "no permission to read from '%s'" , filePath )
}
currentFile . Close ( )
}
return nil
} )
}
2015-08-30 09:48:03 -04:00
// validateTag checks if the given image name can be resolved.
func validateTag ( rawRepo string ) ( string , error ) {
2015-12-11 14:00:13 -05:00
_ , err := reference . ParseNamed ( rawRepo )
2015-11-18 17:20:54 -05:00
if err != nil {
2015-08-30 09:48:03 -04:00
return "" , err
}
return rawRepo , nil
}
2015-08-05 22:36:10 -04:00
// isUNC returns true if the path is UNC (one starting \\). It always returns
// false on Linux.
func isUNC ( path string ) bool {
return runtime . GOOS == "windows" && strings . HasPrefix ( path , ` \\ ` )
}
2015-07-20 17:53:52 -04:00
// getDockerfileRelPath uses the given context directory for a `docker build`
// and returns the absolute path to the context directory, the relative path of
// the dockerfile in that context directory, and a non-nil error on success.
func getDockerfileRelPath ( givenContextDir , givenDockerfile string ) ( absContextDir , relDockerfile string , err error ) {
if absContextDir , err = filepath . Abs ( givenContextDir ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to get absolute context directory: %v" , err )
}
2015-07-27 13:21:36 -04:00
// The context dir might be a symbolic link, so follow it to the actual
// target directory.
2015-08-05 22:36:10 -04:00
//
// FIXME. We use isUNC (always false on non-Windows platforms) to workaround
// an issue in golang. On Windows, EvalSymLinks does not work on UNC file
// paths (those starting with \\). This hack means that when using links
// on UNC paths, they will not be followed.
if ! isUNC ( absContextDir ) {
absContextDir , err = filepath . EvalSymlinks ( absContextDir )
if err != nil {
return "" , "" , fmt . Errorf ( "unable to evaluate symlinks in context path: %v" , err )
}
2015-07-27 13:21:36 -04:00
}
stat , err := os . Lstat ( absContextDir )
if err != nil {
return "" , "" , fmt . Errorf ( "unable to stat context directory %q: %v" , absContextDir , err )
}
if ! stat . IsDir ( ) {
return "" , "" , fmt . Errorf ( "context must be a directory: %s" , absContextDir )
}
2015-07-20 17:53:52 -04:00
absDockerfile := givenDockerfile
if absDockerfile == "" {
// No -f/--file was specified so use the default relative to the
// context directory.
absDockerfile = filepath . Join ( absContextDir , api . DefaultDockerfileName )
// Just to be nice ;-) look for 'dockerfile' too but only
// use it if we found it, otherwise ignore this check
if _ , err = os . Lstat ( absDockerfile ) ; os . IsNotExist ( err ) {
altPath := filepath . Join ( absContextDir , strings . ToLower ( api . DefaultDockerfileName ) )
if _ , err = os . Lstat ( altPath ) ; err == nil {
absDockerfile = altPath
}
}
}
// If not already an absolute path, the Dockerfile path should be joined to
// the base directory.
if ! filepath . IsAbs ( absDockerfile ) {
absDockerfile = filepath . Join ( absContextDir , absDockerfile )
}
2015-07-27 19:09:08 -04:00
// Evaluate symlinks in the path to the Dockerfile too.
2015-08-05 22:36:10 -04:00
//
// FIXME. We use isUNC (always false on non-Windows platforms) to workaround
// an issue in golang. On Windows, EvalSymLinks does not work on UNC file
// paths (those starting with \\). This hack means that when using links
// on UNC paths, they will not be followed.
if ! isUNC ( absDockerfile ) {
absDockerfile , err = filepath . EvalSymlinks ( absDockerfile )
if err != nil {
return "" , "" , fmt . Errorf ( "unable to evaluate symlinks in Dockerfile path: %v" , err )
}
2015-07-20 17:53:52 -04:00
}
if _ , err := os . Lstat ( absDockerfile ) ; err != nil {
if os . IsNotExist ( err ) {
2015-07-27 19:09:08 -04:00
return "" , "" , fmt . Errorf ( "Cannot locate Dockerfile: %q" , absDockerfile )
2015-07-20 17:53:52 -04:00
}
return "" , "" , fmt . Errorf ( "unable to stat Dockerfile: %v" , err )
}
if relDockerfile , err = filepath . Rel ( absContextDir , absDockerfile ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to get relative Dockerfile path: %v" , err )
}
2015-07-27 19:09:08 -04:00
if strings . HasPrefix ( relDockerfile , ".." + string ( filepath . Separator ) ) {
return "" , "" , fmt . Errorf ( "The Dockerfile (%s) must be within the build context (%s)" , givenDockerfile , givenContextDir )
}
2015-07-20 17:53:52 -04:00
return absContextDir , relDockerfile , nil
}
// writeToFile copies from the given reader and writes it to a file with the
// given filename.
func writeToFile ( r io . Reader , filename string ) error {
file , err := os . OpenFile ( filename , os . O_CREATE | os . O_WRONLY | os . O_TRUNC , os . FileMode ( 0600 ) )
if err != nil {
return fmt . Errorf ( "unable to create file: %v" , err )
}
defer file . Close ( )
if _ , err := io . Copy ( file , r ) ; err != nil {
return fmt . Errorf ( "unable to write file: %v" , err )
}
return nil
}
// getContextFromReader will read the contents of the given reader as either a
2016-01-12 16:18:24 -05:00
// Dockerfile or tar archive. Returns a tar archive used as a context and a
// path to the Dockerfile inside the tar.
func getContextFromReader ( r io . ReadCloser , dockerfileName string ) ( out io . ReadCloser , relDockerfile string , err error ) {
2015-07-20 17:53:52 -04:00
buf := bufio . NewReader ( r )
2015-11-20 11:49:33 -05:00
magic , err := buf . Peek ( archive . HeaderSize )
2015-07-20 17:53:52 -04:00
if err != nil && err != io . EOF {
2016-01-12 16:18:24 -05:00
return nil , "" , fmt . Errorf ( "failed to peek context header from STDIN: %v" , err )
2015-07-20 17:53:52 -04:00
}
2016-01-12 16:18:24 -05:00
if archive . IsArchive ( magic ) {
return ioutils . NewReadCloserWrapper ( buf , func ( ) error { return r . Close ( ) } ) , dockerfileName , nil
2015-07-20 17:53:52 -04:00
}
2016-01-12 16:18:24 -05:00
// Input should be read as a Dockerfile.
tmpDir , err := ioutil . TempDir ( "" , "docker-build-context-" )
if err != nil {
return nil , "" , fmt . Errorf ( "unbale to create temporary context directory: %v" , err )
}
2015-07-20 17:53:52 -04:00
2016-01-12 16:18:24 -05:00
f , err := os . Create ( filepath . Join ( tmpDir , api . DefaultDockerfileName ) )
if err != nil {
return nil , "" , err
}
_ , err = io . Copy ( f , buf )
if err != nil {
f . Close ( )
return nil , "" , err
}
2015-07-20 17:53:52 -04:00
2016-01-12 16:18:24 -05:00
if err := f . Close ( ) ; err != nil {
return nil , "" , err
}
if err := r . Close ( ) ; err != nil {
return nil , "" , err
2015-07-20 17:53:52 -04:00
}
2016-01-12 16:18:24 -05:00
tar , err := archive . Tar ( tmpDir , archive . Uncompressed )
if err != nil {
return nil , "" , err
2015-07-20 17:53:52 -04:00
}
2016-01-12 16:18:24 -05:00
return ioutils . NewReadCloserWrapper ( tar , func ( ) error {
err := tar . Close ( )
os . RemoveAll ( tmpDir )
return err
} ) , api . DefaultDockerfileName , nil
2015-07-20 17:53:52 -04:00
}
// getContextFromGitURL uses a Git URL as context for a `docker build`. The
// git repo is cloned into a temporary directory used as the context directory.
// Returns the absolute path to the temporary context directory, the relative
// path of the dockerfile in that context directory, and a non-nil error on
// success.
func getContextFromGitURL ( gitURL , dockerfileName string ) ( absContextDir , relDockerfile string , err error ) {
2016-01-16 05:59:13 -05:00
if _ , err := exec . LookPath ( "git" ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to find 'git': %v" , err )
}
2015-12-14 05:17:51 -05:00
if absContextDir , err = gitutils . Clone ( gitURL ) ; err != nil {
2015-07-20 17:53:52 -04:00
return "" , "" , fmt . Errorf ( "unable to 'git clone' to temporary context directory: %v" , err )
}
return getDockerfileRelPath ( absContextDir , dockerfileName )
}
// getContextFromURL uses a remote URL as context for a `docker build`. The
2016-01-12 16:18:24 -05:00
// remote resource is downloaded as either a Dockerfile or a tar archive.
// Returns the tar archive used for the context and a path of the
// dockerfile inside the tar.
func getContextFromURL ( out io . Writer , remoteURL , dockerfileName string ) ( io . ReadCloser , string , error ) {
2015-07-20 17:53:52 -04:00
response , err := httputils . Download ( remoteURL )
if err != nil {
2016-01-12 16:18:24 -05:00
return nil , "" , fmt . Errorf ( "unable to download remote context %s: %v" , remoteURL , err )
2015-07-20 17:53:52 -04:00
}
2015-11-13 19:59:01 -05:00
progressOutput := streamformatter . NewStreamFormatter ( ) . NewProgressOutput ( out , true )
2015-07-20 17:53:52 -04:00
// Pass the response body through a progress reader.
2015-11-13 19:59:01 -05:00
progReader := progress . NewProgressReader ( response . Body , progressOutput , response . ContentLength , "" , fmt . Sprintf ( "Downloading build context from remote url: %s" , remoteURL ) )
2015-07-20 17:53:52 -04:00
2016-01-12 16:18:24 -05:00
return getContextFromReader ( ioutils . NewReadCloserWrapper ( progReader , func ( ) error { return response . Body . Close ( ) } ) , dockerfileName )
2015-07-20 17:53:52 -04:00
}
// getContextFromLocalDir uses the given local directory as context for a
// `docker build`. Returns the absolute path to the local context directory,
// the relative path of the dockerfile in that context directory, and a non-nil
// error on success.
func getContextFromLocalDir ( localDir , dockerfileName string ) ( absContextDir , relDockerfile string , err error ) {
// When using a local context directory, when the Dockerfile is specified
// with the `-f/--file` option then it is considered relative to the
// current directory and not the context directory.
if dockerfileName != "" {
if dockerfileName , err = filepath . Abs ( dockerfileName ) ; err != nil {
return "" , "" , fmt . Errorf ( "unable to get absolute path to Dockerfile: %v" , err )
}
}
return getDockerfileRelPath ( localDir , dockerfileName )
}
2015-07-21 17:47:44 -04:00
var dockerfileFromLinePattern = regexp . MustCompile ( ` (?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+) ` )
2015-07-24 19:35:11 -04:00
// resolvedTag records the repository, tag, and resolved digest reference
// from a Dockerfile rewrite.
type resolvedTag struct {
2015-11-18 17:20:54 -05:00
digestRef reference . Canonical
tagRef reference . NamedTagged
2015-07-24 19:35:11 -04:00
}
2015-07-21 17:47:44 -04:00
// rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in
// "FROM <image>" instructions to a digest reference. `translator` is a
// function that takes a repository name and tag reference and returns a
// trusted digest reference.
2016-01-12 16:18:24 -05:00
func rewriteDockerfileFrom ( dockerfile io . Reader , translator translatorFunc ) ( newDockerfile [ ] byte , resolvedTags [ ] * resolvedTag , err error ) {
2015-07-21 17:47:44 -04:00
scanner := bufio . NewScanner ( dockerfile )
2016-01-12 16:18:24 -05:00
buf := bytes . NewBuffer ( nil )
2015-07-21 17:47:44 -04:00
// Scan the lines of the Dockerfile, looking for a "FROM" line.
for scanner . Scan ( ) {
line := scanner . Text ( )
matches := dockerfileFromLinePattern . FindStringSubmatch ( line )
2015-12-31 08:57:58 -05:00
if matches != nil && matches [ 1 ] != api . NoBaseImageSpecifier {
2015-07-21 17:47:44 -04:00
// Replace the line with a resolved "FROM repo@digest"
2015-11-18 17:20:54 -05:00
ref , err := reference . ParseNamed ( matches [ 1 ] )
if err != nil {
return nil , nil , err
2015-07-21 17:47:44 -04:00
}
2015-12-10 14:01:34 -05:00
ref = reference . WithDefaultTag ( ref )
if ref , ok := ref . ( reference . NamedTagged ) ; ok && isTrusted ( ) {
trustedRef , err := translator ( ref )
2015-07-21 17:47:44 -04:00
if err != nil {
2015-07-24 19:35:11 -04:00
return nil , nil , err
2015-07-21 17:47:44 -04:00
}
2015-11-18 17:20:54 -05:00
line = dockerfileFromLinePattern . ReplaceAllLiteralString ( line , fmt . Sprintf ( "FROM %s" , trustedRef . String ( ) ) )
2015-07-24 19:35:11 -04:00
resolvedTags = append ( resolvedTags , & resolvedTag {
digestRef : trustedRef ,
2015-12-10 14:01:34 -05:00
tagRef : ref ,
2015-07-24 19:35:11 -04:00
} )
2015-07-21 17:47:44 -04:00
}
}
2016-01-12 16:18:24 -05:00
_ , err := fmt . Fprintln ( buf , line )
2015-07-21 17:47:44 -04:00
if err != nil {
2015-07-24 19:35:11 -04:00
return nil , nil , err
2015-07-21 17:47:44 -04:00
}
}
2016-01-12 16:18:24 -05:00
return buf . Bytes ( ) , resolvedTags , scanner . Err ( )
2015-07-21 17:47:44 -04:00
}
// replaceDockerfileTarWrapper wraps the given input tar archive stream and
// replaces the entry with the given Dockerfile name with the contents of the
// new Dockerfile. Returns a new tar archive stream with the replaced
// Dockerfile.
2016-01-12 16:18:24 -05:00
func replaceDockerfileTarWrapper ( inputTarStream io . ReadCloser , dockerfileName string , translator translatorFunc , resolvedTags * [ ] * resolvedTag ) io . ReadCloser {
2015-07-21 17:47:44 -04:00
pipeReader , pipeWriter := io . Pipe ( )
go func ( ) {
tarReader := tar . NewReader ( inputTarStream )
tarWriter := tar . NewWriter ( pipeWriter )
defer inputTarStream . Close ( )
for {
hdr , err := tarReader . Next ( )
if err == io . EOF {
// Signals end of archive.
tarWriter . Close ( )
pipeWriter . Close ( )
return
}
if err != nil {
pipeWriter . CloseWithError ( err )
return
}
var content io . Reader = tarReader
if hdr . Name == dockerfileName {
// This entry is the Dockerfile. Since the tar archive was
// generated from a directory on the local filesystem, the
// Dockerfile will only appear once in the archive.
2016-01-12 16:18:24 -05:00
var newDockerfile [ ] byte
newDockerfile , * resolvedTags , err = rewriteDockerfileFrom ( content , translator )
if err != nil {
pipeWriter . CloseWithError ( err )
return
}
hdr . Size = int64 ( len ( newDockerfile ) )
content = bytes . NewBuffer ( newDockerfile )
2015-07-21 17:47:44 -04:00
}
if err := tarWriter . WriteHeader ( hdr ) ; err != nil {
pipeWriter . CloseWithError ( err )
return
}
if _ , err := io . Copy ( tarWriter , content ) ; err != nil {
pipeWriter . CloseWithError ( err )
return
}
}
} ( )
return pipeReader
}