github.com/mweagle/Sparta@v1.15.0/aws/cloudformation/util.go (about)

     1  package cloudformation
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha1"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"math/rand"
    12  	"os"
    13  	"regexp"
    14  	"sort"
    15  	"strconv"
    16  	"strings"
    17  	"text/template"
    18  	"time"
    19  
    20  	"github.com/aws/aws-sdk-go/aws"
    21  	"github.com/aws/aws-sdk-go/aws/session"
    22  	"github.com/aws/aws-sdk-go/service/cloudformation"
    23  	"github.com/aws/aws-sdk-go/service/s3/s3manager"
    24  	"github.com/briandowns/spinner"
    25  	humanize "github.com/dustin/go-humanize"
    26  	gocf "github.com/mweagle/go-cloudformation"
    27  	"github.com/pkg/errors"
    28  	"github.com/sirupsen/logrus"
    29  )
    30  
    31  //var cacheLock sync.Mutex
    32  
    33  func init() {
    34  	rand.Seed(time.Now().Unix())
    35  }
    36  
    37  // RE to ensure CloudFormation compatible resource names
    38  // Issue: https://github.com/mweagle/Sparta/issues/8
    39  // Ref: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html
    40  var reCloudFormationInvalidChars = regexp.MustCompile("[^A-Za-z0-9]+")
    41  
    42  // maximum amount of time allowed for polling CloudFormation
    43  var cloudformationPollingTimeout = 3 * time.Minute
    44  
    45  ////////////////////////////////////////////////////////////////////////////////
    46  // Private
    47  ////////////////////////////////////////////////////////////////////////////////
    48  
    49  // If the platform specific implementation of user.Current()
    50  // isn't available, go get something that's a "stable" user
    51  // name
    52  func defaultUserName() string {
    53  	userName := os.Getenv("USER")
    54  	if userName == "" {
    55  		userName = os.Getenv("USERNAME")
    56  	}
    57  	if userName == "" {
    58  		userName = fmt.Sprintf("user%d", os.Getuid())
    59  	}
    60  	return userName
    61  }
    62  
    63  type resourceProvisionMetrics struct {
    64  	resourceType      string
    65  	logicalResourceID string
    66  	startTime         time.Time
    67  	endTime           time.Time
    68  	elapsed           time.Duration
    69  }
    70  
    71  // BEGIN - templateConverter
    72  // Struct to encapsulate transforming data into
    73  type templateConverter struct {
    74  	templateReader          io.Reader
    75  	additionalTemplateProps map[string]interface{}
    76  	// internals
    77  	doQuote          bool
    78  	expandedTemplate string
    79  	contents         []gocf.Stringable
    80  	conversionError  error
    81  }
    82  
    83  func (converter *templateConverter) expandTemplate() *templateConverter {
    84  	if nil != converter.conversionError {
    85  		return converter
    86  	}
    87  	templateDataBytes, templateDataErr := ioutil.ReadAll(converter.templateReader)
    88  	if nil != templateDataErr {
    89  		converter.conversionError = templateDataErr
    90  		return converter
    91  	}
    92  	templateData := string(templateDataBytes)
    93  
    94  	parsedTemplate, templateErr := template.New("CloudFormation").Parse(templateData)
    95  	if nil != templateErr {
    96  		converter.conversionError = templateDataErr
    97  		return converter
    98  	}
    99  	output := &bytes.Buffer{}
   100  	executeErr := parsedTemplate.Execute(output, converter.additionalTemplateProps)
   101  	if nil != executeErr {
   102  		converter.conversionError = executeErr
   103  		return converter
   104  	}
   105  	converter.expandedTemplate = output.String()
   106  	return converter
   107  }
   108  
   109  func (converter *templateConverter) parseData() *templateConverter {
   110  	if converter.conversionError != nil {
   111  		return converter
   112  	}
   113  	// TODO - parse this better... 🤔
   114  	// First see if it's JSON...if so, just walk it
   115  	reAWSProp := regexp.MustCompile("\\{\\s*\"\\s*(Ref|Fn::GetAtt|Fn::FindInMap)")
   116  	splitData := strings.Split(converter.expandedTemplate, "\n")
   117  	splitDataLineCount := len(splitData)
   118  
   119  	for eachLineIndex, eachLine := range splitData {
   120  		curContents := eachLine
   121  		for len(curContents) != 0 {
   122  
   123  			matchInfo := reAWSProp.FindStringSubmatchIndex(curContents)
   124  			if nil != matchInfo {
   125  				// If there's anything at the head, push it.
   126  				if matchInfo[0] != 0 {
   127  					head := curContents[0:matchInfo[0]]
   128  					converter.contents = append(converter.contents, gocf.String(head))
   129  					curContents = curContents[len(head):]
   130  				}
   131  
   132  				// There's at least one match...find the closing brace...
   133  				var parsed map[string]interface{}
   134  				for indexPos, eachChar := range curContents {
   135  					if string(eachChar) == "}" {
   136  						testBlock := curContents[0 : indexPos+1]
   137  						err := json.Unmarshal([]byte(testBlock), &parsed)
   138  						if err == nil {
   139  							parsedContents, parsedContentsErr := parseFnJoinExpr(parsed)
   140  							if nil != parsedContentsErr {
   141  								converter.conversionError = parsedContentsErr
   142  								return converter
   143  							}
   144  							if converter.doQuote {
   145  								converter.contents = append(converter.contents, gocf.Join("",
   146  									gocf.String("\""),
   147  									parsedContents,
   148  									gocf.String("\"")))
   149  							} else {
   150  								converter.contents = append(converter.contents, parsedContents)
   151  							}
   152  							curContents = curContents[indexPos+1:]
   153  							if len(curContents) <= 0 && (eachLineIndex < (splitDataLineCount - 1)) {
   154  								converter.contents = append(converter.contents, gocf.String("\n"))
   155  							}
   156  							break
   157  						}
   158  					}
   159  				}
   160  				if nil == parsed {
   161  					// We never did find the end...
   162  					converter.conversionError = fmt.Errorf("invalid CloudFormation JSON expression on line: %s", eachLine)
   163  					return converter
   164  				}
   165  			} else {
   166  				// No match, just include it iff there is another line afterwards
   167  				newlineValue := ""
   168  				if eachLineIndex < (splitDataLineCount - 1) {
   169  					newlineValue = "\n"
   170  				}
   171  				// Always include a newline at a minimum
   172  				appendLine := fmt.Sprintf("%s%s", curContents, newlineValue)
   173  				if len(appendLine) != 0 {
   174  					converter.contents = append(converter.contents, gocf.String(appendLine))
   175  				}
   176  				break
   177  			}
   178  		}
   179  	}
   180  	return converter
   181  }
   182  
   183  func (converter *templateConverter) results() (*gocf.StringExpr, error) {
   184  	if nil != converter.conversionError {
   185  		return nil, converter.conversionError
   186  	}
   187  	return gocf.Join("", converter.contents...), nil
   188  }
   189  
   190  // END - templateConverter
   191  
   192  func cloudformationPollingDelay() time.Duration {
   193  	return time.Duration(3+rand.Int31n(5)) * time.Second
   194  }
   195  
   196  func updateStackViaChangeSet(serviceName string,
   197  	cfTemplate *gocf.Template,
   198  	cfTemplateURL string,
   199  	awsTags []*cloudformation.Tag,
   200  	awsCloudFormation *cloudformation.CloudFormation,
   201  	logger *logrus.Logger) error {
   202  
   203  	// Create a change set name...
   204  	changeSetRequestName := CloudFormationResourceName(fmt.Sprintf("%sChangeSet", serviceName))
   205  	_, changesErr := CreateStackChangeSet(changeSetRequestName,
   206  		serviceName,
   207  		cfTemplate,
   208  		cfTemplateURL,
   209  		awsTags,
   210  		awsCloudFormation,
   211  		logger)
   212  	if nil != changesErr {
   213  		return changesErr
   214  	}
   215  
   216  	//////////////////////////////////////////////////////////////////////////////
   217  	// Apply the change
   218  	executeChangeSetInput := cloudformation.ExecuteChangeSetInput{
   219  		ChangeSetName: aws.String(changeSetRequestName),
   220  		StackName:     aws.String(serviceName),
   221  	}
   222  	executeChangeSetOutput, executeChangeSetError := awsCloudFormation.ExecuteChangeSet(&executeChangeSetInput)
   223  
   224  	logger.WithFields(logrus.Fields{
   225  		"ExecuteChangeSetOutput": executeChangeSetOutput,
   226  	}).Debug("ExecuteChangeSet result")
   227  
   228  	if nil == executeChangeSetError {
   229  		logger.WithFields(logrus.Fields{
   230  			"StackName": serviceName,
   231  		}).Info("Issued ExecuteChangeSet request")
   232  	}
   233  	return executeChangeSetError
   234  
   235  }
   236  
   237  // func existingLambdaResourceVersions(serviceName string,
   238  // 	lambdaResourceName string,
   239  // 	session *session.Session,
   240  // 	logger *logrus.Logger) (*lambda.ListVersionsByFunctionOutput, error) {
   241  
   242  // 	errorIsNotExist := func(apiError error) bool {
   243  // 		return apiError != nil && strings.Contains(apiError.Error(), "does not exist")
   244  // 	}
   245  
   246  // 	logger.WithFields(logrus.Fields{
   247  // 		"ResourceName": lambdaResourceName,
   248  // 	}).Info("Fetching existing function versions")
   249  
   250  // 	cloudFormationSvc := cloudformation.New(session)
   251  // 	describeParams := &cloudformation.DescribeStackResourceInput{
   252  // 		StackName:         aws.String(serviceName),
   253  // 		LogicalResourceId: aws.String(lambdaResourceName),
   254  // 	}
   255  // 	describeResponse, describeResponseErr := cloudFormationSvc.DescribeStackResource(describeParams)
   256  // 	logger.WithFields(logrus.Fields{
   257  // 		"Response":    describeResponse,
   258  // 		"ResponseErr": describeResponseErr,
   259  // 	}).Debug("Describe response")
   260  // 	if errorIsNotExist(describeResponseErr) {
   261  // 		return nil, nil
   262  // 	} else if describeResponseErr != nil {
   263  // 		return nil, describeResponseErr
   264  // 	}
   265  
   266  // 	listVersionsParams := &lambda.ListVersionsByFunctionInput{
   267  // 		FunctionName: describeResponse.StackResourceDetail.PhysicalResourceId,
   268  // 		MaxItems:     aws.Int64(128),
   269  // 	}
   270  // 	lambdaSvc := lambda.New(session)
   271  // 	listVersionsResp, listVersionsRespErr := lambdaSvc.ListVersionsByFunction(listVersionsParams)
   272  // 	if errorIsNotExist(listVersionsRespErr) {
   273  // 		return nil, nil
   274  // 	} else if listVersionsRespErr != nil {
   275  // 		return nil, listVersionsRespErr
   276  // 	}
   277  // 	logger.WithFields(logrus.Fields{
   278  // 		"Response":    listVersionsResp,
   279  // 		"ResponseErr": listVersionsRespErr,
   280  // 	}).Debug("ListVersionsByFunction")
   281  // 	return listVersionsResp, nil
   282  // }
   283  
   284  func toExpressionSlice(input interface{}) ([]string, error) {
   285  	var expressions []string
   286  	slice, sliceOK := input.([]interface{})
   287  	if !sliceOK {
   288  		return nil, fmt.Errorf("failed to convert to slice")
   289  	}
   290  	for _, eachValue := range slice {
   291  		switch str := eachValue.(type) {
   292  		case string:
   293  			expressions = append(expressions, str)
   294  		}
   295  	}
   296  	return expressions, nil
   297  }
   298  func parseFnJoinExpr(data map[string]interface{}) (*gocf.StringExpr, error) {
   299  	if len(data) <= 0 {
   300  		return nil, fmt.Errorf("data for FnJoinExpr is empty")
   301  	}
   302  	for eachKey, eachValue := range data {
   303  		switch eachKey {
   304  		case "Ref":
   305  			return gocf.Ref(eachValue.(string)).String(), nil
   306  		case "Fn::GetAtt":
   307  			attrValues, attrValuesErr := toExpressionSlice(eachValue)
   308  			if nil != attrValuesErr {
   309  				return nil, attrValuesErr
   310  			}
   311  			if len(attrValues) != 2 {
   312  				return nil, fmt.Errorf("invalid params for Fn::GetAtt: %s", eachValue)
   313  			}
   314  			return gocf.GetAtt(attrValues[0], attrValues[1]).String(), nil
   315  		case "Fn::FindInMap":
   316  			attrValues, attrValuesErr := toExpressionSlice(eachValue)
   317  			if nil != attrValuesErr {
   318  				return nil, attrValuesErr
   319  			}
   320  			if len(attrValues) != 3 {
   321  				return nil, fmt.Errorf("invalid params for Fn::FindInMap: %s", eachValue)
   322  			}
   323  			return gocf.FindInMap(attrValues[0], gocf.String(attrValues[1]), gocf.String(attrValues[2])), nil
   324  		}
   325  	}
   326  	return nil, fmt.Errorf("unsupported AWS Function detected: %#v", data)
   327  }
   328  
   329  func stackCapabilities(template *gocf.Template) []*string {
   330  	capabilitiesMap := make(map[string]bool)
   331  
   332  	// Only require IAM capability if the definition requires it.
   333  	for _, eachResource := range template.Resources {
   334  		if eachResource.Properties.CfnResourceType() == "AWS::IAM::Role" {
   335  			capabilitiesMap["CAPABILITY_IAM"] = true
   336  			switch typedResource := eachResource.Properties.(type) {
   337  			case gocf.IAMRole:
   338  				capabilitiesMap["CAPABILITY_NAMED_IAM"] = (typedResource.RoleName != nil)
   339  			case *gocf.IAMRole:
   340  				capabilitiesMap["CAPABILITY_NAMED_IAM"] = (typedResource.RoleName != nil)
   341  			}
   342  		}
   343  	}
   344  	capabilities := make([]*string, len(capabilitiesMap))
   345  	capabilitiesIndex := 0
   346  	for eachKey := range capabilitiesMap {
   347  		capabilities[capabilitiesIndex] = aws.String(eachKey)
   348  		capabilitiesIndex++
   349  	}
   350  	return capabilities
   351  }
   352  
   353  ////////////////////////////////////////////////////////////////////////////////
   354  // Public
   355  ////////////////////////////////////////////////////////////////////////////////
   356  
   357  // DynamicValueToStringExpr is a DRY function to type assert
   358  // a potentiall dynamic value into a gocf.Stringable
   359  // satisfying type
   360  func DynamicValueToStringExpr(dynamicValue interface{}) gocf.Stringable {
   361  	var stringExpr gocf.Stringable
   362  	switch typedValue := dynamicValue.(type) {
   363  	case string:
   364  		stringExpr = gocf.String(typedValue)
   365  	case *gocf.StringExpr:
   366  		stringExpr = typedValue
   367  	case gocf.Stringable:
   368  		stringExpr = typedValue.String()
   369  	default:
   370  		panic(fmt.Sprintf("Unsupported dynamic value type: %+v", typedValue))
   371  	}
   372  	return stringExpr
   373  }
   374  
   375  // S3AllKeysArnForBucket returns a CloudFormation-compatible Arn expression
   376  // (string or Ref) for all bucket keys (`/*`).  The bucket
   377  // parameter may be either a string or an interface{} ("Ref: "myResource")
   378  // value
   379  func S3AllKeysArnForBucket(bucket interface{}) *gocf.StringExpr {
   380  	arnParts := []gocf.Stringable{
   381  		gocf.String("arn:aws:s3:::"),
   382  		DynamicValueToStringExpr(bucket),
   383  		gocf.String("/*"),
   384  	}
   385  	return gocf.Join("", arnParts...).String()
   386  }
   387  
   388  // S3ArnForBucket returns a CloudFormation-compatible Arn expression
   389  // (string or Ref) suitable for template reference.  The bucket
   390  // parameter may be either a string or an interface{} ("Ref: "myResource")
   391  // value
   392  func S3ArnForBucket(bucket interface{}) *gocf.StringExpr {
   393  	arnParts := []gocf.Stringable{
   394  		gocf.String("arn:aws:s3:::"),
   395  		DynamicValueToStringExpr(bucket),
   396  	}
   397  	return gocf.Join("", arnParts...).String()
   398  }
   399  
   400  // MapToResourceTags transforms a go map[string]string to a CloudFormation-compliant
   401  // Tags representation.  See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html
   402  func MapToResourceTags(tagMap map[string]string) []interface{} {
   403  	tags := make([]interface{}, len(tagMap))
   404  	tagsIndex := 0
   405  	for eachKey, eachValue := range tagMap {
   406  		tags[tagsIndex] = map[string]interface{}{
   407  			"Key":   eachKey,
   408  			"Value": eachValue,
   409  		}
   410  		tagsIndex++
   411  	}
   412  	return tags
   413  }
   414  
   415  // ConvertToTemplateExpression transforms the templateData contents into
   416  // an Fn::Join- compatible representation for template serialization.
   417  // The templateData contents may include both golang text/template properties
   418  // and single-line JSON Fn::Join supported serializations.
   419  func ConvertToTemplateExpression(templateData io.Reader,
   420  	additionalUserTemplateProperties map[string]interface{}) (*gocf.StringExpr, error) {
   421  	converter := &templateConverter{
   422  		templateReader:          templateData,
   423  		additionalTemplateProps: additionalUserTemplateProperties,
   424  	}
   425  	return converter.expandTemplate().parseData().results()
   426  }
   427  
   428  // ConvertToInlineJSONTemplateExpression transforms the templateData contents into
   429  // an Fn::Join- compatible inline JSON representation for template serialization.
   430  // The templateData contents may include both golang text/template properties
   431  // and single-line JSON Fn::Join supported serializations.
   432  func ConvertToInlineJSONTemplateExpression(templateData io.Reader,
   433  	additionalUserTemplateProperties map[string]interface{}) (*gocf.StringExpr, error) {
   434  	converter := &templateConverter{
   435  		templateReader:          templateData,
   436  		additionalTemplateProps: additionalUserTemplateProperties,
   437  		doQuote:                 true,
   438  	}
   439  	return converter.expandTemplate().parseData().results()
   440  }
   441  
   442  // StackEvents returns the slice of cloudformation.StackEvents for the given stackID or stackName
   443  func StackEvents(stackID string,
   444  	eventFilterLowerBoundInclusive time.Time,
   445  	awsSession *session.Session) ([]*cloudformation.StackEvent, error) {
   446  
   447  	cfService := cloudformation.New(awsSession)
   448  	var events []*cloudformation.StackEvent
   449  
   450  	nextToken := ""
   451  	for {
   452  		params := &cloudformation.DescribeStackEventsInput{
   453  			StackName: aws.String(stackID),
   454  		}
   455  		if len(nextToken) > 0 {
   456  			params.NextToken = aws.String(nextToken)
   457  		}
   458  
   459  		resp, err := cfService.DescribeStackEvents(params)
   460  		if nil != err {
   461  			return nil, err
   462  		}
   463  		for _, eachEvent := range resp.StackEvents {
   464  			if eachEvent.Timestamp.Equal(eventFilterLowerBoundInclusive) ||
   465  				eachEvent.Timestamp.After(eventFilterLowerBoundInclusive) {
   466  				events = append(events, eachEvent)
   467  			}
   468  		}
   469  		if nil == resp.NextToken {
   470  			break
   471  		} else {
   472  			nextToken = *resp.NextToken
   473  		}
   474  	}
   475  	return events, nil
   476  }
   477  
   478  // WaitForStackOperationCompleteResult encapsulates the stackInfo
   479  // following a WaitForStackOperationComplete call
   480  type WaitForStackOperationCompleteResult struct {
   481  	operationSuccessful bool
   482  	stackInfo           *cloudformation.Stack
   483  }
   484  
   485  // WaitForStackOperationComplete is a blocking, polling based call that
   486  // periodically fetches the stackID set of events and uses the state value
   487  // to determine if an operation is complete
   488  func WaitForStackOperationComplete(stackID string,
   489  	pollingMessage string,
   490  	awsCloudFormation *cloudformation.CloudFormation,
   491  	logger *logrus.Logger) (*WaitForStackOperationCompleteResult, error) {
   492  
   493  	result := &WaitForStackOperationCompleteResult{}
   494  
   495  	startTime := time.Now()
   496  
   497  	// Startup a spinner...
   498  	// TODO: special case iTerm per https://github.com/briandowns/spinner/issues/64
   499  	charSetIndex := 39
   500  	if strings.Contains(os.Getenv("LC_TERMINAL"), "iTerm") {
   501  		charSetIndex = 7
   502  	}
   503  	cliSpinner := spinner.New(spinner.CharSets[charSetIndex],
   504  		333*time.Millisecond)
   505  	spinnerErr := cliSpinner.Color("red", "bold")
   506  	if spinnerErr != nil {
   507  		logger.WithField("error", spinnerErr).Warn("Failed to set spinner color")
   508  	}
   509  	cliSpinnerStarted := false
   510  
   511  	// Poll for the current stackID state, and
   512  	describeStacksInput := &cloudformation.DescribeStacksInput{
   513  		StackName: aws.String(stackID),
   514  	}
   515  	for waitComplete := false; !waitComplete; {
   516  		// Startup the spinner if needed...
   517  		switch logger.Formatter.(type) {
   518  		case *logrus.JSONFormatter:
   519  			{
   520  				logger.Info(pollingMessage)
   521  			}
   522  		default:
   523  			if !cliSpinnerStarted {
   524  				cliSpinner.Start()
   525  				defer cliSpinner.Stop()
   526  				cliSpinnerStarted = true
   527  			}
   528  			spinnerText := fmt.Sprintf(" %s (requested: %s)",
   529  				pollingMessage,
   530  				humanize.Time(startTime))
   531  			cliSpinner.Suffix = spinnerText
   532  		}
   533  
   534  		// Then sleep and figure out if things are done...
   535  		sleepDuration := time.Duration(11+rand.Int31n(13)) * time.Second
   536  		time.Sleep(sleepDuration)
   537  
   538  		describeStacksOutput, err := awsCloudFormation.DescribeStacks(describeStacksInput)
   539  		if nil != err {
   540  			// TODO - add retry iff we're RateExceeded due to collective access
   541  			return nil, err
   542  		}
   543  		if len(describeStacksOutput.Stacks) <= 0 {
   544  			return nil, fmt.Errorf("failed to enumerate stack info: %v", *describeStacksInput.StackName)
   545  		}
   546  		result.stackInfo = describeStacksOutput.Stacks[0]
   547  		switch *(result.stackInfo).StackStatus {
   548  		case cloudformation.StackStatusCreateComplete,
   549  			cloudformation.StackStatusUpdateComplete:
   550  			result.operationSuccessful = true
   551  			waitComplete = true
   552  		case
   553  			// Include DeleteComplete as new provisions will automatically rollback
   554  			cloudformation.StackStatusDeleteComplete,
   555  			cloudformation.StackStatusCreateFailed,
   556  			cloudformation.StackStatusDeleteFailed,
   557  			cloudformation.StackStatusRollbackFailed,
   558  			cloudformation.StackStatusRollbackComplete,
   559  			cloudformation.StackStatusUpdateRollbackComplete:
   560  			result.operationSuccessful = false
   561  			waitComplete = true
   562  		default:
   563  			// If this is JSON output, just do the normal thing
   564  			// NOP
   565  		}
   566  	}
   567  	return result, nil
   568  }
   569  
   570  // StableResourceName returns a stable resource name
   571  func StableResourceName(value string) string {
   572  	return CloudFormationResourceName(value, value)
   573  }
   574  
   575  // CloudFormationResourceName returns a name suitable as a logical
   576  // CloudFormation resource value.  See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html
   577  // for more information.  The `prefix` value should provide a hint as to the
   578  // resource type (eg, `SNSConfigurator`, `ImageTranscoder`).  Note that the returned
   579  // name is not content-addressable.
   580  func CloudFormationResourceName(prefix string, parts ...string) string {
   581  	hash := sha1.New()
   582  	_, writeErr := hash.Write([]byte(prefix))
   583  	//lint:ignore SA9003 because it's TODO
   584  	if writeErr != nil {
   585  	}
   586  	if len(parts) <= 0 {
   587  		randValue := rand.Int63()
   588  		_, writeErr = hash.Write([]byte(strconv.FormatInt(randValue, 10)))
   589  		//lint:ignore SA9003 because it's TODO
   590  		if writeErr != nil {
   591  		}
   592  	} else {
   593  		for _, eachPart := range parts {
   594  			_, writeErr = hash.Write([]byte(eachPart))
   595  			//lint:ignore SA9003 because it's TODO
   596  			if writeErr != nil {
   597  			}
   598  		}
   599  	}
   600  	resourceName := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(hash.Sum(nil)))
   601  
   602  	// Ensure that any non alphanumeric characters are replaced with ""
   603  	return reCloudFormationInvalidChars.ReplaceAllString(resourceName, "x")
   604  }
   605  
   606  // UploadTemplate marshals the given cfTemplate and uploads it to the
   607  // supplied bucket using the given KeyName
   608  func UploadTemplate(serviceName string,
   609  	cfTemplate *gocf.Template,
   610  	s3Bucket string,
   611  	s3KeyName string,
   612  	awsSession *session.Session,
   613  	logger *logrus.Logger) (string, error) {
   614  
   615  	logger.WithFields(logrus.Fields{
   616  		"Key":    s3KeyName,
   617  		"Bucket": s3Bucket,
   618  	}).Info("Uploading CloudFormation template")
   619  
   620  	s3Uploader := s3manager.NewUploader(awsSession)
   621  
   622  	// Serialize the template and upload it
   623  	cfTemplateJSON, err := json.Marshal(cfTemplate)
   624  	if err != nil {
   625  		return "", errors.Wrap(err, "Failed to Marshal CloudFormation template")
   626  	}
   627  
   628  	// Upload the actual CloudFormation template to S3 to maximize the template
   629  	// size limit
   630  	// Ref: http://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html
   631  	contentBody := string(cfTemplateJSON)
   632  	uploadInput := &s3manager.UploadInput{
   633  		Bucket:      &s3Bucket,
   634  		Key:         &s3KeyName,
   635  		ContentType: aws.String("application/json"),
   636  		Body:        strings.NewReader(contentBody),
   637  	}
   638  	templateUploadResult, templateUploadResultErr := s3Uploader.Upload(uploadInput)
   639  	if nil != templateUploadResultErr {
   640  		return "", templateUploadResultErr
   641  	}
   642  
   643  	// Be transparent
   644  	logger.WithFields(logrus.Fields{
   645  		"URL": templateUploadResult.Location,
   646  	}).Info("Template uploaded")
   647  	return templateUploadResult.Location, nil
   648  }
   649  
   650  // StackExists returns whether the given stackName or stackID currently exists
   651  func StackExists(stackNameOrID string, awsSession *session.Session, logger *logrus.Logger) (bool, error) {
   652  	cf := cloudformation.New(awsSession)
   653  
   654  	describeStacksInput := &cloudformation.DescribeStacksInput{
   655  		StackName: aws.String(stackNameOrID),
   656  	}
   657  	describeStacksOutput, err := cf.DescribeStacks(describeStacksInput)
   658  	logger.WithFields(logrus.Fields{
   659  		"DescribeStackOutput": describeStacksOutput,
   660  	}).Debug("DescribeStackOutput results")
   661  
   662  	exists := false
   663  	if err != nil {
   664  		logger.WithFields(logrus.Fields{
   665  			"DescribeStackOutputError": err,
   666  		}).Debug("DescribeStackOutput")
   667  
   668  		// If the stack doesn't exist, then no worries
   669  		if strings.Contains(err.Error(), "does not exist") {
   670  			exists = false
   671  		} else {
   672  			return false, err
   673  		}
   674  	} else {
   675  		exists = true
   676  	}
   677  	return exists, nil
   678  }
   679  
   680  // CreateStackChangeSet returns the DescribeChangeSetOutput
   681  // for a given stack transformation
   682  func CreateStackChangeSet(changeSetRequestName string,
   683  	serviceName string,
   684  	cfTemplate *gocf.Template,
   685  	templateURL string,
   686  	awsTags []*cloudformation.Tag,
   687  	awsCloudFormation *cloudformation.CloudFormation,
   688  	logger *logrus.Logger) (*cloudformation.DescribeChangeSetOutput, error) {
   689  
   690  	capabilities := stackCapabilities(cfTemplate)
   691  	changeSetInput := &cloudformation.CreateChangeSetInput{
   692  		Capabilities:  capabilities,
   693  		ChangeSetName: aws.String(changeSetRequestName),
   694  		ClientToken:   aws.String(changeSetRequestName),
   695  		Description:   aws.String(fmt.Sprintf("Change set for service: %s", serviceName)),
   696  		StackName:     aws.String(serviceName),
   697  		TemplateURL:   aws.String(templateURL),
   698  	}
   699  	if len(awsTags) != 0 {
   700  		changeSetInput.Tags = awsTags
   701  	}
   702  	_, changeSetError := awsCloudFormation.CreateChangeSet(changeSetInput)
   703  	if nil != changeSetError {
   704  		return nil, changeSetError
   705  	}
   706  
   707  	logger.WithFields(logrus.Fields{
   708  		"StackName": serviceName,
   709  	}).Info("Issued CreateChangeSet request")
   710  
   711  	describeChangeSetInput := cloudformation.DescribeChangeSetInput{
   712  		ChangeSetName: aws.String(changeSetRequestName),
   713  		StackName:     aws.String(serviceName),
   714  	}
   715  
   716  	var describeChangeSetOutput *cloudformation.DescribeChangeSetOutput
   717  
   718  	// Loop, with a total timeout of 3 minutes
   719  	startTime := time.Now()
   720  	changeSetStabilized := false
   721  	for !changeSetStabilized {
   722  		sleepDuration := cloudformationPollingDelay()
   723  		time.Sleep(sleepDuration)
   724  
   725  		changeSetOutput, describeChangeSetError := awsCloudFormation.DescribeChangeSet(&describeChangeSetInput)
   726  
   727  		if nil != describeChangeSetError {
   728  			return nil, describeChangeSetError
   729  		}
   730  		describeChangeSetOutput = changeSetOutput
   731  		// The current status of the change set, such as CREATE_IN_PROGRESS, CREATE_COMPLETE,
   732  		// or FAILED.
   733  		if nil != describeChangeSetOutput {
   734  			switch *describeChangeSetOutput.Status {
   735  			case "CREATE_IN_PROGRESS":
   736  				// If this has taken more than 3 minutes, then that's an error
   737  				elapsedTime := time.Since(startTime)
   738  				if elapsedTime > cloudformationPollingTimeout {
   739  					return nil, fmt.Errorf("failed to finalize ChangeSet within window: %s", elapsedTime.String())
   740  				}
   741  			case "CREATE_COMPLETE":
   742  				changeSetStabilized = true
   743  			case "FAILED":
   744  				return nil, fmt.Errorf("failed to create ChangeSet: %#v", *describeChangeSetOutput)
   745  			}
   746  		}
   747  	}
   748  
   749  	logger.WithFields(logrus.Fields{
   750  		"DescribeChangeSetOutput": describeChangeSetOutput,
   751  	}).Debug("DescribeChangeSet result")
   752  
   753  	//////////////////////////////////////////////////////////////////////////////
   754  	// If there aren't any changes, then skip it...
   755  	if len(describeChangeSetOutput.Changes) <= 0 {
   756  		logger.WithFields(logrus.Fields{
   757  			"StackName": serviceName,
   758  		}).Info("No changes detected for service")
   759  
   760  		// Delete it...
   761  		_, deleteChangeSetResultErr := DeleteChangeSet(serviceName,
   762  			changeSetRequestName,
   763  			awsCloudFormation)
   764  		return nil, deleteChangeSetResultErr
   765  	}
   766  	return describeChangeSetOutput, nil
   767  }
   768  
   769  // DeleteChangeSet is a utility function that attempts to delete
   770  // an existing CloudFormation change set, with a bit of retry
   771  // logic in case of EC
   772  func DeleteChangeSet(stackName string,
   773  	changeSetRequestName string,
   774  	awsCloudFormation *cloudformation.CloudFormation) (*cloudformation.DeleteChangeSetOutput, error) {
   775  
   776  	// Delete request...
   777  	deleteChangeSetInput := cloudformation.DeleteChangeSetInput{
   778  		ChangeSetName: aws.String(changeSetRequestName),
   779  		StackName:     aws.String(stackName),
   780  	}
   781  
   782  	startTime := time.Now()
   783  	for {
   784  		elapsedTime := time.Since(startTime)
   785  
   786  		deleteChangeSetResults, deleteChangeSetResultErr := awsCloudFormation.DeleteChangeSet(&deleteChangeSetInput)
   787  		if nil == deleteChangeSetResultErr {
   788  			return deleteChangeSetResults, nil
   789  		} else if strings.Contains(deleteChangeSetResultErr.Error(), "CREATE_IN_PROGRESS") {
   790  			if elapsedTime > cloudformationPollingTimeout {
   791  				return nil, fmt.Errorf("failed to delete ChangeSet within timeout window: %s", elapsedTime.String())
   792  			}
   793  			sleepDuration := cloudformationPollingDelay()
   794  			time.Sleep(sleepDuration)
   795  		} else {
   796  			return nil, deleteChangeSetResultErr
   797  		}
   798  	}
   799  }
   800  
   801  // ListStacks returns a slice of stacks that meet the given filter.
   802  func ListStacks(session *session.Session,
   803  	maxReturned int,
   804  	stackFilters ...string) ([]*cloudformation.StackSummary, error) {
   805  
   806  	listStackInput := &cloudformation.ListStacksInput{
   807  		StackStatusFilter: []*string{},
   808  	}
   809  	for _, eachFilter := range stackFilters {
   810  		listStackInput.StackStatusFilter = append(listStackInput.StackStatusFilter, aws.String(eachFilter))
   811  	}
   812  	cloudformationSvc := cloudformation.New(session)
   813  	accumulator := []*cloudformation.StackSummary{}
   814  	for {
   815  		listResult, listResultErr := cloudformationSvc.ListStacks(listStackInput)
   816  		if listResultErr != nil {
   817  			return nil, listResultErr
   818  		}
   819  		accumulator = append(accumulator, listResult.StackSummaries...)
   820  		if len(accumulator) >= maxReturned || listResult.NextToken == nil {
   821  			return accumulator, nil
   822  		}
   823  		listStackInput.NextToken = listResult.NextToken
   824  	}
   825  }
   826  
   827  // ConvergeStackState ensures that the serviceName converges to the template
   828  // state defined by cfTemplate. This function establishes a polling loop to determine
   829  // when the stack operation has completed.
   830  func ConvergeStackState(serviceName string,
   831  	cfTemplate *gocf.Template,
   832  	templateURL string,
   833  	tags map[string]string,
   834  	startTime time.Time,
   835  	operationTimeout time.Duration,
   836  	awsSession *session.Session,
   837  	outputsDividerChar string,
   838  	dividerWidth int,
   839  	logger *logrus.Logger) (*cloudformation.Stack, error) {
   840  
   841  	awsCloudFormation := cloudformation.New(awsSession)
   842  	// Update the tags
   843  	awsTags := make([]*cloudformation.Tag, 0)
   844  	if nil != tags {
   845  		for eachKey, eachValue := range tags {
   846  			awsTags = append(awsTags,
   847  				&cloudformation.Tag{
   848  					Key:   aws.String(eachKey),
   849  					Value: aws.String(eachValue),
   850  				})
   851  		}
   852  	}
   853  	exists, existsErr := StackExists(serviceName, awsSession, logger)
   854  	if nil != existsErr {
   855  		return nil, existsErr
   856  	}
   857  	stackID := ""
   858  	if exists {
   859  		updateErr := updateStackViaChangeSet(serviceName,
   860  			cfTemplate,
   861  			templateURL,
   862  			awsTags,
   863  			awsCloudFormation,
   864  			logger)
   865  
   866  		if nil != updateErr {
   867  			return nil, updateErr
   868  		}
   869  		stackID = serviceName
   870  	} else {
   871  		// Create stack
   872  		createStackInput := &cloudformation.CreateStackInput{
   873  			StackName:        aws.String(serviceName),
   874  			TemplateURL:      aws.String(templateURL),
   875  			TimeoutInMinutes: aws.Int64(int64(operationTimeout.Minutes())),
   876  			OnFailure:        aws.String(cloudformation.OnFailureDelete),
   877  			Capabilities:     stackCapabilities(cfTemplate),
   878  		}
   879  		if len(awsTags) != 0 {
   880  			createStackInput.Tags = awsTags
   881  		}
   882  		createStackResponse, createStackResponseErr := awsCloudFormation.CreateStack(createStackInput)
   883  		if nil != createStackResponseErr {
   884  			return nil, createStackResponseErr
   885  		}
   886  		logger.WithFields(logrus.Fields{
   887  			"StackID": *createStackResponse.StackId,
   888  		}).Info("Creating stack")
   889  
   890  		stackID = *createStackResponse.StackId
   891  	}
   892  	// Wait for the operation to succeed
   893  	pollingMessage := "Waiting for CloudFormation operation to complete"
   894  	convergeResult, convergeErr := WaitForStackOperationComplete(stackID,
   895  		pollingMessage,
   896  		awsCloudFormation,
   897  		logger)
   898  	if nil != convergeErr {
   899  		return nil, convergeErr
   900  	}
   901  	// Get the events and assemble them into either errors to output
   902  	// or summary information
   903  	resourceMetrics := make(map[string]*resourceProvisionMetrics)
   904  	errorMessages := []string{}
   905  	events, err := StackEvents(stackID, startTime, awsSession)
   906  	if nil != err {
   907  		return nil, fmt.Errorf("failed to retrieve stack events: %s", err.Error())
   908  	}
   909  
   910  	for _, eachEvent := range events {
   911  		switch *eachEvent.ResourceStatus {
   912  		case cloudformation.ResourceStatusCreateFailed,
   913  			cloudformation.ResourceStatusDeleteFailed,
   914  			cloudformation.ResourceStatusUpdateFailed:
   915  			errMsg := fmt.Sprintf("\tError ensuring %s (%s): %s",
   916  				aws.StringValue(eachEvent.ResourceType),
   917  				aws.StringValue(eachEvent.LogicalResourceId),
   918  				aws.StringValue(eachEvent.ResourceStatusReason))
   919  			// Only append if the resource failed because something else failed
   920  			// and this resource was canceled.
   921  			if !strings.Contains(errMsg, "cancelled") {
   922  				errorMessages = append(errorMessages, errMsg)
   923  			}
   924  		case cloudformation.ResourceStatusCreateInProgress,
   925  			cloudformation.ResourceStatusUpdateInProgress:
   926  			existingMetric, existingMetricExists := resourceMetrics[*eachEvent.LogicalResourceId]
   927  			if !existingMetricExists {
   928  				existingMetric = &resourceProvisionMetrics{}
   929  			}
   930  			existingMetric.resourceType = *eachEvent.ResourceType
   931  			existingMetric.logicalResourceID = *eachEvent.LogicalResourceId
   932  			existingMetric.startTime = *eachEvent.Timestamp
   933  			resourceMetrics[*eachEvent.LogicalResourceId] = existingMetric
   934  		case cloudformation.ResourceStatusCreateComplete,
   935  			cloudformation.ResourceStatusUpdateComplete:
   936  			existingMetric, existingMetricExists := resourceMetrics[*eachEvent.LogicalResourceId]
   937  			if !existingMetricExists {
   938  				existingMetric = &resourceProvisionMetrics{}
   939  			}
   940  			existingMetric.logicalResourceID = *eachEvent.LogicalResourceId
   941  			existingMetric.endTime = *eachEvent.Timestamp
   942  			resourceMetrics[*eachEvent.LogicalResourceId] = existingMetric
   943  		default:
   944  			// NOP
   945  		}
   946  	}
   947  
   948  	// If it didn't work, then output some failure information
   949  	if !convergeResult.operationSuccessful {
   950  		logger.Error("Stack provisioning error")
   951  		for _, eachError := range errorMessages {
   952  			logger.Error(eachError)
   953  		}
   954  		return nil, fmt.Errorf("failed to provision: %s", serviceName)
   955  	}
   956  
   957  	// Rip through the events so that we can output exactly how long it took to
   958  	// update each resource
   959  	resourceStats := make([]*resourceProvisionMetrics, len(resourceMetrics))
   960  	resourceStatIndex := 0
   961  	for _, eachResource := range resourceMetrics {
   962  		eachResource.elapsed = eachResource.endTime.Sub(eachResource.startTime)
   963  		resourceStats[resourceStatIndex] = eachResource
   964  		resourceStatIndex++
   965  	}
   966  	// Create a slice with them all, sorted by total elapsed mutation time
   967  	sort.Slice(resourceStats, func(i, j int) bool {
   968  		return resourceStats[i].elapsed > resourceStats[j].elapsed
   969  	})
   970  
   971  	// Output the sorted time it took to create the necessary resources...
   972  	outputHeader := "CloudFormation Metrics "
   973  	suffix := strings.Repeat(outputsDividerChar, dividerWidth-len(outputHeader))
   974  	logger.Info(fmt.Sprintf("%s%s", outputHeader, suffix))
   975  	for _, eachResourceStat := range resourceStats {
   976  		logger.WithFields(logrus.Fields{
   977  			"Resource": eachResourceStat.logicalResourceID,
   978  			"Type":     eachResourceStat.resourceType,
   979  			"Duration": fmt.Sprintf("%.2fs", eachResourceStat.elapsed.Seconds()),
   980  		}).Info("    Operation duration")
   981  	}
   982  
   983  	if nil != convergeResult.stackInfo.Outputs {
   984  		// Add a nice divider if there are Stack specific output
   985  		outputHeader := "Stack Outputs "
   986  		suffix := strings.Repeat(outputsDividerChar, dividerWidth-len(outputHeader))
   987  		logger.Info(fmt.Sprintf("%s%s", outputHeader, suffix))
   988  
   989  		for _, eachOutput := range convergeResult.stackInfo.Outputs {
   990  			logger.WithFields(logrus.Fields{
   991  				"Value":       aws.StringValue(eachOutput.OutputValue),
   992  				"Description": aws.StringValue(eachOutput.Description),
   993  			}).Info(fmt.Sprintf("    %s", aws.StringValue(eachOutput.OutputKey)))
   994  		}
   995  	}
   996  	return convergeResult.stackInfo, nil
   997  }
   998  
   999  // UserAccountScopedStackName returns a CloudFormation stack
  1000  // name that takes into account the current username that is
  1001  //associated with the supplied AWS credentials
  1002  /*
  1003  A stack name can contain only alphanumeric characters
  1004  (case sensitive) and hyphens. It must start with an alphabetic
  1005  \character and cannot be longer than 128 characters.
  1006  */
  1007  func UserAccountScopedStackName(basename string,
  1008  	awsSession *session.Session) (string, error) {
  1009  	awsName, awsNameErr := platformAccountUserName(awsSession)
  1010  	if awsNameErr != nil {
  1011  		return "", awsNameErr
  1012  	}
  1013  	userName := strings.Replace(awsName, " ", "-", -1)
  1014  	userName = strings.Replace(userName, ".", "-", -1)
  1015  	return fmt.Sprintf("%s-%s", basename, userName), nil
  1016  }
  1017  
  1018  // UserScopedStackName returns a CloudFormation stack
  1019  // name that takes into account the current username
  1020  /*
  1021  A stack name can contain only alphanumeric characters
  1022  (case sensitive) and hyphens. It must start with an alphabetic
  1023  \character and cannot be longer than 128 characters.
  1024  */
  1025  func UserScopedStackName(basename string) string {
  1026  	platformUserName := platformUserName()
  1027  	if platformUserName == "" {
  1028  		return basename
  1029  	}
  1030  	userName := strings.Replace(platformUserName, " ", "-", -1)
  1031  	userName = strings.Replace(userName, ".", "-", -1)
  1032  	return fmt.Sprintf("%s-%s", basename, userName)
  1033  }