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 }