func waitForCompletion(stack string, CloudFormation *cloudformation.CloudFormation, isDeleting bool) (string, error) { for { dres, err := CloudFormation.DescribeStacks(&cloudformation.DescribeStacksInput{ StackName: aws.String(stack), }) if err != nil { stdcli.Error(err) } err = displayProgress(stack, CloudFormation, isDeleting) if err != nil { stdcli.Error(err) } if len(dres.Stacks) != 1 { stdcli.Error(fmt.Errorf("could not read stack status")) } switch *dres.Stacks[0].StackStatus { case "CREATE_COMPLETE": // Dump .env if DEVELOPMENT if isDevelopment { fmt.Printf("Development .env:\n") // convert Port5432TcpAddr to PORT_5432_TCP_ADDR re := regexp.MustCompile("([a-z])([A-Z0-9])") // lower case letter followed by upper case or number, i.e. Port5432 re2 := regexp.MustCompile("([0-9])([A-Z])") // number followed by upper case letter, i.e. 5432Tcp for _, o := range dres.Stacks[0].Outputs { k := re.ReplaceAllString(*o.OutputKey, "${1}_${2}") k = re2.ReplaceAllString(k, "${1}_${2}") k = strings.ToUpper(k) fmt.Printf("%v=%v\n", k, *o.OutputValue) } } for _, o := range dres.Stacks[0].Outputs { if *o.OutputKey == "Dashboard" { return *o.OutputValue, nil } } return "", fmt.Errorf("could not install stack, contact [email protected] for assistance") case "CREATE_FAILED": return "", fmt.Errorf("stack creation failed, contact [email protected] for assistance") case "ROLLBACK_COMPLETE": return "", fmt.Errorf("stack creation failed, contact [email protected] for assistance") case "DELETE_COMPLETE": return "", nil case "DELETE_FAILED": return "", fmt.Errorf("stack deletion failed, contact [email protected] for assistance") } time.Sleep(2 * time.Second) } }
// getCloudFormationFailures returns ResourceStatusReason(s) // of events that should be failures based on regexp match of status func getCloudFormationFailures(stackName *string, afterTime time.Time, conn *cloudformation.CloudFormation) ([]string, error) { var failures []string // Only catching failures from last 100 events // Some extra iteration logic via NextToken could be added // but in reality it's nearly impossible to generate >100 // events by a single stack update events, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ StackName: stackName, }) if err != nil { return nil, err } failRe := regexp.MustCompile("_FAILED$") rollbackRe := regexp.MustCompile("^ROLLBACK_") for _, e := range events.StackEvents { if (failRe.MatchString(*e.ResourceStatus) || rollbackRe.MatchString(*e.ResourceStatus)) && e.Timestamp.After(afterTime) && e.ResourceStatusReason != nil { failures = append(failures, *e.ResourceStatusReason) } } return failures, nil }
func stackLambdaResources(serviceName string, cf *cloudformation.CloudFormation, logger *logrus.Logger) (provisionedResources, error) { resources := make(provisionedResources, 0) nextToken := "" for { params := &cloudformation.ListStackResourcesInput{ StackName: aws.String(serviceName), } if "" != nextToken { params.NextToken = aws.String(nextToken) } resp, err := cf.ListStackResources(params) if err != nil { logger.Error(err.Error()) return nil, err } for _, eachSummary := range resp.StackResourceSummaries { if *eachSummary.ResourceType == "AWS::Lambda::Function" { resources = append(resources, eachSummary) } } if nil != resp.NextToken { nextToken = *resp.NextToken } else { break } } return resources, nil }
// Does a given stack exist? func stackExists(stackNameOrID string, cf *cloudformation.CloudFormation, logger *logrus.Logger) (bool, error) { describeStacksInput := &cloudformation.DescribeStacksInput{ StackName: aws.String(stackNameOrID), } describeStacksOutput, err := cf.DescribeStacks(describeStacksInput) logger.WithFields(logrus.Fields{ "DescribeStackOutput": describeStacksOutput, }).Debug("DescribeStackOutput results") exists := false if err != nil { logger.WithFields(logrus.Fields{ "DescribeStackOutputError": err, }).Debug("DescribeStackOutput") // If the stack doesn't exist, then no worries if strings.Contains(err.Error(), "does not exist") { exists = false } else { return false, err } } else { exists = true } return exists, nil }
func destroyStack(svc *cloudformation.CloudFormation, name string) error { dreq := &cloudformation.DeleteStackInput{ StackName: aws.String(name), } _, err := svc.DeleteStack(dreq) return err }
// Return the StackEvents for the given StackName/StackID func stackEvents(stackID string, cfService *cloudformation.CloudFormation) ([]*cloudformation.StackEvent, error) { var events []*cloudformation.StackEvent nextToken := "" for { params := &cloudformation.DescribeStackEventsInput{ StackName: aws.String(stackID), } if len(nextToken) > 0 { params.NextToken = aws.String(nextToken) } resp, err := cfService.DescribeStackEvents(params) if nil != err { return nil, err } events = append(events, resp.StackEvents...) if nil == resp.NextToken { break } else { nextToken = *resp.NextToken } } return events, nil }
// getLastCfEventTimestamp takes the first event in a list // of events ordered from the newest to the oldest // and extracts timestamp from it // LastUpdatedTime only provides last >successful< updated time func getLastCfEventTimestamp(stackName string, conn *cloudformation.CloudFormation) ( *time.Time, error) { output, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ StackName: aws.String(stackName), }) if err != nil { return nil, err } return output.StackEvents[0].Timestamp, nil }
func descStack(svc *awscf.CloudFormation, stackName string) { input := &awscf.DescribeStackEventsInput{ StackName: aws.String(stackName), } resp, err := svc.DescribeStackEvents(input) if err != nil { log.Fatal(err) } if len(resp.StackEvents) > 0 { log.Println(awsutil.StringValue(resp.StackEvents[0])) } }
func cost(svc *awscf.CloudFormation, b []byte, params []*awscf.Parameter) { estInput := &awscf.EstimateTemplateCostInput{ Parameters: params, TemplateBody: aws.String(string(b)), } cost, err := svc.EstimateTemplateCost(estInput) if err != nil { log.Fatal(err) } fmt.Println(*cost.URL) }
func validateStack(svc *cloudformation.CloudFormation, stackBody string) (string, error) { input := &cloudformation.ValidateTemplateInput{ TemplateBody: aws.String(stackBody), } validationReport, err := svc.ValidateTemplate(input) if err != nil { return "", fmt.Errorf("Invalid cloudformation stack: %v", err) } return validationReport.String(), err }
func deleteStack(s Stack, distinctId string, CF *cloudformation.CloudFormation) error { deleteAttempts[s.StackName] += 1 switch deleteAttempts[s.StackName] { case 1: fmt.Printf("Deleting %s...\n", s.Name) default: fmt.Printf("Retrying deleting %s...\n", s.Name) } _, err := CF.DeleteStack(&cloudformation.DeleteStackInput{ StackName: aws.String(s.StackName), }) return err }
func (r *Run) runUpsert(client *cf.CloudFormation, d tacks.Document, stack string) error { e := d.Environment resp, _ := client.DescribeStacks(&cf.DescribeStacksInput{ StackName: aws.String(e.StackName), }) if len(resp.Stacks) == 0 { return r.runCreate(client, d, stack) } else { return r.runUpdate(client, d, stack) } }
func updateStack(svc *cloudformation.CloudFormation, stackName, stackBody string) (string, error) { input := &cloudformation.UpdateStackInput{ Capabilities: []*string{aws.String(cloudformation.CapabilityCapabilityIam)}, StackName: aws.String(stackName), TemplateBody: aws.String(stackBody), } updateOutput, err := svc.UpdateStack(input) if err != nil { return "", fmt.Errorf("Error updating cloudformation stack: %v", err) } return updateOutput.String(), waitForStackUpdateComplete(svc, *updateOutput.StackId) }
func getCloudFormationFailures(stackId string, conn *cloudformation.CloudFormation) ([]string, error) { var failures []string err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{ StackName: aws.String(stackId), }, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool { for _, e := range page.StackEvents { if cfStackEventIsFailure(e) { failures = append(failures, *e.ResourceStatusReason) } } return !lastPage }) return failures, err }
func delStack(svc *awscf.CloudFormation, stackName string) { input := &awscf.DeleteStackInput{ StackName: aws.String(stackName), } _, err := svc.DeleteStack(input) if err != nil { log.Fatal(err) } // the log.Println ends up looking like // 2015/06/04 16:55:36 { // // } // // log.Println(awsutil.StringValue(resp)) }
func waitForCompletion(stack string, CloudFormation *cloudformation.CloudFormation, isDeleting bool) (string, error) { for { dres, err := CloudFormation.DescribeStacks(&cloudformation.DescribeStacksInput{ StackName: aws.String(stack), }) if err != nil { return "", err } err = displayProgress(stack, CloudFormation, isDeleting) if err != nil { return "", err } if len(dres.Stacks) != 1 { return "", fmt.Errorf("could not read stack status") } switch *dres.Stacks[0].StackStatus { case "CREATE_COMPLETE": for _, o := range dres.Stacks[0].Outputs { if *o.OutputKey == "Dashboard" { return *o.OutputValue, nil } } stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", fmt.Errorf("could not install stack, contact [email protected] for assistance") case "CREATE_FAILED": stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", fmt.Errorf("stack creation failed, contact [email protected] for assistance") case "ROLLBACK_COMPLETE": stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", fmt.Errorf("stack creation failed, contact [email protected] for assistance") case "DELETE_COMPLETE": stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", nil case "DELETE_FAILED": stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", fmt.Errorf("stack deletion failed, contact [email protected] for assistance") } time.Sleep(2 * time.Second) } }
// WaitForStackOperationComplete is a blocking, polling based call that // periodically fetches the stackID set of events and uses the state value // to determine if an operation is complete func WaitForStackOperationComplete(stackID string, pollingMessage string, awsCloudFormation *cloudformation.CloudFormation, logger *logrus.Logger) (*WaitForStackOperationCompleteResult, error) { result := &WaitForStackOperationCompleteResult{} // Poll for the current stackID state, and describeStacksInput := &cloudformation.DescribeStacksInput{ StackName: aws.String(stackID), } for waitComplete := false; !waitComplete; { sleepDuration := time.Duration(11+rand.Int31n(13)) * time.Second time.Sleep(sleepDuration) describeStacksOutput, err := awsCloudFormation.DescribeStacks(describeStacksInput) if nil != err { // TODO - add retry iff we're RateExceeded due to collective access return nil, err } if len(describeStacksOutput.Stacks) <= 0 { return nil, fmt.Errorf("Failed to enumerate stack info: %v", *describeStacksInput.StackName) } result.stackInfo = describeStacksOutput.Stacks[0] switch *(result.stackInfo).StackStatus { case cloudformation.StackStatusCreateComplete, cloudformation.StackStatusUpdateComplete: result.operationSuccessful = true waitComplete = true case // Include DeleteComplete as new provisions will automatically rollback cloudformation.StackStatusDeleteComplete, cloudformation.StackStatusCreateFailed, cloudformation.StackStatusDeleteFailed, cloudformation.StackStatusRollbackFailed, cloudformation.StackStatusRollbackComplete, cloudformation.StackStatusUpdateRollbackComplete: result.operationSuccessful = false waitComplete = true default: logger.Info(pollingMessage) } } return result, nil }
func provisionStack(svc *awscf.CloudFormation, b []byte, onFailure string, params []*awscf.Parameter, stackName string) { input := &awscf.CreateStackInput{ StackName: aws.String(stackName), Capabilities: []*string{ aws.String("CAPABILITY_IAM"), }, OnFailure: aws.String(onFailure), Parameters: params, TemplateBody: aws.String(string(b)), TimeoutInMinutes: aws.Long(20), } resp, err := svc.CreateStack(input) if err != nil { log.Fatal(err) } log.Println(awsutil.StringValue(resp)) }
func (r *Run) runCreate(client *cf.CloudFormation, d tacks.Document, stack string) error { e := d.Environment tacks.Logger().Infof("Creating stack %s", e.StackName) var ( capabilities []*string onFailure = "DO_NOTHING" tags []*cf.Tag timeoutInMinutes uint8 = 15 ) if d.IsIamCapabilitiesRequired() { capabilities = append(capabilities, aws.String("CAPABILITY_IAM")) } if e.DeleteOnFailure { onFailure = "DELETE" } if e.Timeout > 0 { timeoutInMinutes = e.Timeout } for key, value := range e.Tags { tags = append(tags, &cf.Tag{ Key: aws.String(key), Value: aws.String(value), }) } _, err := client.CreateStack(&cf.CreateStackInput{ Capabilities: capabilities, OnFailure: aws.String(onFailure), StackName: aws.String(e.StackName), Tags: tags, TemplateBody: aws.String(stack), TimeoutInMinutes: aws.Long(int64(timeoutInMinutes)), }) return err }
func createStackAndWait(svc *cloudformation.CloudFormation, name, stackBody string) error { creq := &cloudformation.CreateStackInput{ StackName: aws.String(name), OnFailure: aws.String("DO_NOTHING"), Capabilities: []*string{aws.String(cloudformation.CapabilityCapabilityIam)}, TemplateBody: aws.String(stackBody), } resp, err := svc.CreateStack(creq) if err != nil { return err } if err := waitForStackCreateComplete(svc, aws.StringValue(resp.StackId)); err != nil { return err } return nil }
func getStackResources(svc *cloudformation.CloudFormation, stackID string) ([]cloudformation.StackResourceSummary, error) { resources := make([]cloudformation.StackResourceSummary, 0) req := cloudformation.ListStackResourcesInput{ StackName: aws.String(stackID), } for { resp, err := svc.ListStackResources(&req) if err != nil { return nil, err } for _, s := range resp.StackResourceSummaries { resources = append(resources, *s) } req.NextToken = resp.NextToken if aws.StringValue(req.NextToken) == "" { break } } return resources, nil }
func runDescribeStackEevntsQuery(svc *cloudformation.CloudFormation, params *cloudformation.DescribeStackEventsInput) []*cloudformation.StackEvent { resp, err := svc.DescribeStackEvents(params) if err != nil { if awsErr, ok := err.(awserr.Error); ok { // Generic AWS error with Code, Message, and original error (if any) fmt.Println(awsErr.Code(), awsErr.Message(), awsErr.OrigErr()) if reqErr, ok := err.(awserr.RequestFailure); ok { // A service error occurred fmt.Println(reqErr.Code(), reqErr.Message(), reqErr.StatusCode(), reqErr.RequestID()) } } else { // This case should never be hit, the SDK should always return an // error which satisfies the awserr.Error interface. fmt.Println(err.Error()) } } return resp.StackEvents }
func getCloudFormationRollbackReasons(stackId string, afterTime *time.Time, conn *cloudformation.CloudFormation) ([]string, error) { var failures []string err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{ StackName: aws.String(stackId), }, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool { for _, e := range page.StackEvents { if afterTime != nil && !e.Timestamp.After(*afterTime) { continue } if cfStackEventIsFailure(e) || cfStackEventIsRollback(e) { failures = append(failures, *e.ResourceStatusReason) } } return !lastPage }) return failures, err }
func waitForStackCreateComplete(svc *cloudformation.CloudFormation, stackID string) error { req := cloudformation.DescribeStacksInput{ StackName: aws.String(stackID), } for { resp, err := svc.DescribeStacks(&req) if err != nil { return err } if len(resp.Stacks) == 0 { return fmt.Errorf("stack not found") } switch aws.StringValue(resp.Stacks[0].StackStatus) { case cloudformation.ResourceStatusCreateComplete: return nil case cloudformation.ResourceStatusCreateFailed: return errors.New(aws.StringValue(resp.Stacks[0].StackStatusReason)) } time.Sleep(3 * time.Second) } }
func watch(svc *awscf.CloudFormation, verbose bool, interval int, stackName string) { req := &awscf.DescribeStacksInput{StackName: aws.String(stackName)} var maxError int previousStatus := "" var err error var firstLoop bool for maxError < 3 { if !firstLoop { time.Sleep(time.Duration(interval) * time.Second) } else { firstLoop = true } resp, err := svc.DescribeStacks(req) if err != nil { if previousStatus == "DELETE_IN_PROGRESS" { fmt.Printf("%s Finished\n", time.Now().Format(time.RFC3339)) return } fmt.Printf("Error: %s - retrying\n", err) maxError++ continue } for _, stack := range resp.Stacks { if *stack.StackName == stackName { if *stack.StackStatus != previousStatus || verbose { fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), *stack.StackStatus) previousStatus = *stack.StackStatus } } if strings.HasSuffix(previousStatus, "COMPLETE") { fmt.Printf("%s Finished\n", time.Now().Format(time.RFC3339)) return } } } fmt.Printf("Error: %s - giving up\n", err) }
func waitForStackUpdateComplete(svc *cloudformation.CloudFormation, stackID string) error { req := cloudformation.DescribeStacksInput{ StackName: aws.String(stackID), } for { resp, err := svc.DescribeStacks(&req) if err != nil { return err } if len(resp.Stacks) == 0 { return fmt.Errorf("stack not found") } statusString := aws.StringValue(resp.Stacks[0].StackStatus) switch statusString { case cloudformation.ResourceStatusUpdateComplete: return nil case cloudformation.ResourceStatusUpdateFailed, cloudformation.StackStatusUpdateRollbackComplete, cloudformation.StackStatusUpdateRollbackFailed: errMsg := fmt.Sprintf("Stack status: %s : %s", statusString, aws.StringValue(resp.Stacks[0].StackStatusReason)) return errors.New(errMsg) } time.Sleep(3 * time.Second) } }
func (r *Run) runUpdate(client *cf.CloudFormation, d tacks.Document, stack string) error { e := d.Environment tacks.Logger().Infof("Updating stack %s", e.StackName) var ( capabilities []*string ) if d.IsIamCapabilitiesRequired() { capabilities = append(capabilities, aws.String("CAPABILITY_IAM")) } _, err := client.UpdateStack(&cf.UpdateStackInput{ Capabilities: capabilities, StackName: aws.String(e.StackName), TemplateBody: aws.String(stack), }) return err }
func updateStackViaChangeSet(serviceName string, cfTemplateURL string, capabilities []*string, awsTags []*cloudformation.Tag, awsCloudFormation *cloudformation.CloudFormation, logger *logrus.Logger) error { // Create a change set name... changeSetRequestName := CloudFormationResourceName(fmt.Sprintf("%sChangeSet", serviceName)) changeSetInput := &cloudformation.CreateChangeSetInput{ Capabilities: capabilities, ChangeSetName: aws.String(changeSetRequestName), ClientToken: aws.String(changeSetRequestName), Description: aws.String(fmt.Sprintf("Change set for service: %s", serviceName)), StackName: aws.String(serviceName), TemplateURL: aws.String(cfTemplateURL), } if len(awsTags) != 0 { changeSetInput.Tags = awsTags } _, changeSetError := awsCloudFormation.CreateChangeSet(changeSetInput) if nil != changeSetError { return changeSetError } logger.WithFields(logrus.Fields{ "StackName": serviceName, }).Info("Issued CreateChangeSet request") describeChangeSetInput := cloudformation.DescribeChangeSetInput{ ChangeSetName: aws.String(changeSetRequestName), StackName: aws.String(serviceName), } var describeChangeSetOutput *cloudformation.DescribeChangeSetOutput for waitComplete := false; !waitComplete; { sleepDuration := time.Duration(11+rand.Int31n(13)) * time.Second time.Sleep(sleepDuration) changeSetOutput, describeChangeSetError := awsCloudFormation.DescribeChangeSet(&describeChangeSetInput) if nil != describeChangeSetError { return describeChangeSetError } describeChangeSetOutput = changeSetOutput waitComplete = (nil != describeChangeSetOutput) } logger.WithFields(logrus.Fields{ "DescribeChangeSetOutput": describeChangeSetOutput, }).Debug("DescribeChangeSet result") ////////////////////////////////////////////////////////////////////////////// // If there aren't any changes, then skip it... if len(describeChangeSetOutput.Changes) <= 0 { logger.WithFields(logrus.Fields{ "StackName": serviceName, }).Info("No changes detected for service") // Delete it... deleteChangeSetInput := cloudformation.DeleteChangeSetInput{ ChangeSetName: aws.String(changeSetRequestName), StackName: aws.String(serviceName), } _, deleteChangeSetResultErr := awsCloudFormation.DeleteChangeSet(&deleteChangeSetInput) return deleteChangeSetResultErr } ////////////////////////////////////////////////////////////////////////////// // Apply the change executeChangeSetInput := cloudformation.ExecuteChangeSetInput{ ChangeSetName: aws.String(changeSetRequestName), StackName: aws.String(serviceName), } executeChangeSetOutput, executeChangeSetError := awsCloudFormation.ExecuteChangeSet(&executeChangeSetInput) logger.WithFields(logrus.Fields{ "ExecuteChangeSetOutput": executeChangeSetOutput, }).Debug("ExecuteChangeSet result") if nil == executeChangeSetError { logger.WithFields(logrus.Fields{ "StackName": serviceName, }).Info("Issued ExecuteChangeSet request") } return executeChangeSetError }
} func (f *FakeCloudFormationBackend) UpdateStack(input *cloudformation.UpdateStackInput) (*cloudformation.UpdateStackOutput, error) { f.UpdateStackCall.Receives = input return f.UpdateStackCall.ReturnsResult, f.UpdateStackCall.ReturnsError } func (f *FakeCloudFormationBackend) DescribeStacks(input *cloudformation.DescribeStacksInput) (*cloudformation.DescribeStacksOutput, error) { f.DescribeStacksCall.Receives = input return f.DescribeStacksCall.ReturnsResult, f.DescribeStacksCall.ReturnsError } var _ = Describe("Mocking out the CloudFormation service", func() { var ( fakeBackend *FakeCloudFormationBackend fakeServer *httptest.Server client *cloudformation.CloudFormation ) BeforeEach(func() { fakeBackend = &FakeCloudFormationBackend{} fakeServer = httptest.NewServer(awsfaker.New(fakeBackend)) client = cloudformation.New(newSession(fakeServer.URL)) }) AfterEach(func() { if fakeServer != nil { fakeServer.Close() } })
func displayProgress(stack string, CloudFormation *cloudformation.CloudFormation, isDeleting bool) error { res, err := CloudFormation.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ StackName: aws.String(stack), }) if err != nil { return err } for _, event := range res.StackEvents { if events[*event.EventId] == true { continue } events[*event.EventId] = true // Log all CREATE_FAILED to display and MixPanel if !isDeleting && *event.ResourceStatus == "CREATE_FAILED" { msg := fmt.Sprintf("Failed %s: %s", *event.ResourceType, *event.ResourceStatusReason) fmt.Println(msg) sendMixpanelEvent("convox-install-error", msg) } name := friendlyName(*event.ResourceType) if name == "" { continue } switch *event.ResourceStatus { case "CREATE_IN_PROGRESS": case "CREATE_COMPLETE": if !isDeleting { id := *event.PhysicalResourceId if strings.HasPrefix(id, "arn:") { id = *event.LogicalResourceId } fmt.Printf("Created %s: %s\n", name, id) } case "CREATE_FAILED": case "DELETE_IN_PROGRESS": case "DELETE_COMPLETE": id := *event.PhysicalResourceId if strings.HasPrefix(id, "arn:") { id = *event.LogicalResourceId } fmt.Printf("Deleted %s: %s\n", name, id) case "DELETE_SKIPPED": id := *event.PhysicalResourceId if strings.HasPrefix(id, "arn:") { id = *event.LogicalResourceId } fmt.Printf("Skipped %s: %s\n", name, id) case "DELETE_FAILED": return fmt.Errorf("stack deletion failed") case "ROLLBACK_IN_PROGRESS", "ROLLBACK_COMPLETE": case "UPDATE_IN_PROGRESS", "UPDATE_COMPLETE", "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "UPDATE_FAILED", "UPDATE_ROLLBACK_IN_PROGRESS", "UPDATE_ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_FAILED": default: return fmt.Errorf("Unhandled status: %s\n", *event.ResourceStatus) } } return nil }