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