github.com/MOXA-ISD/edge-library-libcompose@v0.4.1-0.20200417083957-c90441e63650/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 "golang.org/x/net/context" 23 ) 24 25 // DefaultDockerfileName is the default name of a Dockerfile 26 const DefaultDockerfileName = "Dockerfile" 27 28 // Builder defines methods to provide a docker builder. This makes libcompose 29 // not tied up to the docker daemon builder. 30 type Builder interface { 31 Build(imageName string) error 32 } 33 34 // DaemonBuilder is the daemon "docker build" Builder implementation. 35 type DaemonBuilder struct { 36 Client client.ImageAPIClient 37 ContextDirectory string 38 Dockerfile string 39 AuthConfigs map[string]types.AuthConfig 40 NoCache bool 41 ForceRemove bool 42 Pull bool 43 BuildArgs map[string]*string 44 CacheFrom []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 CacheFrom: d.CacheFrom, 100 }) 101 if err != nil { 102 return err 103 } 104 105 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, outFd, isTerminalOut, nil) 106 if err != nil { 107 if jerr, ok := err.(*jsonmessage.JSONError); ok { 108 // If no error code is set, default to 1 109 if jerr.Code == 0 { 110 jerr.Code = 1 111 } 112 errBuff.Write([]byte(jerr.Error())) 113 return fmt.Errorf("Status: %s, Code: %d", jerr.Message, jerr.Code) 114 } 115 } 116 return err 117 } 118 119 // CreateTar create a build context tar for the specified project and service name. 120 func CreateTar(contextDirectory, dockerfile string) (io.ReadCloser, error) { 121 // This code was ripped off from docker/api/client/build.go 122 dockerfileName := filepath.Join(contextDirectory, dockerfile) 123 124 absContextDirectory, err := filepath.Abs(contextDirectory) 125 if err != nil { 126 return nil, err 127 } 128 129 filename := dockerfileName 130 131 if dockerfile == "" { 132 // No -f/--file was specified so use the default 133 dockerfileName = DefaultDockerfileName 134 filename = filepath.Join(absContextDirectory, dockerfileName) 135 136 // Just to be nice ;-) look for 'dockerfile' too but only 137 // use it if we found it, otherwise ignore this check 138 if _, err = os.Lstat(filename); os.IsNotExist(err) { 139 tmpFN := path.Join(absContextDirectory, strings.ToLower(dockerfileName)) 140 if _, err = os.Lstat(tmpFN); err == nil { 141 dockerfileName = strings.ToLower(dockerfileName) 142 filename = tmpFN 143 } 144 } 145 } 146 147 origDockerfile := dockerfileName // used for error msg 148 if filename, err = filepath.Abs(filename); err != nil { 149 return nil, err 150 } 151 152 // Now reset the dockerfileName to be relative to the build context 153 dockerfileName, err = filepath.Rel(absContextDirectory, filename) 154 if err != nil { 155 return nil, err 156 } 157 158 // And canonicalize dockerfile name to a platform-independent one 159 dockerfileName, err = archive.CanonicalTarNameForPath(dockerfileName) 160 if err != nil { 161 return nil, fmt.Errorf("Cannot canonicalize dockerfile path %s: %v", dockerfileName, err) 162 } 163 164 if _, err = os.Lstat(filename); os.IsNotExist(err) { 165 return nil, fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile) 166 } 167 var includes = []string{"."} 168 var excludes []string 169 170 dockerIgnorePath := path.Join(contextDirectory, ".dockerignore") 171 dockerIgnore, err := os.Open(dockerIgnorePath) 172 if err != nil { 173 if !os.IsNotExist(err) { 174 return nil, err 175 } 176 logrus.Warnf("Error while reading .dockerignore (%s) : %s", dockerIgnorePath, err.Error()) 177 excludes = make([]string, 0) 178 } else { 179 excludes, err = dockerignore.ReadAll(dockerIgnore) 180 if err != nil { 181 return nil, err 182 } 183 } 184 185 // If .dockerignore mentions .dockerignore or the Dockerfile 186 // then make sure we send both files over to the daemon 187 // because Dockerfile is, obviously, needed no matter what, and 188 // .dockerignore is needed to know if either one needs to be 189 // removed. The deamon will remove them for us, if needed, after it 190 // parses the Dockerfile. 191 keepThem1, _ := fileutils.Matches(".dockerignore", excludes) 192 keepThem2, _ := fileutils.Matches(dockerfileName, excludes) 193 if keepThem1 || keepThem2 { 194 includes = append(includes, ".dockerignore", dockerfileName) 195 } 196 197 if err := build.ValidateContextDirectory(contextDirectory, excludes); err != nil { 198 return nil, fmt.Errorf("error checking context is accessible: '%s', please check permissions and try again", err) 199 } 200 201 options := &archive.TarOptions{ 202 Compression: archive.Uncompressed, 203 ExcludePatterns: excludes, 204 IncludeFiles: includes, 205 } 206 207 return archive.TarWithOptions(contextDirectory, options) 208 }