github.com/StackPointCloud/packer@v0.10.2-0.20180716202532-b28098e0f79b/builder/amazon/common/state.go (about) 1 package common 2 3 import ( 4 "log" 5 "os" 6 "strconv" 7 "time" 8 9 "github.com/aws/aws-sdk-go/aws" 10 "github.com/aws/aws-sdk-go/aws/request" 11 "github.com/aws/aws-sdk-go/service/ec2" 12 "github.com/hashicorp/packer/helper/multistep" 13 ) 14 15 // StateRefreshFunc is a function type used for StateChangeConf that is 16 // responsible for refreshing the item being watched for a state change. 17 // 18 // It returns three results. `result` is any object that will be returned 19 // as the final object after waiting for state change. This allows you to 20 // return the final updated object, for example an EC2 instance after refreshing 21 // it. 22 // 23 // `state` is the latest state of that object. And `err` is any error that 24 // may have happened while refreshing the state. 25 type StateRefreshFunc func() (result interface{}, state string, err error) 26 27 // StateChangeConf is the configuration struct used for `WaitForState`. 28 type StateChangeConf struct { 29 Pending []string 30 Refresh StateRefreshFunc 31 StepState multistep.StateBag 32 Target string 33 } 34 35 // Following are wrapper functions that use Packer's environment-variables to 36 // determing retry logic, then call the AWS SDK's built-in waiters. 37 38 func WaitUntilAMIAvailable(ctx aws.Context, conn *ec2.EC2, imageId string) error { 39 imageInput := ec2.DescribeImagesInput{ 40 ImageIds: []*string{&imageId}, 41 } 42 43 err := conn.WaitUntilImageAvailableWithContext( 44 ctx, 45 &imageInput, 46 getWaiterOptions()...) 47 return err 48 } 49 50 func WaitUntilInstanceTerminated(ctx aws.Context, conn *ec2.EC2, instanceId string) error { 51 52 instanceInput := ec2.DescribeInstancesInput{ 53 InstanceIds: []*string{&instanceId}, 54 } 55 56 err := conn.WaitUntilInstanceTerminatedWithContext( 57 ctx, 58 &instanceInput, 59 getWaiterOptions()...) 60 return err 61 } 62 63 // This function works for both requesting and cancelling spot instances. 64 func WaitUntilSpotRequestFulfilled(ctx aws.Context, conn *ec2.EC2, spotRequestId string) error { 65 spotRequestInput := ec2.DescribeSpotInstanceRequestsInput{ 66 SpotInstanceRequestIds: []*string{&spotRequestId}, 67 } 68 69 err := conn.WaitUntilSpotInstanceRequestFulfilledWithContext( 70 ctx, 71 &spotRequestInput, 72 getWaiterOptions()...) 73 return err 74 } 75 76 func WaitUntilVolumeAvailable(ctx aws.Context, conn *ec2.EC2, volumeId string) error { 77 volumeInput := ec2.DescribeVolumesInput{ 78 VolumeIds: []*string{&volumeId}, 79 } 80 81 err := conn.WaitUntilVolumeAvailableWithContext( 82 ctx, 83 &volumeInput, 84 getWaiterOptions()...) 85 return err 86 } 87 88 func WaitUntilSnapshotDone(ctx aws.Context, conn *ec2.EC2, snapshotID string) error { 89 snapInput := ec2.DescribeSnapshotsInput{ 90 SnapshotIds: []*string{&snapshotID}, 91 } 92 93 err := conn.WaitUntilSnapshotCompletedWithContext( 94 ctx, 95 &snapInput, 96 getWaiterOptions()...) 97 return err 98 } 99 100 // Wrappers for our custom AWS waiters 101 102 func WaitUntilVolumeAttached(ctx aws.Context, conn *ec2.EC2, volumeId string) error { 103 volumeInput := ec2.DescribeVolumesInput{ 104 VolumeIds: []*string{&volumeId}, 105 } 106 107 err := WaitForVolumeToBeAttached(conn, 108 ctx, 109 &volumeInput, 110 getWaiterOptions()...) 111 return err 112 } 113 114 func WaitUntilVolumeDetached(ctx aws.Context, conn *ec2.EC2, volumeId string) error { 115 volumeInput := ec2.DescribeVolumesInput{ 116 VolumeIds: []*string{&volumeId}, 117 } 118 119 err := WaitForVolumeToBeDetached(conn, 120 ctx, 121 &volumeInput, 122 getWaiterOptions()...) 123 return err 124 } 125 126 func WaitUntilImageImported(ctx aws.Context, conn *ec2.EC2, taskID string) error { 127 importInput := ec2.DescribeImportImageTasksInput{ 128 ImportTaskIds: []*string{&taskID}, 129 } 130 131 err := WaitForImageToBeImported(conn, 132 ctx, 133 &importInput, 134 getWaiterOptions()...) 135 return err 136 } 137 138 // Custom waiters using AWS's request.Waiter 139 140 func WaitForVolumeToBeAttached(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeVolumesInput, opts ...request.WaiterOption) error { 141 w := request.Waiter{ 142 Name: "DescribeVolumes", 143 MaxAttempts: 40, 144 Delay: request.ConstantWaiterDelay(5 * time.Second), 145 Acceptors: []request.WaiterAcceptor{ 146 { 147 State: request.SuccessWaiterState, 148 Matcher: request.PathAllWaiterMatch, 149 Argument: "Volumes[].Attachments[].State", 150 Expected: "attached", 151 }, 152 }, 153 Logger: c.Config.Logger, 154 NewRequest: func(opts []request.Option) (*request.Request, error) { 155 var inCpy *ec2.DescribeVolumesInput 156 if input != nil { 157 tmp := *input 158 inCpy = &tmp 159 } 160 req, _ := c.DescribeVolumesRequest(inCpy) 161 req.SetContext(ctx) 162 req.ApplyOptions(opts...) 163 return req, nil 164 }, 165 } 166 return w.WaitWithContext(ctx) 167 } 168 169 func WaitForVolumeToBeDetached(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeVolumesInput, opts ...request.WaiterOption) error { 170 w := request.Waiter{ 171 Name: "DescribeVolumes", 172 MaxAttempts: 40, 173 Delay: request.ConstantWaiterDelay(5 * time.Second), 174 Acceptors: []request.WaiterAcceptor{ 175 { 176 State: request.SuccessWaiterState, 177 Matcher: request.PathAllWaiterMatch, 178 Argument: "length(Volumes[].Attachments[]) == `0`", 179 Expected: true, 180 }, 181 }, 182 Logger: c.Config.Logger, 183 NewRequest: func(opts []request.Option) (*request.Request, error) { 184 var inCpy *ec2.DescribeVolumesInput 185 if input != nil { 186 tmp := *input 187 inCpy = &tmp 188 } 189 req, _ := c.DescribeVolumesRequest(inCpy) 190 req.SetContext(ctx) 191 req.ApplyOptions(opts...) 192 return req, nil 193 }, 194 } 195 return w.WaitWithContext(ctx) 196 } 197 198 func WaitForImageToBeImported(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeImportImageTasksInput, opts ...request.WaiterOption) error { 199 w := request.Waiter{ 200 Name: "DescribeImages", 201 MaxAttempts: 300, 202 Delay: request.ConstantWaiterDelay(5 * time.Second), 203 Acceptors: []request.WaiterAcceptor{ 204 { 205 State: request.SuccessWaiterState, 206 Matcher: request.PathAllWaiterMatch, 207 Argument: "ImportImageTasks[].Status", 208 Expected: "completed", 209 }, 210 }, 211 Logger: c.Config.Logger, 212 NewRequest: func(opts []request.Option) (*request.Request, error) { 213 var inCpy *ec2.DescribeImportImageTasksInput 214 if input != nil { 215 tmp := *input 216 inCpy = &tmp 217 } 218 req, _ := c.DescribeImportImageTasksRequest(inCpy) 219 req.SetContext(ctx) 220 req.ApplyOptions(opts...) 221 return req, nil 222 }, 223 } 224 return w.WaitWithContext(ctx) 225 } 226 227 // This helper function uses the environment variables AWS_TIMEOUT_SECONDS and 228 // AWS_POLL_DELAY_SECONDS to generate waiter options that can be passed into any 229 // request.Waiter function. These options will control how many times the waiter 230 // will retry the request, as well as how long to wait between the retries. 231 232 // DEFAULTING BEHAVIOR: 233 // if AWS_POLL_DELAY_SECONDS is set but the others are not, Packer will set this 234 // poll delay and use the waiter-specific default 235 236 // if AWS_TIMEOUT_SECONDS is set but AWS_MAX_ATTEMPTS is not, Packer will use 237 // AWS_TIMEOUT_SECONDS and _either_ AWS_POLL_DELAY_SECONDS _or_ 2 if the user has not set AWS_POLL_DELAY_SECONDS, to determine a max number of attempts to make. 238 239 // if AWS_TIMEOUT_SECONDS, _and_ AWS_MAX_ATTEMPTS are both set, 240 // AWS_TIMEOUT_SECONDS will be ignored. 241 242 // if AWS_MAX_ATTEMPTS is set but AWS_POLL_DELAY_SECONDS is not, then we will 243 // use waiter-specific defaults. 244 245 type envInfo struct { 246 envKey string 247 Val int 248 overridden bool 249 } 250 251 type overridableWaitVars struct { 252 awsPollDelaySeconds envInfo 253 awsMaxAttempts envInfo 254 awsTimeoutSeconds envInfo 255 } 256 257 func getWaiterOptions() []request.WaiterOption { 258 envOverrides := getEnvOverrides() 259 waitOpts := applyEnvOverrides(envOverrides) 260 return waitOpts 261 } 262 263 func getOverride(varInfo envInfo) envInfo { 264 override := os.Getenv(varInfo.envKey) 265 if override != "" { 266 n, err := strconv.Atoi(override) 267 if err != nil { 268 log.Printf("Invalid %s '%s', using default", varInfo.envKey, override) 269 } else { 270 varInfo.overridden = true 271 varInfo.Val = n 272 } 273 } 274 275 return varInfo 276 } 277 func getEnvOverrides() overridableWaitVars { 278 // Load env vars from environment, and use them to override defaults 279 envValues := overridableWaitVars{ 280 envInfo{"AWS_POLL_DELAY_SECONDS", 2, false}, 281 envInfo{"AWS_MAX_ATTEMPTS", 0, false}, 282 envInfo{"AWS_TIMEOUT_SECONDS", 300, false}, 283 } 284 285 envValues.awsMaxAttempts = getOverride(envValues.awsMaxAttempts) 286 envValues.awsPollDelaySeconds = getOverride(envValues.awsPollDelaySeconds) 287 envValues.awsTimeoutSeconds = getOverride(envValues.awsTimeoutSeconds) 288 289 return envValues 290 } 291 292 func applyEnvOverrides(envOverrides overridableWaitVars) []request.WaiterOption { 293 waitOpts := make([]request.WaiterOption, 0) 294 // If user has set poll delay seconds, overwrite it. If user has NOT, 295 // default to a poll delay of 2 seconds 296 if envOverrides.awsPollDelaySeconds.overridden { 297 delaySeconds := request.ConstantWaiterDelay(time.Duration(envOverrides.awsPollDelaySeconds.Val) * time.Second) 298 waitOpts = append(waitOpts, request.WithWaiterDelay(delaySeconds)) 299 } 300 301 // If user has set max attempts, overwrite it. If user hasn't set max 302 // attempts, default to whatever the waiter has set as a default. 303 if envOverrides.awsMaxAttempts.overridden { 304 waitOpts = append(waitOpts, request.WithWaiterMaxAttempts(envOverrides.awsMaxAttempts.Val)) 305 } 306 307 if envOverrides.awsMaxAttempts.overridden && envOverrides.awsTimeoutSeconds.overridden { 308 log.Printf("WARNING: AWS_MAX_ATTEMPTS and AWS_TIMEOUT_SECONDS are" + 309 " both set. Packer will be using AWS_MAX_ATTEMPTS and discarding " + 310 "AWS_TIMEOUT_SECONDS. If you have not set AWS_POLL_DELAY_SECONDS, " + 311 "Packer will default to a 2 second poll delay.") 312 } else if envOverrides.awsTimeoutSeconds.overridden { 313 log.Printf("DEPRECATION WARNING: env var AWS_TIMEOUT_SECONDS is " + 314 "deprecated in favor of AWS_MAX_ATTEMPTS. If you have not " + 315 "explicitly set AWS_POLL_DELAY_SECONDS, we are defaulting to a " + 316 "poll delay of 2 seconds, regardless of the AWS waiter's default.") 317 maxAttempts := envOverrides.awsTimeoutSeconds.Val / envOverrides.awsPollDelaySeconds.Val 318 // override the delay so we can get the timeout right 319 if !envOverrides.awsPollDelaySeconds.overridden { 320 delaySeconds := request.ConstantWaiterDelay(time.Duration(envOverrides.awsPollDelaySeconds.Val) * time.Second) 321 waitOpts = append(waitOpts, request.WithWaiterDelay(delaySeconds)) 322 } 323 waitOpts = append(waitOpts, request.WithWaiterMaxAttempts(maxAttempts)) 324 } 325 if len(waitOpts) == 0 { 326 log.Printf("No AWS timeout and polling overrides have been set. " + 327 "Packer will defalt to waiter-specific delays and timeouts. If you would " + 328 "like to customize the length of time between retries and max " + 329 "number of retries you may do so by setting the environment " + 330 "variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS to your " + 331 "desired values.") 332 } 333 334 return waitOpts 335 }