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

     1  package s3
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"path/filepath"
     8  	"time"
     9  
    10  	"github.com/evergreen-ci/evergreen/model"
    11  	"github.com/evergreen-ci/evergreen/model/artifact"
    12  	"github.com/evergreen-ci/evergreen/plugin"
    13  	"github.com/evergreen-ci/evergreen/thirdparty"
    14  	"github.com/evergreen-ci/evergreen/util"
    15  	"github.com/goamz/goamz/aws"
    16  	"github.com/mitchellh/mapstructure"
    17  	"github.com/mongodb/grip"
    18  	"github.com/mongodb/grip/slogger"
    19  	"github.com/pkg/errors"
    20  )
    21  
    22  var (
    23  	maxS3PutAttempts = 5
    24  	s3PutSleep       = 5 * time.Second
    25  	s3baseURL        = "https://s3.amazonaws.com/"
    26  )
    27  
    28  var errSkippedFile = errors.New("missing optional file was skipped")
    29  
    30  // A plugin command to put a resource to an s3 bucket and download it to
    31  // the local machine.
    32  type S3PutCommand struct {
    33  	// AwsKey and AwsSecret are the user's credentials for
    34  	// authenticating interactions with s3.
    35  	AwsKey    string `mapstructure:"aws_key" plugin:"expand"`
    36  	AwsSecret string `mapstructure:"aws_secret" plugin:"expand"`
    37  
    38  	// LocalFile is the local filepath to the file the user
    39  	// wishes to store in s3
    40  	LocalFile string `mapstructure:"local_file" plugin:"expand"`
    41  
    42  	// LocalFilesIncludeFilter is an array of expressions that specify what files should be
    43  	// included in this upload.
    44  	LocalFilesIncludeFilter []string `mapstructure:"local_files_include_filter" plugin:"expand"`
    45  
    46  	// RemoteFile is the filepath to store the file to,
    47  	// within an s3 bucket. Is a prefix when multiple files are uploaded via LocalFilesIncludeFilter.
    48  	RemoteFile string `mapstructure:"remote_file" plugin:"expand"`
    49  
    50  	// Bucket is the s3 bucket to use when storing the desired file
    51  	Bucket string `mapstructure:"bucket" plugin:"expand"`
    52  
    53  	// Permission is the ACL to apply to the uploaded file. See:
    54  	//  http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
    55  	// for some examples.
    56  	Permissions string `mapstructure:"permissions"`
    57  
    58  	// ContentType is the MIME type of the uploaded file.
    59  	//  E.g. text/html, application/pdf, image/jpeg, ...
    60  	ContentType string `mapstructure:"content_type" plugin:"expand"`
    61  
    62  	// BuildVariants stores a list of MCI build variants to run the command for.
    63  	// If the list is empty, it runs for all build variants.
    64  	BuildVariants []string `mapstructure:"build_variants"`
    65  
    66  	// DisplayName stores the name of the file that is linked. Is a prefix when
    67  	// to the matched file name when multiple files are uploaded.
    68  	DisplayName string `mapstructure:"display_name" plugin:"expand"`
    69  
    70  	// Visibility determines who can see file links in the UI.
    71  	// Visibility can be set to either
    72  	//  "private", which allows logged-in users to see the file;
    73  	//  "public", which allows anyone to see the file; or
    74  	//  "none", which hides the file from the UI for everybody.
    75  	// If unset, the file will be public.
    76  	Visibility string `mapstructure:"visibility" plugin:"expand"`
    77  
    78  	// Optional, when set to true, causes this command to be skipped over without an error when
    79  	// the path specified in local_file does not exist. Defaults to false, which triggers errors
    80  	// for missing files.
    81  	Optional bool `mapstructure:"optional"`
    82  }
    83  
    84  func (s3pc *S3PutCommand) Name() string {
    85  	return S3PutCmd
    86  }
    87  
    88  func (s3pc *S3PutCommand) Plugin() string {
    89  	return S3PluginName
    90  }
    91  
    92  // S3PutCommand-specific implementation of ParseParams.
    93  func (s3pc *S3PutCommand) ParseParams(params map[string]interface{}) error {
    94  	if err := mapstructure.Decode(params, s3pc); err != nil {
    95  		return errors.Wrapf(err, "error decoding %s params", s3pc.Name())
    96  	}
    97  
    98  	// make sure the command params are valid
    99  	if err := s3pc.validateParams(); err != nil {
   100  		return errors.Wrapf(err, "error validating %s params", s3pc.Name())
   101  	}
   102  
   103  	return nil
   104  }
   105  
   106  // Validate that all necessary params are set and valid.
   107  func (s3pc *S3PutCommand) validateParams() error {
   108  	if s3pc.AwsKey == "" {
   109  		return errors.New("aws_key cannot be blank")
   110  	}
   111  	if s3pc.AwsSecret == "" {
   112  		return errors.New("aws_secret cannot be blank")
   113  	}
   114  	if s3pc.LocalFile == "" && len(s3pc.LocalFilesIncludeFilter) == 0 {
   115  		return errors.New("local_file and local_files_include_filter cannot both be blank")
   116  	}
   117  	if s3pc.LocalFile != "" && len(s3pc.LocalFilesIncludeFilter) != 0 {
   118  		return errors.New("local_file and local_files_include_filter cannot both be specified")
   119  	}
   120  	if s3pc.Optional && len(s3pc.LocalFilesIncludeFilter) != 0 {
   121  		return errors.New("cannot use optional upload with local_files_include_filter")
   122  	}
   123  	if s3pc.RemoteFile == "" {
   124  		return errors.New("remote_file cannot be blank")
   125  	}
   126  	if s3pc.ContentType == "" {
   127  		return errors.New("content_type cannot be blank")
   128  	}
   129  	if !util.SliceContains(artifact.ValidVisibilities, s3pc.Visibility) {
   130  		return errors.Errorf("invalid visibility setting: %v", s3pc.Visibility)
   131  	}
   132  
   133  	// make sure the bucket is valid
   134  	if err := validateS3BucketName(s3pc.Bucket); err != nil {
   135  		return errors.Wrapf(err, "%v is an invalid bucket name", s3pc.Bucket)
   136  	}
   137  
   138  	// make sure the s3 permissions are valid
   139  	if !validS3Permissions(s3pc.Permissions) {
   140  		return errors.Errorf("permissions '%v' are not valid", s3pc.Permissions)
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  // Apply the expansions from the relevant task config to all appropriate
   147  // fields of the S3PutCommand.
   148  func (s3pc *S3PutCommand) expandParams(conf *model.TaskConfig) error {
   149  	return errors.WithStack(plugin.ExpandValues(s3pc, conf.Expansions))
   150  }
   151  
   152  // isMulti returns whether or not this using the multiple file upload
   153  // capability of the Put command.
   154  func (s3pc *S3PutCommand) isMulti() bool {
   155  	return (len(s3pc.LocalFilesIncludeFilter) != 0)
   156  }
   157  
   158  func (s3pc *S3PutCommand) shouldRunForVariant(buildVariantName string) bool {
   159  	//No buildvariant filter, so run always
   160  	if len(s3pc.BuildVariants) == 0 {
   161  		return true
   162  	}
   163  
   164  	//Only run if the buildvariant specified appears in our list.
   165  	return util.SliceContains(s3pc.BuildVariants, buildVariantName)
   166  }
   167  
   168  // Implementation of Execute.  Expands the parameters, and then puts the
   169  // resource to s3.
   170  func (s3pc *S3PutCommand) Execute(log plugin.Logger,
   171  	com plugin.PluginCommunicator, conf *model.TaskConfig,
   172  	stop chan bool) error {
   173  
   174  	// expand necessary params
   175  	if err := s3pc.expandParams(conf); err != nil {
   176  		return errors.WithStack(err)
   177  	}
   178  
   179  	// validate the params
   180  	if err := s3pc.validateParams(); err != nil {
   181  		return errors.Wrap(err, "expanded params are not valid")
   182  	}
   183  
   184  	if !s3pc.shouldRunForVariant(conf.BuildVariant.Name) {
   185  		log.LogTask(slogger.INFO, "Skipping S3 put of local file %v for variant %v",
   186  			s3pc.LocalFile,
   187  			conf.BuildVariant.Name)
   188  		return nil
   189  	}
   190  
   191  	if s3pc.isMulti() {
   192  		log.LogTask(slogger.INFO, "Putting files matching filter %v into path %v in s3 bucket %v",
   193  			s3pc.LocalFilesIncludeFilter, s3pc.RemoteFile, s3pc.Bucket)
   194  	} else {
   195  		if !filepath.IsAbs(s3pc.LocalFile) {
   196  			s3pc.LocalFile = filepath.Join(conf.WorkDir, s3pc.LocalFile)
   197  		}
   198  		log.LogTask(slogger.INFO, "Putting %v into path %v in s3 bucket %v",
   199  			s3pc.LocalFile, s3pc.RemoteFile, s3pc.Bucket)
   200  	}
   201  
   202  	errChan := make(chan error)
   203  	go func() {
   204  		errChan <- errors.WithStack(s3pc.PutWithRetry(log, com))
   205  	}()
   206  
   207  	select {
   208  	case err := <-errChan:
   209  		return err
   210  	case <-stop:
   211  		log.LogExecution(slogger.INFO, "Received signal to terminate execution of S3 Put Command")
   212  		return nil
   213  	}
   214  
   215  }
   216  
   217  // Wrapper around the Put() function to retry it.
   218  func (s3pc *S3PutCommand) PutWithRetry(log plugin.Logger, com plugin.PluginCommunicator) error {
   219  	retriablePut := util.RetriableFunc(
   220  		func() error {
   221  			filesList, err := s3pc.Put()
   222  			if err != nil {
   223  				if err == errSkippedFile {
   224  					return errors.WithStack(err)
   225  				}
   226  				log.LogExecution(slogger.ERROR, "Error putting to s3 bucket: %v", err)
   227  				return util.RetriableError{err}
   228  			}
   229  
   230  			catcher := grip.NewCatcher()
   231  
   232  			for _, file := range filesList {
   233  				catcher.Add(errors.Wrapf(s3pc.AttachTaskFiles(log, com, file, s3pc.RemoteFile),
   234  					"problem attaching file: %s to %s", file, s3pc.RemoteFile))
   235  			}
   236  
   237  			return catcher.Resolve()
   238  		},
   239  	)
   240  
   241  	retryFail, err := util.Retry(retriablePut, maxS3PutAttempts, s3PutSleep)
   242  	if err == errSkippedFile {
   243  		log.LogExecution(slogger.INFO, "S3 put skipped optional missing file.")
   244  		return nil
   245  	}
   246  	if retryFail {
   247  		log.LogExecution(slogger.ERROR, "S3 put failed with error: %v", err)
   248  		return errors.WithStack(err)
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  // Put the specified resource to s3.
   255  func (s3pc *S3PutCommand) Put() ([]string, error) {
   256  	var err error
   257  
   258  	filesList := []string{s3pc.LocalFile}
   259  
   260  	if s3pc.isMulti() {
   261  		filesList, err = util.BuildFileList(".", s3pc.LocalFilesIncludeFilter...)
   262  		if err != nil {
   263  			return nil, errors.WithStack(err)
   264  		}
   265  	}
   266  	for _, fpath := range filesList {
   267  		remoteName := s3pc.RemoteFile
   268  		if s3pc.isMulti() {
   269  			fname := filepath.Base(fpath)
   270  			remoteName = fmt.Sprintf("%s%s", s3pc.RemoteFile, fname)
   271  		}
   272  
   273  		auth := &aws.Auth{
   274  			AccessKey: s3pc.AwsKey,
   275  			SecretKey: s3pc.AwsSecret,
   276  		}
   277  		s3URL := url.URL{
   278  			Scheme: "s3",
   279  			Host:   s3pc.Bucket,
   280  			Path:   remoteName,
   281  		}
   282  		err := thirdparty.PutS3File(auth, fpath, s3URL.String(), s3pc.ContentType, s3pc.Permissions)
   283  		if err != nil {
   284  			if !s3pc.isMulti() {
   285  				if s3pc.Optional && os.IsNotExist(err) {
   286  					// important to *not* wrap this error.
   287  					return nil, errSkippedFile
   288  				}
   289  			}
   290  			return nil, errors.WithStack(err)
   291  		}
   292  	}
   293  	return filesList, nil
   294  }
   295  
   296  // AttachTaskFiles is responsible for sending the
   297  // specified file to the API Server. Does not support multiple file putting.
   298  func (s3pc *S3PutCommand) AttachTaskFiles(log plugin.Logger,
   299  	com plugin.PluginCommunicator, localFile, remoteFile string) error {
   300  
   301  	remoteFileName := filepath.ToSlash(remoteFile)
   302  	if s3pc.isMulti() {
   303  		remoteFileName = fmt.Sprintf("%s%s", remoteFile, filepath.Base(localFile))
   304  	}
   305  
   306  	fileLink := s3baseURL + s3pc.Bucket + "/" + remoteFileName
   307  
   308  	displayName := s3pc.DisplayName
   309  	if s3pc.isMulti() || displayName == "" {
   310  		displayName = fmt.Sprintf("%s %s", s3pc.DisplayName, filepath.Base(localFile))
   311  	}
   312  
   313  	file := &artifact.File{
   314  		Name:       displayName,
   315  		Link:       fileLink,
   316  		Visibility: s3pc.Visibility,
   317  	}
   318  
   319  	err := com.PostTaskFiles([]*artifact.File{file})
   320  	if err != nil {
   321  		return errors.Wrap(err, "Attach files failed")
   322  	}
   323  	log.LogExecution(slogger.INFO, "API attach files call succeeded")
   324  	return nil
   325  }