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 }