github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/plugin/builtin/s3/get_command.go (about)

     1  package s3
     2  
     3  import (
     4  	"archive/tar"
     5  	"compress/gzip"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"time"
    10  
    11  	"github.com/evergreen-ci/evergreen/archive"
    12  	"github.com/evergreen-ci/evergreen/model"
    13  	"github.com/evergreen-ci/evergreen/plugin"
    14  	"github.com/evergreen-ci/evergreen/thirdparty"
    15  	"github.com/evergreen-ci/evergreen/util"
    16  	"github.com/goamz/goamz/aws"
    17  	"github.com/mitchellh/mapstructure"
    18  	"github.com/mongodb/grip/slogger"
    19  	"github.com/pkg/errors"
    20  )
    21  
    22  var (
    23  	MaxS3GetAttempts = 10
    24  	S3GetSleep       = 2 * time.Second
    25  )
    26  
    27  // A plugin command to fetch a resource from an s3 bucket and download it to
    28  // the local machine.
    29  type S3GetCommand struct {
    30  	// AwsKey and AwsSecret are the user's credentials for
    31  	// authenticating interactions with s3.
    32  	AwsKey    string `mapstructure:"aws_key" plugin:"expand"`
    33  	AwsSecret string `mapstructure:"aws_secret" plugin:"expand"`
    34  
    35  	// RemoteFile is the filepath of the file to get, within its bucket
    36  	RemoteFile string `mapstructure:"remote_file" plugin:"expand"`
    37  
    38  	// Bucket is the s3 bucket holding the desired file
    39  	Bucket string `mapstructure:"bucket" plugin:"expand"`
    40  
    41  	// BuildVariants stores a list of MCI build variants to run the command for.
    42  	// If the list is empty, it runs for all build variants.
    43  	BuildVariants []string `mapstructure:"build_variants" plugin:"expand"`
    44  
    45  	// Only one of these two should be specified. local_file indicates that the
    46  	// s3 resource should be downloaded as-is to the specified file, and
    47  	// extract_to indicates that the remote resource is a .tgz file to be
    48  	// downloaded to the specified directory.
    49  	LocalFile string `mapstructure:"local_file" plugin:"expand"`
    50  	ExtractTo string `mapstructure:"extract_to" plugin:"expand"`
    51  }
    52  
    53  func (self *S3GetCommand) Name() string {
    54  	return S3GetCmd
    55  }
    56  
    57  func (self *S3GetCommand) Plugin() string {
    58  	return S3PluginName
    59  }
    60  
    61  // S3GetCommand-specific implementation of ParseParams.
    62  func (self *S3GetCommand) ParseParams(params map[string]interface{}) error {
    63  	if err := mapstructure.Decode(params, self); err != nil {
    64  		return errors.Wrapf(err, "error decoding %v params", self.Name())
    65  	}
    66  
    67  	// make sure the command params are valid
    68  	if err := self.validateParams(); err != nil {
    69  		return errors.Wrapf(err, "error validating %v params", self.Name())
    70  	}
    71  
    72  	return nil
    73  }
    74  
    75  // Validate that all necessary params are set, and that only one of
    76  // local_file and extract_to is specified.
    77  func (self *S3GetCommand) validateParams() error {
    78  	if self.AwsKey == "" {
    79  		return errors.New("aws_key cannot be blank")
    80  	}
    81  	if self.AwsSecret == "" {
    82  		return errors.New("aws_secret cannot be blank")
    83  	}
    84  	if self.RemoteFile == "" {
    85  		return errors.New("remote_file cannot be blank")
    86  	}
    87  
    88  	// make sure the bucket is valid
    89  	if err := validateS3BucketName(self.Bucket); err != nil {
    90  		return errors.Wrapf(err, "%v is an invalid bucket name", self.Bucket)
    91  	}
    92  
    93  	// make sure local file and extract-to dir aren't both specified
    94  	if self.LocalFile != "" && self.ExtractTo != "" {
    95  		return errors.New("cannot specify both local_file and extract_to directory")
    96  	}
    97  
    98  	// make sure one is specified
    99  	if self.LocalFile == "" && self.ExtractTo == "" {
   100  		return errors.New("must specify either local_file or extract_to")
   101  	}
   102  	return nil
   103  }
   104  
   105  func (self *S3GetCommand) shouldRunForVariant(buildVariantName string) bool {
   106  	//No buildvariant filter, so run always
   107  	if len(self.BuildVariants) == 0 {
   108  		return true
   109  	}
   110  
   111  	//Only run if the buildvariant specified appears in our list.
   112  	return util.SliceContains(self.BuildVariants, buildVariantName)
   113  }
   114  
   115  // Apply the expansions from the relevant task config to all appropriate
   116  // fields of the S3GetCommand.
   117  func (self *S3GetCommand) expandParams(conf *model.TaskConfig) error {
   118  	return plugin.ExpandValues(self, conf.Expansions)
   119  }
   120  
   121  // Implementation of Execute.  Expands the parameters, and then fetches the
   122  // resource from s3.
   123  func (self *S3GetCommand) Execute(pluginLogger plugin.Logger,
   124  	pluginCom plugin.PluginCommunicator, conf *model.TaskConfig,
   125  	stop chan bool) error {
   126  
   127  	// expand necessary params
   128  	if err := self.expandParams(conf); err != nil {
   129  		return err
   130  	}
   131  
   132  	// validate the params
   133  	if err := self.validateParams(); err != nil {
   134  		return errors.Wrap(err, "expanded params are not valid")
   135  	}
   136  
   137  	if !self.shouldRunForVariant(conf.BuildVariant.Name) {
   138  		pluginLogger.LogTask(slogger.INFO, "Skipping S3 get of remote file %v for variant %v",
   139  			self.RemoteFile,
   140  			conf.BuildVariant.Name)
   141  		return nil
   142  	}
   143  
   144  	// if the local file or extract_to is a relative path, join it to the
   145  	// working dir
   146  	if self.LocalFile != "" && !filepath.IsAbs(self.LocalFile) {
   147  		self.LocalFile = filepath.Join(conf.WorkDir, self.LocalFile)
   148  	}
   149  	if self.ExtractTo != "" && !filepath.IsAbs(self.ExtractTo) {
   150  		self.ExtractTo = filepath.Join(conf.WorkDir, self.ExtractTo)
   151  	}
   152  
   153  	errChan := make(chan error)
   154  	go func() {
   155  		errChan <- errors.WithStack(self.GetWithRetry(pluginLogger))
   156  	}()
   157  
   158  	select {
   159  	case err := <-errChan:
   160  		return errors.WithStack(err)
   161  	case <-stop:
   162  		pluginLogger.LogExecution(slogger.INFO, "Received signal to terminate"+
   163  			" execution of S3 Get Command")
   164  		return nil
   165  	}
   166  
   167  }
   168  
   169  // Wrapper around the Get() function to retry it
   170  func (self *S3GetCommand) GetWithRetry(pluginLogger plugin.Logger) error {
   171  	retriableGet := util.RetriableFunc(
   172  		func() error {
   173  			pluginLogger.LogTask(slogger.INFO, "Fetching %v from"+
   174  				" s3 bucket %v", self.RemoteFile, self.Bucket)
   175  			err := errors.WithStack(self.Get())
   176  			if err != nil {
   177  				pluginLogger.LogExecution(slogger.ERROR, "Error getting from"+
   178  					" s3 bucket: %v", err)
   179  				return util.RetriableError{err}
   180  			}
   181  			return nil
   182  		},
   183  	)
   184  
   185  	retryFail, err := util.Retry(retriableGet, MaxS3GetAttempts, S3GetSleep)
   186  	err = errors.WithStack(err)
   187  	if retryFail {
   188  		pluginLogger.LogExecution(slogger.ERROR, "S3 get failed with error: %v", err)
   189  		return err
   190  	}
   191  	return nil
   192  }
   193  
   194  // Fetch the specified resource from s3.
   195  func (self *S3GetCommand) Get() error {
   196  	// get the appropriate session and bucket
   197  	auth := &aws.Auth{
   198  		AccessKey: self.AwsKey,
   199  		SecretKey: self.AwsSecret,
   200  	}
   201  
   202  	session := thirdparty.NewS3Session(auth, aws.USEast)
   203  	bucket := session.Bucket(self.Bucket)
   204  
   205  	// get a reader for the bucket
   206  	reader, err := bucket.GetReader(self.RemoteFile)
   207  	if err != nil {
   208  		return errors.Wrapf(err, "error getting bucket reader for file %v", self.RemoteFile)
   209  	}
   210  	defer reader.Close()
   211  
   212  	// either untar the remote, or just write to a file
   213  	if self.LocalFile != "" {
   214  		var exists bool
   215  		// remove the file, if it exists
   216  		exists, err = util.FileExists(self.LocalFile)
   217  		if err != nil {
   218  			return errors.Wrapf(err, "error checking existence of local file %v",
   219  				self.LocalFile)
   220  		}
   221  		if exists {
   222  			if err := os.RemoveAll(self.LocalFile); err != nil {
   223  				return errors.Wrapf(err, "error clearing local file %v", self.LocalFile)
   224  			}
   225  		}
   226  
   227  		// open the local file
   228  		file, err := os.Create(self.LocalFile)
   229  		if err != nil {
   230  			return errors.Wrapf(err, "error opening local file %v", self.LocalFile)
   231  		}
   232  		defer file.Close()
   233  
   234  		_, err = io.Copy(file, reader)
   235  		return errors.WithStack(err)
   236  	}
   237  
   238  	// wrap the reader in a gzip reader and a tar reader
   239  	gzipReader, err := gzip.NewReader(reader)
   240  	if err != nil {
   241  		return errors.Wrapf(err, "error creating gzip reader for %v", self.RemoteFile)
   242  	}
   243  
   244  	tarReader := tar.NewReader(gzipReader)
   245  	err = archive.Extract(tarReader, self.ExtractTo)
   246  	if err != nil {
   247  		return errors.Wrapf(err, "error extracting %v to %v", self.RemoteFile, self.ExtractTo)
   248  	}
   249  
   250  	return nil
   251  }