github.com/camronlevanger/libcompose@v0.4.1-0.20180423130544-6bb86d53fa21/docker/builder/builder.go (about) 1 package builder 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path" 8 "path/filepath" 9 "strings" 10 11 "github.com/docker/cli/cli/command/image/build" 12 "github.com/docker/docker/api/types" 13 "github.com/docker/docker/builder/dockerignore" 14 "github.com/docker/docker/client" 15 "github.com/docker/docker/pkg/archive" 16 "github.com/docker/docker/pkg/fileutils" 17 "github.com/docker/docker/pkg/jsonmessage" 18 "github.com/docker/docker/pkg/progress" 19 "github.com/docker/docker/pkg/streamformatter" 20 "github.com/docker/docker/pkg/term" 21 "github.com/docker/libcompose/logger" 22 "github.com/sirupsen/logrus" 23 "golang.org/x/net/context" 24 ) 25 26 // DefaultDockerfileName is the default name of a Dockerfile 27 const DefaultDockerfileName = "Dockerfile" 28 29 // Builder defines methods to provide a docker builder. This makes libcompose 30 // not tied up to the docker daemon builder. 31 type Builder interface { 32 Build(imageName string) error 33 } 34 35 // DaemonBuilder is the daemon "docker build" Builder implementation. 36 type DaemonBuilder struct { 37 Client client.ImageAPIClient 38 ContextDirectory string 39 Dockerfile string 40 AuthConfigs map[string]types.AuthConfig 41 NoCache bool 42 ForceRemove bool 43 Pull bool 44 BuildArgs map[string]*string 45 CacheFrom []string 46 LoggerFactory logger.Factory 47 } 48 49 // Build implements Builder. It consumes the docker build API endpoint and sends 50 // a tar of the specified service build context. 51 func (d *DaemonBuilder) Build(ctx context.Context, imageName string) error { 52 buildCtx, err := CreateTar(d.ContextDirectory, d.Dockerfile) 53 if err != nil { 54 return err 55 } 56 defer buildCtx.Close() 57 if d.LoggerFactory == nil { 58 d.LoggerFactory = &logger.NullLogger{} 59 } 60 61 l := d.LoggerFactory.CreateBuildLogger(imageName) 62 63 progBuff := &logger.Wrapper{ 64 Err: false, 65 Logger: l, 66 } 67 68 buildBuff := &logger.Wrapper{ 69 Err: false, 70 Logger: l, 71 } 72 73 errBuff := &logger.Wrapper{ 74 Err: true, 75 Logger: l, 76 } 77 78 // Setup an upload progress bar 79 progressOutput := streamformatter.NewProgressOutput(progBuff) 80 81 var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") 82 83 logrus.Infof("Building %s...", imageName) 84 85 outFd, isTerminalOut := term.GetFdInfo(os.Stdout) 86 w := l.OutWriter() 87 if w != nil { 88 outFd, isTerminalOut = term.GetFdInfo(w) 89 } 90 91 response, err := d.Client.ImageBuild(ctx, body, types.ImageBuildOptions{ 92 Tags: []string{imageName}, 93 NoCache: d.NoCache, 94 Remove: true, 95 ForceRemove: d.ForceRemove, 96 PullParent: d.Pull, 97 Dockerfile: d.Dockerfile, 98 AuthConfigs: d.AuthConfigs, 99 BuildArgs: d.BuildArgs, 100 CacheFrom: d.CacheFrom, 101 }) 102 if err != nil { 103 return err 104 } 105 106 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, outFd, isTerminalOut, nil) 107 if err != nil { 108 if jerr, ok := err.(*jsonmessage.JSONError); ok { 109 // If no error code is set, default to 1 110 if jerr.Code == 0 { 111 jerr.Code = 1 112 } 113 errBuff.Write([]byte(jerr.Error())) 114 return fmt.Errorf("Status: %s, Code: %d", jerr.Message, jerr.Code) 115 } 116 } 117 return err 118 } 119 120 // CreateTar create a build context tar for the specified project and service name. 121 func CreateTar(contextDirectory, dockerfile string) (io.ReadCloser, error) { 122 // This code was ripped off from docker/api/client/build.go 123 dockerfileName := filepath.Join(contextDirectory, dockerfile) 124 125 absContextDirectory, err := filepath.Abs(contextDirectory) 126 if err != nil { 127 return nil, err 128 } 129 130 filename := dockerfileName 131 132 if dockerfile == "" { 133 // No -f/--file was specified so use the default 134 dockerfileName = DefaultDockerfileName 135 filename = filepath.Join(absContextDirectory, dockerfileName) 136 137 // Just to be nice ;-) look for 'dockerfile' too but only 138 // use it if we found it, otherwise ignore this check 139 if _, err = os.Lstat(filename); os.IsNotExist(err) { 140 tmpFN := path.Join(absContextDirectory, strings.ToLower(dockerfileName)) 141 if _, err = os.Lstat(tmpFN); err == nil { 142 dockerfileName = strings.ToLower(dockerfileName) 143 filename = tmpFN 144 } 145 } 146 } 147 148 origDockerfile := dockerfileName // used for error msg 149 if filename, err = filepath.Abs(filename); err != nil { 150 return nil, err 151 } 152 153 // Now reset the dockerfileName to be relative to the build context 154 dockerfileName, err = filepath.Rel(absContextDirectory, filename) 155 if err != nil { 156 return nil, err 157 } 158 159 // And canonicalize dockerfile name to a platform-independent one 160 dockerfileName, err = archive.CanonicalTarNameForPath(dockerfileName) 161 if err != nil { 162 return nil, fmt.Errorf("Cannot canonicalize dockerfile path %s: %v", dockerfileName, err) 163 } 164 165 if _, err = os.Lstat(filename); os.IsNotExist(err) { 166 return nil, fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile) 167 } 168 var includes = []string{"."} 169 var excludes []string 170 171 dockerIgnorePath := path.Join(contextDirectory, ".dockerignore") 172 dockerIgnore, err := os.Open(dockerIgnorePath) 173 if err != nil { 174 if !os.IsNotExist(err) { 175 return nil, err 176 } 177 logrus.Warnf("Error while reading .dockerignore (%s) : %s", dockerIgnorePath, err.Error()) 178 excludes = make([]string, 0) 179 } else { 180 excludes, err = dockerignore.ReadAll(dockerIgnore) 181 if err != nil { 182 return nil, err 183 } 184 } 185 186 // If .dockerignore mentions .dockerignore or the Dockerfile 187 // then make sure we send both files over to the daemon 188 // because Dockerfile is, obviously, needed no matter what, and 189 // .dockerignore is needed to know if either one needs to be 190 // removed. The deamon will remove them for us, if needed, after it 191 // parses the Dockerfile. 192 keepThem1, _ := fileutils.Matches(".dockerignore", excludes) 193 keepThem2, _ := fileutils.Matches(dockerfileName, excludes) 194 if keepThem1 || keepThem2 { 195 includes = append(includes, ".dockerignore", dockerfileName) 196 } 197 198 if err := build.ValidateContextDirectory(contextDirectory, excludes); err != nil { 199 return nil, fmt.Errorf("error checking context is accessible: '%s', please check permissions and try again", err) 200 } 201 202 options := &archive.TarOptions{ 203 Compression: archive.Uncompressed, 204 ExcludePatterns: excludes, 205 IncludeFiles: includes, 206 } 207 208 return archive.TarWithOptions(contextDirectory, options) 209 }