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  }