diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index f8f443b73..bbbad7eea 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/directoryservice" @@ -47,6 +48,7 @@ type Config struct { } type AWSClient struct { + cfconn *cloudformation.CloudFormation cloudwatchconn *cloudwatch.CloudWatch cloudwatchlogsconn *cloudwatchlogs.CloudWatchLogs dsconn *directoryservice.DirectoryService @@ -175,6 +177,9 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing Lambda Connection") client.lambdaconn = lambda.New(awsConfig) + log.Println("[INFO] Initializing Cloudformation Connection") + client.cfconn = cloudformation.New(awsConfig) + log.Println("[INFO] Initializing CloudWatch SDK connection") client.cloudwatchconn = cloudwatch.New(awsConfig) diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index f73580d0f..547f9617a 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -163,6 +163,7 @@ func Provider() terraform.ResourceProvider { "aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), "aws_autoscaling_policy": resourceAwsAutoscalingPolicy(), + "aws_cloudformation_stack": resourceAwsCloudFormationStack(), "aws_cloudwatch_log_group": resourceAwsCloudWatchLogGroup(), "aws_autoscaling_lifecycle_hook": resourceAwsAutoscalingLifecycleHook(), "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), diff --git a/builtin/providers/aws/resource_aws_cloudformation_stack.go b/builtin/providers/aws/resource_aws_cloudformation_stack.go new file mode 100644 index 000000000..1846a3105 --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudformation_stack.go @@ -0,0 +1,451 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/cloudformation" +) + +func resourceAwsCloudFormationStack() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsCloudFormationStackCreate, + Read: resourceAwsCloudFormationStackRead, + Update: resourceAwsCloudFormationStackUpdate, + Delete: resourceAwsCloudFormationStackDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "template_body": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + StateFunc: normalizeJson, + }, + "template_url": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "capabilities": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "disable_rollback": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + "notification_arns": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "on_failure": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "parameters": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Computed: true, + }, + "outputs": &schema.Schema{ + Type: schema.TypeMap, + Computed: true, + }, + "policy_body": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + StateFunc: normalizeJson, + }, + "policy_url": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "timeout_in_minutes": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "tags": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsCloudFormationStackCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cfconn + + input := cloudformation.CreateStackInput{ + StackName: aws.String(d.Get("name").(string)), + } + if v, ok := d.GetOk("template_body"); ok { + input.TemplateBody = aws.String(normalizeJson(v.(string))) + } + if v, ok := d.GetOk("template_url"); ok { + input.TemplateURL = aws.String(v.(string)) + } + if v, ok := d.GetOk("capabilities"); ok { + input.Capabilities = expandStringList(v.(*schema.Set).List()) + } + if v, ok := d.GetOk("disable_rollback"); ok { + input.DisableRollback = aws.Bool(v.(bool)) + } + if v, ok := d.GetOk("notification_arns"); ok { + input.NotificationARNs = expandStringList(v.(*schema.Set).List()) + } + if v, ok := d.GetOk("on_failure"); ok { + input.OnFailure = aws.String(v.(string)) + } + if v, ok := d.GetOk("parameters"); ok { + input.Parameters = expandCloudFormationParameters(v.(map[string]interface{})) + } + if v, ok := d.GetOk("policy_body"); ok { + input.StackPolicyBody = aws.String(normalizeJson(v.(string))) + } + if v, ok := d.GetOk("policy_url"); ok { + input.StackPolicyURL = aws.String(v.(string)) + } + if v, ok := d.GetOk("tags"); ok { + input.Tags = expandCloudFormationTags(v.(map[string]interface{})) + } + if v, ok := d.GetOk("timeout_in_minutes"); ok { + input.TimeoutInMinutes = aws.Int64(int64(v.(int))) + } + + log.Printf("[DEBUG] Creating CloudFormation Stack: %s", input) + resp, err := conn.CreateStack(&input) + if err != nil { + return fmt.Errorf("Creating CloudFormation stack failed: %s", err.Error()) + } + + d.SetId(*resp.StackId) + + wait := resource.StateChangeConf{ + Pending: []string{"CREATE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS", "ROLLBACK_COMPLETE"}, + Target: "CREATE_COMPLETE", + Timeout: 30 * time.Minute, + MinTimeout: 5 * time.Second, + Refresh: func() (interface{}, string, error) { + resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ + StackName: aws.String(d.Get("name").(string)), + }) + status := *resp.Stacks[0].StackStatus + log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) + + if status == "ROLLBACK_COMPLETE" { + stack := resp.Stacks[0] + failures, err := getCloudFormationFailures(stack.StackName, *stack.CreationTime, conn) + if err != nil { + return resp, "", fmt.Errorf( + "Failed getting details about rollback: %q", err.Error()) + } + + return resp, "", fmt.Errorf("ROLLBACK_COMPLETE:\n%q", failures) + } + return resp, status, err + }, + } + + _, err = wait.WaitForState() + if err != nil { + return err + } + + log.Printf("[INFO] CloudFormation Stack %q created", d.Get("name").(string)) + + return resourceAwsCloudFormationStackRead(d, meta) +} + +func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cfconn + stackName := d.Get("name").(string) + + input := &cloudformation.DescribeStacksInput{ + StackName: aws.String(stackName), + } + resp, err := conn.DescribeStacks(input) + if err != nil { + return err + } + + stacks := resp.Stacks + if len(stacks) < 1 { + return nil + } + + tInput := cloudformation.GetTemplateInput{ + StackName: aws.String(stackName), + } + out, err := conn.GetTemplate(&tInput) + if err != nil { + return err + } + + d.Set("template_body", normalizeJson(*out.TemplateBody)) + + stack := stacks[0] + log.Printf("[DEBUG] Received CloudFormation stack: %s", stack) + + d.Set("name", stack.StackName) + d.Set("arn", stack.StackId) + + if stack.TimeoutInMinutes != nil { + d.Set("timeout_in_minutes", int(*stack.TimeoutInMinutes)) + } + if stack.Description != nil { + d.Set("description", stack.Description) + } + if stack.DisableRollback != nil { + d.Set("disable_rollback", stack.DisableRollback) + } + if len(stack.NotificationARNs) > 0 { + err = d.Set("notification_arns", schema.NewSet(schema.HashString, flattenStringList(stack.NotificationARNs))) + if err != nil { + return err + } + } + + originalParams := d.Get("parameters").(map[string]interface{}) + err = d.Set("parameters", flattenCloudFormationParameters(stack.Parameters, originalParams)) + if err != nil { + return err + } + + err = d.Set("tags", flattenCloudFormationTags(stack.Tags)) + if err != nil { + return err + } + + err = d.Set("outputs", flattenCloudFormationOutputs(stack.Outputs)) + if err != nil { + return err + } + + if len(stack.Capabilities) > 0 { + err = d.Set("capabilities", schema.NewSet(schema.HashString, flattenStringList(stack.Capabilities))) + if err != nil { + return err + } + } + + return nil +} + +func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cfconn + + input := &cloudformation.UpdateStackInput{ + StackName: aws.String(d.Get("name").(string)), + } + + if d.HasChange("template_body") { + input.TemplateBody = aws.String(normalizeJson(d.Get("template_body").(string))) + } + if d.HasChange("template_url") { + input.TemplateURL = aws.String(d.Get("template_url").(string)) + } + if d.HasChange("capabilities") { + input.Capabilities = expandStringList(d.Get("capabilities").(*schema.Set).List()) + } + if d.HasChange("notification_arns") { + input.NotificationARNs = expandStringList(d.Get("notification_arns").(*schema.Set).List()) + } + if d.HasChange("parameters") { + input.Parameters = expandCloudFormationParameters(d.Get("parameters").(map[string]interface{})) + } + if d.HasChange("policy_body") { + input.StackPolicyBody = aws.String(normalizeJson(d.Get("policy_body").(string))) + } + if d.HasChange("policy_url") { + input.StackPolicyURL = aws.String(d.Get("policy_url").(string)) + } + + log.Printf("[DEBUG] Updating CloudFormation stack: %s", input) + stack, err := conn.UpdateStack(input) + if err != nil { + return err + } + + lastUpdatedTime, err := getLastCfEventTimestamp(d.Get("name").(string), conn) + if err != nil { + return err + } + + wait := resource.StateChangeConf{ + Pending: []string{ + "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "UPDATE_IN_PROGRESS", + "UPDATE_ROLLBACK_IN_PROGRESS", + "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", + "UPDATE_ROLLBACK_COMPLETE", + }, + Target: "UPDATE_COMPLETE", + Timeout: 15 * time.Minute, + MinTimeout: 5 * time.Second, + Refresh: func() (interface{}, string, error) { + resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ + StackName: aws.String(d.Get("name").(string)), + }) + stack := resp.Stacks[0] + status := *stack.StackStatus + log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) + + if status == "UPDATE_ROLLBACK_COMPLETE" { + failures, err := getCloudFormationFailures(stack.StackName, *lastUpdatedTime, conn) + if err != nil { + return resp, "", fmt.Errorf( + "Failed getting details about rollback: %q", err.Error()) + } + + return resp, "", fmt.Errorf( + "UPDATE_ROLLBACK_COMPLETE:\n%q", failures) + } + + return resp, status, err + }, + } + + _, err = wait.WaitForState() + if err != nil { + return err + } + + log.Printf("[DEBUG] CloudFormation stack %q has been updated", *stack.StackId) + + return resourceAwsCloudFormationStackRead(d, meta) +} + +func resourceAwsCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cfconn + + input := &cloudformation.DeleteStackInput{ + StackName: aws.String(d.Get("name").(string)), + } + log.Printf("[DEBUG] Deleting CloudFormation stack %s", input) + _, err := conn.DeleteStack(input) + if err != nil { + awsErr, ok := err.(awserr.Error) + if !ok { + return err + } + + if awsErr.Code() == "ValidationError" { + // Ignore stack which has been already deleted + return nil + } + return err + } + + wait := resource.StateChangeConf{ + Pending: []string{"DELETE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS"}, + Target: "DELETE_COMPLETE", + Timeout: 30 * time.Minute, + MinTimeout: 5 * time.Second, + Refresh: func() (interface{}, string, error) { + resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ + StackName: aws.String(d.Get("name").(string)), + }) + + if err != nil { + awsErr, ok := err.(awserr.Error) + if !ok { + return resp, "DELETE_FAILED", err + } + + log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s", + awsErr.Code(), awsErr.Message()) + + if awsErr.Code() == "ValidationError" { + return resp, "DELETE_COMPLETE", nil + } + } + + if len(resp.Stacks) == 0 { + log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Get("name")) + return resp, "DELETE_COMPLETE", nil + } + + status := *resp.Stacks[0].StackStatus + log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) + + return resp, status, err + }, + } + + _, err = wait.WaitForState() + if err != nil { + return err + } + + log.Printf("[DEBUG] CloudFormation stack %q has been deleted", d.Id()) + + d.SetId("") + + return 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 +} + +// 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 +} diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index 5976a8ff0..fd581c84a 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/directoryservice" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" @@ -601,3 +602,57 @@ func flattenDSVpcSettings( return []map[string]interface{}{settings} } + +func expandCloudFormationParameters(params map[string]interface{}) []*cloudformation.Parameter { + var cfParams []*cloudformation.Parameter + for k, v := range params { + cfParams = append(cfParams, &cloudformation.Parameter{ + ParameterKey: aws.String(k), + ParameterValue: aws.String(v.(string)), + }) + } + + return cfParams +} + +// flattenCloudFormationParameters is flattening list of +// *cloudformation.Parameters and only returning existing +// parameters to avoid clash with default values +func flattenCloudFormationParameters(cfParams []*cloudformation.Parameter, + originalParams map[string]interface{}) map[string]interface{} { + params := make(map[string]interface{}, len(cfParams)) + for _, p := range cfParams { + _, isConfigured := originalParams[*p.ParameterKey] + if isConfigured { + params[*p.ParameterKey] = *p.ParameterValue + } + } + return params +} + +func expandCloudFormationTags(tags map[string]interface{}) []*cloudformation.Tag { + var cfTags []*cloudformation.Tag + for k, v := range tags { + cfTags = append(cfTags, &cloudformation.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + return cfTags +} + +func flattenCloudFormationTags(cfTags []*cloudformation.Tag) map[string]string { + tags := make(map[string]string, len(cfTags)) + for _, t := range cfTags { + tags[*t.Key] = *t.Value + } + return tags +} + +func flattenCloudFormationOutputs(cfOutputs []*cloudformation.Output) map[string]string { + outputs := make(map[string]string, len(cfOutputs)) + for _, o := range cfOutputs { + outputs[*o.OutputKey] = *o.OutputValue + } + return outputs +}