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