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  }