github.com/discordapp/buildkite-agent@v2.6.6+incompatible/agent/artifact_uploader.go (about) 1 package agent 2 3 import ( 4 "crypto/sha1" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "runtime" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/buildkite/agent/api" 16 "github.com/buildkite/agent/logger" 17 "github.com/buildkite/agent/pool" 18 "github.com/buildkite/agent/retry" 19 zglob "github.com/mattn/go-zglob" 20 ) 21 22 const ( 23 ArtifactPathDelimiter = ";" 24 ) 25 26 type ArtifactUploader struct { 27 // The APIClient that will be used when uploading jobs 28 APIClient *api.Client 29 30 // The ID of the Job 31 JobID string 32 33 // The path of the uploads 34 Paths string 35 36 // Where we'll be uploading artifacts 37 Destination string 38 } 39 40 func (a *ArtifactUploader) Upload() error { 41 // Create artifact structs for all the files we need to upload 42 artifacts, err := a.Collect() 43 if err != nil { 44 return err 45 } 46 47 if len(artifacts) == 0 { 48 logger.Info("No files matched paths: %s", a.Paths) 49 } else { 50 logger.Info("Found %d files that match \"%s\"", len(artifacts), a.Paths) 51 52 err := a.upload(artifacts) 53 if err != nil { 54 return err 55 } 56 } 57 58 return nil 59 } 60 61 func isDir(path string) bool { 62 fi, err := os.Stat(path) 63 if err != nil { 64 return false 65 } 66 return fi.IsDir() 67 } 68 69 func (a *ArtifactUploader) Collect() (artifacts []*api.Artifact, err error) { 70 wd, err := os.Getwd() 71 if err != nil { 72 return nil, err 73 } 74 75 for _, globPath := range strings.Split(a.Paths, ArtifactPathDelimiter) { 76 globPath = strings.TrimSpace(globPath) 77 if globPath == "" { 78 continue 79 } 80 81 logger.Debug("Searching for %s", globPath) 82 83 // Resolve the globs (with * and ** in them) 84 files, err := zglob.Glob(globPath) 85 if err != nil { 86 return nil, err 87 } 88 89 // Process each glob match into an api.Artifact 90 for _, file := range files { 91 absolutePath, err := filepath.Abs(file) 92 if err != nil { 93 return nil, err 94 } 95 96 // Ignore directories, we only want files 97 if isDir(absolutePath) { 98 logger.Debug("Skipping directory %s", file) 99 continue 100 } 101 102 // If a glob is absolute, we need to make it relative to the root so that 103 // it can be combined with the download destination to make a valid path. 104 // This is possibly weird and crazy, this logic dates back to 105 // https://github.com/buildkite/agent/commit/8ae46d975aa60d1ae0e2cc0bff7a43d3bf960935 106 // from 2014, so I'm replicating it here to avoid breaking things 107 if filepath.IsAbs(globPath) { 108 if runtime.GOOS == "windows" { 109 wd = filepath.VolumeName(absolutePath) + "/" 110 } else { 111 wd = "/" 112 } 113 } 114 115 path, err := filepath.Rel(wd, absolutePath) 116 if err != nil { 117 return nil, err 118 } 119 120 // Build an artifact object using the paths we have. 121 artifact, err := a.build(path, absolutePath, globPath) 122 if err != nil { 123 return nil, err 124 } 125 126 artifacts = append(artifacts, artifact) 127 } 128 } 129 130 return artifacts, nil 131 } 132 133 func (a *ArtifactUploader) build(path string, absolutePath string, globPath string) (*api.Artifact, error) { 134 // Temporarily open the file to get it's size 135 file, err := os.Open(absolutePath) 136 if err != nil { 137 return nil, err 138 } 139 defer file.Close() 140 141 // Grab it's file info (which includes it's file size) 142 fileInfo, err := file.Stat() 143 if err != nil { 144 return nil, err 145 } 146 147 // Generate a sha1 checksum for the file 148 hash := sha1.New() 149 io.Copy(hash, file) 150 checksum := fmt.Sprintf("%x", hash.Sum(nil)) 151 152 // Create our new artifact data structure 153 artifact := &api.Artifact{ 154 Path: path, 155 AbsolutePath: absolutePath, 156 GlobPath: globPath, 157 FileSize: fileInfo.Size(), 158 Sha1Sum: checksum, 159 } 160 161 return artifact, nil 162 } 163 164 func (a *ArtifactUploader) upload(artifacts []*api.Artifact) error { 165 var uploader Uploader 166 167 // Determine what uploader to use 168 if a.Destination != "" { 169 if strings.HasPrefix(a.Destination, "s3://") { 170 uploader = new(S3Uploader) 171 } else { 172 return errors.New("Unknown upload destination: " + a.Destination) 173 } 174 } else { 175 uploader = new(FormUploader) 176 } 177 178 // Setup the uploader 179 err := uploader.Setup(a.Destination, a.APIClient.DebugHTTP) 180 if err != nil { 181 return err 182 } 183 184 // Set the URL's of the artifacts based on the uploader 185 for _, artifact := range artifacts { 186 artifact.URL = uploader.URL(artifact) 187 } 188 189 // Create the artifacts on Buildkite 190 batchCreator := ArtifactBatchCreator{ 191 APIClient: a.APIClient, 192 JobID: a.JobID, 193 Artifacts: artifacts, 194 UploadDestination: a.Destination, 195 } 196 artifacts, err = batchCreator.Create() 197 if err != nil { 198 return err 199 } 200 201 // Prepare a concurrency pool to upload the artifacts 202 p := pool.New(pool.MaxConcurrencyLimit) 203 errors := []error{} 204 var errorsMutex sync.Mutex 205 206 // Create a wait group so we can make sure the uploader waits for all 207 // the artifact states to upload before finishing 208 var stateUploaderWaitGroup sync.WaitGroup 209 stateUploaderWaitGroup.Add(1) 210 211 // A map to keep track of artifact states and how many we've uploaded 212 artifactStates := make(map[string]string) 213 artifactStatesUploaded := 0 214 var artifactStatesMutex sync.Mutex 215 216 // Spin up a gourtine that'll uploading artifact statuses every few 217 // seconds in batches 218 go func() { 219 for artifactStatesUploaded < len(artifacts) { 220 statesToUpload := make(map[string]string) 221 222 // Grab all the states we need to upload, and remove 223 // them from the tracking map 224 // 225 // Since we mutate the artifactStates variable in 226 // multiple routines, we need to lock it to make sure 227 // nothing else is changing it at the same time. 228 artifactStatesMutex.Lock() 229 for id, state := range artifactStates { 230 statesToUpload[id] = state 231 delete(artifactStates, id) 232 } 233 artifactStatesMutex.Unlock() 234 235 if len(statesToUpload) > 0 { 236 artifactStatesUploaded += len(statesToUpload) 237 for id, state := range statesToUpload { 238 logger.Debug("Artifact `%s` has state `%s`", id, state) 239 } 240 241 // Update the states of the artifacts in bulk. 242 err = retry.Do(func(s *retry.Stats) error { 243 _, err = a.APIClient.Artifacts.Update(a.JobID, statesToUpload) 244 if err != nil { 245 logger.Warn("%s (%s)", err, s) 246 } 247 248 return err 249 }, &retry.Config{Maximum: 10, Interval: 5 * time.Second}) 250 251 if err != nil { 252 logger.Error("Error uploading artifact states: %s", err) 253 254 // Track the error that was raised. We need to 255 // aquire a lock since we mutate the errors 256 // slice in mutliple routines. 257 errorsMutex.Lock() 258 errors = append(errors, err) 259 errorsMutex.Unlock() 260 } 261 262 logger.Debug("Uploaded %d artfact states (%d/%d)", len(statesToUpload), artifactStatesUploaded, len(artifacts)) 263 } 264 265 // Check again for states to upload in a few seconds 266 time.Sleep(1 * time.Second) 267 } 268 269 stateUploaderWaitGroup.Done() 270 }() 271 272 for _, artifact := range artifacts { 273 // Create new instance of the artifact for the goroutine 274 // See: http://golang.org/doc/effective_go.html#channels 275 artifact := artifact 276 277 p.Spawn(func() { 278 // Show a nice message that we're starting to upload the file 279 logger.Info("Uploading artifact %s %s (%d bytes)", artifact.ID, artifact.Path, artifact.FileSize) 280 281 // Upload the artifact and then set the state depending 282 // on whether or not it passed. We'll retry the upload 283 // a couple of times before giving up. 284 err = retry.Do(func(s *retry.Stats) error { 285 err := uploader.Upload(artifact) 286 if err != nil { 287 logger.Warn("%s (%s)", err, s) 288 } 289 290 return err 291 }, &retry.Config{Maximum: 10, Interval: 5 * time.Second}) 292 293 var state string 294 295 // Did the upload eventually fail? 296 if err != nil { 297 logger.Error("Error uploading artifact \"%s\": %s", artifact.Path, err) 298 299 // Track the error that was raised. We need to 300 // aquire a lock since we mutate the errors 301 // slice in mutliple routines. 302 errorsMutex.Lock() 303 errors = append(errors, err) 304 errorsMutex.Unlock() 305 306 state = "error" 307 } else { 308 state = "finished" 309 } 310 311 // Since we mutate the artifactStates variable in 312 // multiple routines, we need to lock it to make sure 313 // nothing else is changing it at the same time. 314 artifactStatesMutex.Lock() 315 artifactStates[artifact.ID] = state 316 artifactStatesMutex.Unlock() 317 }) 318 } 319 320 // Wait for the pool to finish 321 p.Wait() 322 323 // Wait for the statuses to finish uploading 324 stateUploaderWaitGroup.Wait() 325 326 if len(errors) > 0 { 327 logger.Fatal("There were errors with uploading some of the artifacts") 328 } 329 330 return nil 331 }