diff --git a/builtin/providers/aws/resource_aws_cloudformation_stack.go b/builtin/providers/aws/resource_aws_cloudformation_stack.go index 8b9f1617e..c9448734b 100644 --- a/builtin/providers/aws/resource_aws_cloudformation_stack.go +++ b/builtin/providers/aws/resource_aws_cloudformation_stack.go @@ -398,9 +398,17 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface } log.Printf("[DEBUG] Updating CloudFormation stack: %s", input) - stack, err := conn.UpdateStack(input) + _, err := conn.UpdateStack(input) if err != nil { - return err + awsErr, ok := err.(awserr.Error) + // ValidationError: No updates are to be performed. + if !ok || + awsErr.Code() != "ValidationError" || + awsErr.Message() != "No updates are to be performed." { + return err + } + + log.Printf("[DEBUG] Current CloudFormation stack has no updates") } lastUpdatedTime, err := getLastCfEventTimestamp(d.Id(), conn) @@ -416,6 +424,7 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface } } var lastStatus string + var stackId string wait := resource.StateChangeConf{ Pending: []string{ "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", @@ -424,6 +433,7 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", }, Target: []string{ + "CREATE_COMPLETE", // If no stack update was performed "UPDATE_COMPLETE", "UPDATE_ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_FAILED", @@ -439,6 +449,8 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface return nil, "", err } + stackId = aws.StringValue(resp.Stacks[0].StackId) + status := *resp.Stacks[0].StackStatus lastStatus = status log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) @@ -453,7 +465,7 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface } if lastStatus == "UPDATE_ROLLBACK_COMPLETE" || lastStatus == "UPDATE_ROLLBACK_FAILED" { - reasons, err := getCloudFormationRollbackReasons(*stack.StackId, lastUpdatedTime, conn) + reasons, err := getCloudFormationRollbackReasons(stackId, lastUpdatedTime, conn) if err != nil { return fmt.Errorf("Failed getting details about rollback: %q", err.Error()) } @@ -461,7 +473,7 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface return fmt.Errorf("%s: %q", lastStatus, reasons) } - log.Printf("[DEBUG] CloudFormation stack %q has been updated", *stack.StackId) + log.Printf("[DEBUG] CloudFormation stack %q has been updated", stackId) return resourceAwsCloudFormationStackRead(d, meta) } diff --git a/builtin/providers/aws/resource_aws_cloudformation_stack_test.go b/builtin/providers/aws/resource_aws_cloudformation_stack_test.go index d7aae42fc..c2c2540dc 100644 --- a/builtin/providers/aws/resource_aws_cloudformation_stack_test.go +++ b/builtin/providers/aws/resource_aws_cloudformation_stack_test.go @@ -143,6 +143,8 @@ func TestAccAWSCloudFormation_withParams(t *testing.T) { // Regression for https://github.com/hashicorp/terraform/issues/4534 func TestAccAWSCloudFormation_withUrl_withParams(t *testing.T) { var stack cloudformation.Stack + cfRandInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + cfBucketName := fmt.Sprintf("tf-stack-with-url-and-params-%d", cfRandInt) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -150,13 +152,13 @@ func TestAccAWSCloudFormation_withUrl_withParams(t *testing.T) { CheckDestroy: testAccCheckAWSCloudFormationDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSCloudFormationConfig_templateUrl_withParams, + Config: testAccAWSCloudFormationConfig_templateUrl_withParams(cfBucketName, "tf-cf-stack.json", "11.0.0.0/16"), Check: resource.ComposeTestCheckFunc( testAccCheckCloudFormationStackExists("aws_cloudformation_stack.with-url-and-params", &stack), ), }, { - Config: testAccAWSCloudFormationConfig_templateUrl_withParams_modified, + Config: testAccAWSCloudFormationConfig_templateUrl_withParams(cfBucketName, "tf-cf-stack.json", "13.0.0.0/16"), Check: resource.ComposeTestCheckFunc( testAccCheckCloudFormationStackExists("aws_cloudformation_stack.with-url-and-params", &stack), ), @@ -167,6 +169,8 @@ func TestAccAWSCloudFormation_withUrl_withParams(t *testing.T) { func TestAccAWSCloudFormation_withUrl_withParams_withYaml(t *testing.T) { var stack cloudformation.Stack + cfRandInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + cfBucketName := fmt.Sprintf("tf-stack-with-url-and-params-%d", cfRandInt) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -174,7 +178,7 @@ func TestAccAWSCloudFormation_withUrl_withParams_withYaml(t *testing.T) { CheckDestroy: testAccCheckAWSCloudFormationDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSCloudFormationConfig_templateUrl_withParams_withYaml, + Config: testAccAWSCloudFormationConfig_templateUrl_withParams_withYaml(cfBucketName, "tf-cf-stack.yaml", "13.0.0.0/16"), Check: resource.ComposeTestCheckFunc( testAccCheckCloudFormationStackExists("aws_cloudformation_stack.with-url-and-params-and-yaml", &stack), ), @@ -183,6 +187,33 @@ func TestAccAWSCloudFormation_withUrl_withParams_withYaml(t *testing.T) { }) } +// Test for https://github.com/hashicorp/terraform/issues/5653 +func TestAccAWSCloudFormation_withUrl_withParams_noUpdate(t *testing.T) { + var stack cloudformation.Stack + cfRandInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + cfBucketName := fmt.Sprintf("tf-stack-with-url-and-params-%d", cfRandInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudFormationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCloudFormationConfig_templateUrl_withParams(cfBucketName, "tf-cf-stack-1.json", "11.0.0.0/16"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudFormationStackExists("aws_cloudformation_stack.with-url-and-params", &stack), + ), + }, + { + Config: testAccAWSCloudFormationConfig_templateUrl_withParams(cfBucketName, "tf-cf-stack-2.json", "11.0.0.0/16"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudFormationStackExists("aws_cloudformation_stack.with-url-and-params", &stack), + ), + }, + }, + }) +} + func testAccCheckCloudFormationStackExists(n string, stack *cloudformation.Stack) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -490,7 +521,8 @@ var testAccAWSCloudFormationConfig_withParams_modified = fmt.Sprintf( tpl_testAccAWSCloudFormationConfig_withParams, "12.0.0.0/16") -var tpl_testAccAWSCloudFormationConfig_templateUrl_withParams = ` +func testAccAWSCloudFormationConfig_templateUrl_withParams(bucketName, bucketKey, vpcCidr string) string { + return fmt.Sprintf(` resource "aws_s3_bucket" "b" { bucket = "%s" acl = "public-read" @@ -519,7 +551,7 @@ POLICY resource "aws_s3_bucket_object" "object" { bucket = "${aws_s3_bucket.b.id}" - key = "tf-cf-stack.json" + key = "%s" source = "test-fixtures/cloudformation-template.json" } @@ -532,9 +564,11 @@ resource "aws_cloudformation_stack" "with-url-and-params" { on_failure = "DELETE" timeout_in_minutes = 1 } -` +`, bucketName, bucketName, bucketKey, vpcCidr) +} -var tpl_testAccAWSCloudFormationConfig_templateUrl_withParams_withYaml = ` +func testAccAWSCloudFormationConfig_templateUrl_withParams_withYaml(bucketName, bucketKey, vpcCidr string) string { + return fmt.Sprintf(` resource "aws_s3_bucket" "b" { bucket = "%s" acl = "public-read" @@ -563,7 +597,7 @@ POLICY resource "aws_s3_bucket_object" "object" { bucket = "${aws_s3_bucket.b.id}" - key = "tf-cf-stack.yaml" + key = "%s" source = "test-fixtures/cloudformation-template.yaml" } @@ -576,17 +610,5 @@ resource "aws_cloudformation_stack" "with-url-and-params-and-yaml" { on_failure = "DELETE" timeout_in_minutes = 1 } -` - -var cfRandInt = rand.New(rand.NewSource(time.Now().UnixNano())).Int() -var cfBucketName = "tf-stack-with-url-and-params-" + fmt.Sprintf("%d", cfRandInt) - -var testAccAWSCloudFormationConfig_templateUrl_withParams = fmt.Sprintf( - tpl_testAccAWSCloudFormationConfig_templateUrl_withParams, - cfBucketName, cfBucketName, "11.0.0.0/16") -var testAccAWSCloudFormationConfig_templateUrl_withParams_modified = fmt.Sprintf( - tpl_testAccAWSCloudFormationConfig_templateUrl_withParams, - cfBucketName, cfBucketName, "13.0.0.0/16") -var testAccAWSCloudFormationConfig_templateUrl_withParams_withYaml = fmt.Sprintf( - tpl_testAccAWSCloudFormationConfig_templateUrl_withParams_withYaml, - cfBucketName, cfBucketName, "13.0.0.0/16") +`, bucketName, bucketName, bucketKey, vpcCidr) +}