2016-09-08 13:11:39 -04:00
package image
import (
"archive/tar"
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"runtime"
2017-01-11 16:54:52 -05:00
"github.com/docker/distribution/reference"
2016-09-08 13:11:39 -04:00
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/builder/dockerignore"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
2016-12-22 16:25:02 -05:00
"github.com/docker/docker/cli/command/image/build"
2016-09-08 13:11:39 -04:00
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/urlutil"
runconfigopts "github.com/docker/docker/runconfig/opts"
2016-12-22 16:25:02 -05:00
units "github.com/docker/go-units"
2016-09-08 13:11:39 -04:00
"github.com/spf13/cobra"
2016-12-23 14:09:12 -05:00
"golang.org/x/net/context"
2016-09-08 13:11:39 -04:00
)
type buildOptions struct {
context string
dockerfileName string
tags opts . ListOpts
2016-09-29 17:59:52 -04:00
labels opts . ListOpts
2016-09-08 13:11:39 -04:00
buildArgs opts . ListOpts
2016-12-23 14:09:12 -05:00
ulimits * opts . UlimitOpt
2016-09-08 13:11:39 -04:00
memory string
memorySwap string
2016-12-28 17:44:07 -05:00
shmSize opts . MemBytes
2016-09-08 13:11:39 -04:00
cpuShares int64
cpuPeriod int64
cpuQuota int64
cpuSetCpus string
cpuSetMems string
cgroupParent string
isolation string
quiet bool
noCache bool
rm bool
forceRm bool
pull bool
2016-09-22 17:38:00 -04:00
cacheFrom [ ] string
2016-08-18 04:35:23 -04:00
compress bool
2016-06-07 15:15:50 -04:00
securityOpt [ ] string
2016-03-06 07:29:23 -05:00
networkMode string
2016-04-21 12:08:37 -04:00
squash bool
2016-09-08 13:11:39 -04:00
}
// NewBuildCommand creates a new `docker build` command
func NewBuildCommand ( dockerCli * command . DockerCli ) * cobra . Command {
ulimits := make ( map [ string ] * units . Ulimit )
options := buildOptions {
tags : opts . NewListOpts ( validateTag ) ,
2016-12-23 14:09:12 -05:00
buildArgs : opts . NewListOpts ( opts . ValidateEnv ) ,
ulimits : opts . NewUlimitOpt ( & ulimits ) ,
labels : opts . NewListOpts ( opts . ValidateEnv ) ,
2016-09-08 13:11:39 -04:00
}
cmd := & cobra . Command {
Use : "build [OPTIONS] PATH | URL | -" ,
Short : "Build an image from a Dockerfile" ,
Args : cli . ExactArgs ( 1 ) ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
options . context = args [ 0 ]
return runBuild ( dockerCli , options )
} ,
}
flags := cmd . Flags ( )
flags . VarP ( & options . tags , "tag" , "t" , "Name and optionally a tag in the 'name:tag' format" )
flags . Var ( & options . buildArgs , "build-arg" , "Set build-time variables" )
flags . Var ( options . ulimits , "ulimit" , "Ulimit options" )
flags . StringVarP ( & options . dockerfileName , "file" , "f" , "" , "Name of the Dockerfile (Default is 'PATH/Dockerfile')" )
flags . StringVarP ( & options . memory , "memory" , "m" , "" , "Memory limit" )
flags . StringVar ( & options . memorySwap , "memory-swap" , "" , "Swap limit equal to memory plus swap: '-1' to enable unlimited swap" )
2016-12-28 17:44:07 -05:00
flags . Var ( & options . shmSize , "shm-size" , "Size of /dev/shm" )
2016-09-08 13:11:39 -04:00
flags . Int64VarP ( & options . cpuShares , "cpu-shares" , "c" , 0 , "CPU shares (relative weight)" )
flags . Int64Var ( & options . cpuPeriod , "cpu-period" , 0 , "Limit the CPU CFS (Completely Fair Scheduler) period" )
flags . Int64Var ( & options . cpuQuota , "cpu-quota" , 0 , "Limit the CPU CFS (Completely Fair Scheduler) quota" )
flags . StringVar ( & options . cpuSetCpus , "cpuset-cpus" , "" , "CPUs in which to allow execution (0-3, 0,1)" )
flags . StringVar ( & options . cpuSetMems , "cpuset-mems" , "" , "MEMs in which to allow execution (0-3, 0,1)" )
flags . StringVar ( & options . cgroupParent , "cgroup-parent" , "" , "Optional parent cgroup for the container" )
flags . StringVar ( & options . isolation , "isolation" , "" , "Container isolation technology" )
2016-09-29 17:59:52 -04:00
flags . Var ( & options . labels , "label" , "Set metadata for an image" )
2016-09-08 13:11:39 -04:00
flags . BoolVar ( & options . noCache , "no-cache" , false , "Do not use cache when building the image" )
flags . BoolVar ( & options . rm , "rm" , true , "Remove intermediate containers after a successful build" )
flags . BoolVar ( & options . forceRm , "force-rm" , false , "Always remove intermediate containers" )
flags . BoolVarP ( & options . quiet , "quiet" , "q" , false , "Suppress the build output and print image ID on success" )
flags . BoolVar ( & options . pull , "pull" , false , "Always attempt to pull a newer version of the image" )
2016-09-22 17:38:00 -04:00
flags . StringSliceVar ( & options . cacheFrom , "cache-from" , [ ] string { } , "Images to consider as cache sources" )
2016-08-18 04:35:23 -04:00
flags . BoolVar ( & options . compress , "compress" , false , "Compress the build context using gzip" )
2016-06-07 15:15:50 -04:00
flags . StringSliceVar ( & options . securityOpt , "security-opt" , [ ] string { } , "Security options" )
2016-12-09 10:15:26 -05:00
flags . StringVar ( & options . networkMode , "network" , "default" , "Set the networking mode for the RUN instructions during build" )
2017-01-16 11:57:26 -05:00
flags . SetAnnotation ( "network" , "version" , [ ] string { "1.25" } )
2016-09-08 13:11:39 -04:00
2017-01-17 09:46:07 -05:00
command . AddTrustVerificationFlags ( flags )
2016-09-08 13:11:39 -04:00
2016-11-02 20:43:32 -04:00
flags . BoolVar ( & options . squash , "squash" , false , "Squash newly built layers into a single new layer" )
flags . SetAnnotation ( "squash" , "experimental" , nil )
2016-11-02 20:43:32 -04:00
flags . SetAnnotation ( "squash" , "version" , [ ] string { "1.25" } )
2016-04-21 12:08:37 -04:00
2016-09-08 13:11:39 -04:00
return cmd
}
// lastProgressOutput is the same as progress.Output except
// that it only output with the last update. It is used in
// non terminal scenarios to depresss verbose messages
type lastProgressOutput struct {
output progress . Output
}
// WriteProgress formats progress information from a ProgressReader.
func ( out * lastProgressOutput ) WriteProgress ( prog progress . Progress ) error {
if ! prog . LastUpdate {
return nil
}
return out . output . WriteProgress ( prog )
}
func runBuild ( dockerCli * command . DockerCli , options buildOptions ) error {
var (
2016-11-07 03:17:04 -05:00
buildCtx io . ReadCloser
err error
2016-09-08 13:11:39 -04:00
contextDir string
tempDir string
relDockerfile string
progBuff io . Writer
buildBuff io . Writer
)
2016-11-02 20:43:32 -04:00
2016-11-07 03:17:04 -05:00
specifiedContext := options . context
2016-09-08 13:11:39 -04:00
progBuff = dockerCli . Out ( )
buildBuff = dockerCli . Out ( )
if options . quiet {
progBuff = bytes . NewBuffer ( nil )
buildBuff = bytes . NewBuffer ( nil )
}
switch {
case specifiedContext == "-" :
2016-12-22 16:25:02 -05:00
buildCtx , relDockerfile , err = build . GetContextFromReader ( dockerCli . In ( ) , options . dockerfileName )
2017-01-24 08:19:31 -05:00
case isLocalDir ( specifiedContext ) :
contextDir , relDockerfile , err = build . GetContextFromLocalDir ( specifiedContext , options . dockerfileName )
2016-09-08 13:11:39 -04:00
case urlutil . IsGitURL ( specifiedContext ) :
2016-12-22 16:25:02 -05:00
tempDir , relDockerfile , err = build . GetContextFromGitURL ( specifiedContext , options . dockerfileName )
2016-09-08 13:11:39 -04:00
case urlutil . IsURL ( specifiedContext ) :
2016-12-22 16:25:02 -05:00
buildCtx , relDockerfile , err = build . GetContextFromURL ( progBuff , specifiedContext , options . dockerfileName )
2016-09-08 13:11:39 -04:00
default :
2017-01-24 08:19:31 -05:00
return fmt . Errorf ( "unable to prepare context: path %q not found" , specifiedContext )
2016-09-08 13:11:39 -04:00
}
if err != nil {
if options . quiet && urlutil . IsURL ( specifiedContext ) {
fmt . Fprintln ( dockerCli . Err ( ) , progBuff )
}
return fmt . Errorf ( "unable to prepare context: %s" , err )
}
if tempDir != "" {
defer os . RemoveAll ( tempDir )
contextDir = tempDir
}
if buildCtx == nil {
// And canonicalize dockerfile name to a platform-independent one
relDockerfile , err = archive . CanonicalTarNameForPath ( relDockerfile )
if err != nil {
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 ) {
return err
}
defer f . Close ( )
var excludes [ ] string
if err == nil {
excludes , err = dockerignore . ReadAll ( f )
if err != nil {
return err
}
}
2016-12-22 16:25:02 -05:00
if err := build . ValidateContextDirectory ( contextDir , excludes ) ; err != nil {
2016-09-08 13:11:39 -04:00
return fmt . Errorf ( "Error checking context: '%s'." , err )
}
// 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 )
}
2016-08-18 04:35:23 -04:00
compression := archive . Uncompressed
if options . compress {
compression = archive . Gzip
}
2016-09-08 13:11:39 -04:00
buildCtx , err = archive . TarWithOptions ( contextDir , & archive . TarOptions {
2016-08-18 04:35:23 -04:00
Compression : compression ,
2016-09-08 13:11:39 -04:00
ExcludePatterns : excludes ,
IncludeFiles : includes ,
} )
if err != nil {
return err
}
}
ctx := context . Background ( )
var resolvedTags [ ] * resolvedTag
if command . IsTrusted ( ) {
2016-08-29 14:45:29 -04:00
translator := func ( ctx context . Context , ref reference . NamedTagged ) ( reference . Canonical , error ) {
2016-12-27 15:51:00 -05:00
return TrustedReference ( ctx , dockerCli , ref , nil )
2016-08-29 14:45:29 -04:00
}
2016-09-08 13:11:39 -04:00
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
// Dockerfile which uses trusted pulls.
2016-08-29 14:45:29 -04:00
buildCtx = replaceDockerfileTarWrapper ( ctx , buildCtx , relDockerfile , translator , & resolvedTags )
2016-09-08 13:11:39 -04:00
}
// Setup an upload progress bar
progressOutput := streamformatter . NewStreamFormatter ( ) . NewProgressOutput ( progBuff , true )
if ! dockerCli . Out ( ) . IsTerminal ( ) {
progressOutput = & lastProgressOutput { output : progressOutput }
}
var body io . Reader = progress . NewProgressReader ( buildCtx , progressOutput , 0 , "" , "Sending build context to Docker daemon" )
var memory int64
if options . memory != "" {
parsedMemory , err := units . RAMInBytes ( options . memory )
if err != nil {
return err
}
memory = parsedMemory
}
var memorySwap int64
if options . memorySwap != "" {
if options . memorySwap == "-1" {
memorySwap = - 1
} else {
parsedMemorySwap , err := units . RAMInBytes ( options . memorySwap )
if err != nil {
return err
}
memorySwap = parsedMemorySwap
}
}
2016-08-18 17:23:10 -04:00
authConfigs , _ := dockerCli . GetAllCredentials ( )
2016-09-08 13:11:39 -04:00
buildOptions := types . ImageBuildOptions {
Memory : memory ,
MemorySwap : memorySwap ,
Tags : options . tags . GetAll ( ) ,
SuppressOutput : options . quiet ,
NoCache : options . noCache ,
Remove : options . rm ,
ForceRemove : options . forceRm ,
PullParent : options . pull ,
Isolation : container . Isolation ( options . isolation ) ,
CPUSetCPUs : options . cpuSetCpus ,
CPUSetMems : options . cpuSetMems ,
CPUShares : options . cpuShares ,
CPUQuota : options . cpuQuota ,
CPUPeriod : options . cpuPeriod ,
CgroupParent : options . cgroupParent ,
Dockerfile : relDockerfile ,
2016-12-28 17:44:07 -05:00
ShmSize : options . shmSize . Value ( ) ,
2016-09-08 13:11:39 -04:00
Ulimits : options . ulimits . GetList ( ) ,
2016-12-03 08:46:04 -05:00
BuildArgs : runconfigopts . ConvertKVStringsToMapWithNil ( options . buildArgs . GetAll ( ) ) ,
2016-08-18 17:23:10 -04:00
AuthConfigs : authConfigs ,
2016-09-29 17:59:52 -04:00
Labels : runconfigopts . ConvertKVStringsToMap ( options . labels . GetAll ( ) ) ,
2016-09-22 17:38:00 -04:00
CacheFrom : options . cacheFrom ,
2016-06-07 15:15:50 -04:00
SecurityOpt : options . securityOpt ,
2016-03-06 07:29:23 -05:00
NetworkMode : options . networkMode ,
2016-04-21 12:08:37 -04:00
Squash : options . squash ,
2016-09-08 13:11:39 -04:00
}
response , err := dockerCli . Client ( ) . ImageBuild ( ctx , body , buildOptions )
if err != nil {
2016-09-13 00:06:04 -04:00
if options . quiet {
fmt . Fprintf ( dockerCli . Err ( ) , "%s" , progBuff )
}
2016-09-08 13:11:39 -04:00
return err
}
defer response . Body . Close ( )
err = jsonmessage . DisplayJSONMessagesStream ( response . Body , buildBuff , dockerCli . Out ( ) . FD ( ) , dockerCli . Out ( ) . IsTerminal ( ) , nil )
if err != nil {
if jerr , ok := err . ( * jsonmessage . JSONError ) ; ok {
// If no error code is set, default to 1
if jerr . Code == 0 {
jerr . Code = 1
}
if options . quiet {
fmt . Fprintf ( dockerCli . Err ( ) , "%s%s" , progBuff , buildBuff )
}
return cli . StatusError { Status : jerr . Message , StatusCode : jerr . Code }
}
}
// Windows: show error message about modified file permissions if the
// daemon isn't running Windows.
if response . OSType != "windows" && runtime . GOOS == "windows" && ! options . quiet {
2017-01-03 14:40:44 -05:00
fmt . Fprintln ( dockerCli . Out ( ) , ` 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. ` )
2016-09-08 13:11:39 -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 options . quiet {
fmt . Fprintf ( dockerCli . Out ( ) , "%s" , buildBuff )
}
if command . IsTrusted ( ) {
// Since the build was successful, now we must tag any of the resolved
// images from the above Dockerfile rewrite.
for _ , resolved := range resolvedTags {
2016-08-29 14:45:29 -04:00
if err := TagTrusted ( ctx , dockerCli , resolved . digestRef , resolved . tagRef ) ; err != nil {
2016-09-08 13:11:39 -04:00
return err
}
}
}
return nil
}
2017-01-24 08:19:31 -05:00
func isLocalDir ( c string ) bool {
_ , err := os . Stat ( c )
return err == nil
}
2016-09-08 13:11:39 -04:00
type translatorFunc func ( context . Context , reference . NamedTagged ) ( reference . Canonical , error )
// validateTag checks if the given image name can be resolved.
func validateTag ( rawRepo string ) ( string , error ) {
2017-01-11 16:54:52 -05:00
_ , err := reference . ParseNormalizedNamed ( rawRepo )
2016-09-08 13:11:39 -04:00
if err != nil {
return "" , err
}
return rawRepo , nil
}
var dockerfileFromLinePattern = regexp . MustCompile ( ` (?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+) ` )
// resolvedTag records the repository, tag, and resolved digest reference
// from a Dockerfile rewrite.
type resolvedTag struct {
digestRef reference . Canonical
tagRef reference . NamedTagged
}
// 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.
func rewriteDockerfileFrom ( ctx context . Context , dockerfile io . Reader , translator translatorFunc ) ( newDockerfile [ ] byte , resolvedTags [ ] * resolvedTag , err error ) {
scanner := bufio . NewScanner ( dockerfile )
buf := bytes . NewBuffer ( nil )
// Scan the lines of the Dockerfile, looking for a "FROM" line.
for scanner . Scan ( ) {
line := scanner . Text ( )
matches := dockerfileFromLinePattern . FindStringSubmatch ( line )
if matches != nil && matches [ 1 ] != api . NoBaseImageSpecifier {
// Replace the line with a resolved "FROM repo@digest"
2017-01-11 16:54:52 -05:00
var ref reference . Named
ref , err = reference . ParseNormalizedNamed ( matches [ 1 ] )
2016-09-08 13:11:39 -04:00
if err != nil {
return nil , nil , err
}
2017-01-11 16:54:52 -05:00
if reference . IsNameOnly ( ref ) {
ref = reference . EnsureTagged ( ref )
}
2016-09-08 13:11:39 -04:00
if ref , ok := ref . ( reference . NamedTagged ) ; ok && command . IsTrusted ( ) {
trustedRef , err := translator ( ctx , ref )
if err != nil {
return nil , nil , err
}
2017-01-11 16:54:52 -05:00
line = dockerfileFromLinePattern . ReplaceAllLiteralString ( line , fmt . Sprintf ( "FROM %s" , reference . FamiliarString ( trustedRef ) ) )
2016-09-08 13:11:39 -04:00
resolvedTags = append ( resolvedTags , & resolvedTag {
digestRef : trustedRef ,
tagRef : ref ,
} )
}
}
_ , err := fmt . Fprintln ( buf , line )
if err != nil {
return nil , nil , err
}
}
return buf . Bytes ( ) , resolvedTags , scanner . Err ( )
}
// 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.
func replaceDockerfileTarWrapper ( ctx context . Context , inputTarStream io . ReadCloser , dockerfileName string , translator translatorFunc , resolvedTags * [ ] * resolvedTag ) io . ReadCloser {
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
}
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.
var newDockerfile [ ] byte
newDockerfile , * resolvedTags , err = rewriteDockerfileFrom ( ctx , content , translator )
if err != nil {
pipeWriter . CloseWithError ( err )
return
}
hdr . Size = int64 ( len ( newDockerfile ) )
content = bytes . NewBuffer ( newDockerfile )
}
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
}