Skip to content

Commit 57e5784

Browse files
committed
Merge pull request #2 from gruntwork-io/improve-test-success-rate
Refine test conditions.
2 parents cb8bbe2 + 4349f00 commit 57e5784

File tree

18 files changed

+373
-128
lines changed

18 files changed

+373
-128
lines changed

README.md

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,30 +80,7 @@ We have two types of tests:
8080

8181
We use Go's built-in [package testing](https://golang.org/pkg/testing/) for tests. Therefore, simply create a file ending in `_test.go` and write your test.
8282

83-
Here's a sample:
84-
85-
```go
86-
func TestTerraformApplyMainFunction(t *testing.T) {
87-
// Running Terraform tests requires various resources like EC2 Keypairs, a unique identifier, etc.
88-
// This creates all necessary common resources with randomly chosen names in an randomly selected AWS region.
89-
rand, err := main.CreateRandomResourceCollection()
90-
defer main.DestroyRandomResourceCollection(rand)
91-
if err != nil {
92-
t.Errorf("Failed to create random resource collection: %s\n", err.Error())
93-
}
94-
95-
// Populate your template's variables.
96-
// Feel free to use string literals or get clever and try to compute a variable here.
97-
vars := make(map[string]string)
98-
vars["aws_region"] = rand.AwsRegion
99-
vars["ec2_key_name"] = rand.KeyPair.Name
100-
vars["ec2_instance_name"] = rand.UniqueId
101-
vars["ec2_image"] = rand.AmiId
102-
103-
// This will run both "terraform apply" and "terraform destroy" for a full-cycle unit test.
104-
main.TerraformApply("Integration Test - TestTerraformApplyMainFunction", "resources/minimal-example", vars, false)
105-
}
106-
```
83+
For samples, see the [terratest_test.go](_terratest_test.go) file which shows many examples of how to use terratest.
10784

10885
### Running unit tests
10986

apply.go

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,48 +10,37 @@ import (
1010

1111
// Apply handles all setup required for a Terraform Apply operation but does not perform a destroy operation or do any cleanup.
1212
// The caller of this function is expected to call Destroy to clean up the Terraform template when done.
13-
func Apply(ao *ApplyOptions) (string, *RandomResourceCollection, *ApplyOptions, error) {
13+
func Apply(ao *ApplyOptions) (string, error) {
1414
logger := log.NewLogger(ao.TestName)
1515
var output string
1616

1717
// SETUP
18-
19-
// Generate random values to allow tests to run in parallel
20-
logger.Println("Generating random values needed for terraform apply...")
21-
var rand *RandomResourceCollection
22-
23-
defer rand.DestroyResources()
24-
rand, err := CreateRandomResourceCollection()
25-
if err != nil {
26-
return output, rand, ao, fmt.Errorf("Failed to create random resource collection: %s", err.Error())
27-
}
28-
2918
// Configure terraform to use Remote State.
30-
err = aws.AssertS3BucketExists(ao.TfRemoteStateS3BucketRegion, ao.TfRemoteStateS3BucketName)
19+
err := aws.AssertS3BucketExists(ao.TfRemoteStateS3BucketRegion, ao.TfRemoteStateS3BucketName)
3120
if err != nil {
32-
return output, rand, ao, fmt.Errorf("Test failed because the S3 Bucket '%s' does not exist in the '%s' region.\n", ao.TfRemoteStateS3BucketName, ao.TfRemoteStateS3BucketRegion)
21+
return output, fmt.Errorf("Test failed because the S3 Bucket '%s' does not exist in the '%s' region.\n", ao.TfRemoteStateS3BucketName, ao.TfRemoteStateS3BucketRegion)
3322
}
3423

35-
terraform.ConfigureRemoteState(ao.TemplatePath, ao.TfRemoteStateS3BucketName, ao.generateTfStateFileName(rand), ao.TfRemoteStateS3BucketRegion, logger)
24+
terraform.ConfigureRemoteState(ao.TemplatePath, ao.TfRemoteStateS3BucketName, ao.getTfStateFileName(), ao.TfRemoteStateS3BucketRegion, logger)
3625

3726
// TERRAFORM APPLY
3827
// Download all the Terraform modules
3928
logger.Println("Running terraform get...")
4029
err = terraform.Get(ao.TemplatePath, logger)
4130
if err != nil {
42-
return output, rand, ao, fmt.Errorf("Failed to call terraform get successfully: %s\n", err.Error())
31+
return output, fmt.Errorf("Failed to call terraform get successfully: %s\n", err.Error())
4332
}
4433

4534
// Apply the Terraform template
4635
logger.Println("Running terraform apply...")
4736
if ao.AttemptTerraformRetry {
48-
output, err = terraform.ApplyAndGetOutputWithRetry(ao.TemplatePath, ao.Vars, logger)
37+
output, err = terraform.ApplyAndGetOutputWithRetry(ao.TemplatePath, ao.Vars, ao.RetryableTerraformErrors, logger)
4938
} else {
5039
output, err = terraform.ApplyAndGetOutput(ao.TemplatePath, ao.Vars, logger)
5140
}
5241
if err != nil {
53-
return output, rand, ao, fmt.Errorf("Failed to terraform apply: %s\n", err.Error())
42+
return output, fmt.Errorf("Failed to terraform apply: %s\n", err.Error())
5443
}
5544

56-
return output, rand, ao, nil
45+
return output, nil
5746
}

apply_and_destroy.go

Lines changed: 2 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,7 @@
11
package terratest
22

3-
import (
4-
"fmt"
5-
6-
"github.com/gruntwork-io/terratest/log"
7-
"github.com/gruntwork-io/terratest/aws"
8-
"github.com/gruntwork-io/terratest/terraform"
9-
)
10-
113
// ApplyAndDestroy wraps all setup and teardown required for a Terraform Apply operation. It returns the output of the terraform operations.
124
func ApplyAndDestroy(ao *ApplyOptions) (string, error) {
13-
logger := log.NewLogger(ao.TestName)
14-
var output string
15-
16-
// SETUP
17-
// Generate random values to allow tests to run in parallel
18-
logger.Println("Generating random values needed for terraform apply...")
19-
var rand *RandomResourceCollection
20-
21-
defer rand.DestroyResources()
22-
rand, err := CreateRandomResourceCollection()
23-
if err != nil {
24-
return output, fmt.Errorf("Failed to create random resource collection: %s", err.Error())
25-
}
26-
27-
// Configure terraform to use Remote State.
28-
err = aws.AssertS3BucketExists(ao.TfRemoteStateS3BucketRegion, ao.TfRemoteStateS3BucketName)
29-
if err != nil {
30-
return output, fmt.Errorf("Test failed because the S3 Bucket '%s' does not exist in the '%s' region.\n", ao.TfRemoteStateS3BucketName, ao.TfRemoteStateS3BucketRegion)
31-
}
32-
33-
terraform.ConfigureRemoteState(ao.TemplatePath, ao.TfRemoteStateS3BucketName, ao.generateTfStateFileName(rand), ao.TfRemoteStateS3BucketRegion, logger)
34-
35-
// TERRAFORM APPLY
36-
// Download all the Terraform modules
37-
logger.Println("Running terraform get...")
38-
err = terraform.Get(ao.TemplatePath, logger)
39-
if err != nil {
40-
return output, fmt.Errorf("Failed to call terraform get successfully: %s\n", err.Error())
41-
}
42-
43-
// Apply the Terraform template
44-
logger.Println("Running terraform apply...")
45-
46-
defer destroyHelper(ao, ao.generateTfStateFileName(rand))
47-
if ao.AttemptTerraformRetry {
48-
output, err = terraform.ApplyAndGetOutputWithRetry(ao.TemplatePath, ao.Vars, logger)
49-
} else {
50-
output, err = terraform.ApplyAndGetOutput(ao.TemplatePath, ao.Vars, logger)
51-
}
52-
if err != nil {
53-
return output, fmt.Errorf("Failed to terraform apply: %s\n", err.Error())
54-
}
55-
56-
return output, nil
5+
defer destroyHelper(ao, ao.getTfStateFileName())
6+
return Apply(ao)
577
}

apply_options.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package terratest
22

33
// The options to be passed into any terratest.Apply or Destroy function
44
type ApplyOptions struct {
5+
UniqueId string // A unique identifier for this terraform run.
56
TestName string // the name of the test to run, for logging purposes.
67
TemplatePath string // the relative or absolute path to the terraform template to be applied.
78
Vars map[string]string // the vars to pass to the terraform template.
89
AttemptTerraformRetry bool // if true, if a known error message occurs, automatically attempt a retry.
10+
RetryableTerraformErrors map[string]string // a map of error messages which we expect on this template and which should invoke a second terraform apply, along with an additional message offering details on this error text.
911
TfRemoteStateS3BucketName string // S3 bucket name where terraform.tfstate files should be stored for running any terraform runs. Bucket should already exist.
1012
TfRemoteStateS3BucketRegion string // AWS Region where the TfRemoteStateS3BucketName exists.
1113
}
@@ -20,6 +22,6 @@ func NewApplyOptions() *ApplyOptions {
2022

2123
// generateTfStateFileName creates a path and filename used to reference a terraform tfstate file. E.g. this is
2224
// useful with S3 for deciding where the tfstate file should be within a given bucket.
23-
func (ao *ApplyOptions) generateTfStateFileName(r *RandomResourceCollection) string {
24-
return r.UniqueId + "/terraform.tfstate"
25+
func (ao *ApplyOptions) getTfStateFileName() string {
26+
return ao.UniqueId + "/terraform.tfstate"
2527
}

aws/region.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import (
99
"github.com/aws/aws-sdk-go/service/ec2"
1010
)
1111

12-
func GetForbiddenRegions() []string {
12+
func GetGloballyForbiddenRegions() []string {
1313
return []string{
1414
"us-west-2",
1515
}
1616
}
1717

18-
// Get a randomly chosen AWS region
19-
func GetRandomRegion() string {
18+
// Get a randomly chosen AWS region that's not in the forbiddenRegions list
19+
func GetRandomRegion(forbiddenRegions []string) string {
2020

2121
allRegions := []string{
2222
"us-east-1",
@@ -40,7 +40,8 @@ func GetRandomRegion() string {
4040
randomIndex = util.Random(0,len(allRegions))
4141
randomIndexIsValid = true
4242

43-
for _, forbiddenRegion := range GetForbiddenRegions() {
43+
// The ... allows append to be used to concatenate two slices
44+
for _, forbiddenRegion := range append(GetGloballyForbiddenRegions(), forbiddenRegions...) {
4445
if forbiddenRegion == allRegions[randomIndex] {
4546
randomIndexIsValid = false
4647
}

aws/region_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ func TestGetRandomRegionExcludesForbiddenRegions(t *testing.T) {
88
t.Parallel()
99

1010
for i := 0; i < 1000; i++ {
11-
randomRegion := GetRandomRegion()
12-
for _, forbiddenRegion := range GetForbiddenRegions() {
11+
randomRegion := GetRandomRegion(nil)
12+
for _, forbiddenRegion := range GetGloballyForbiddenRegions() {
1313
if forbiddenRegion == randomRegion {
1414
t.Fatalf("Returned a forbidden AWS Region: %s", forbiddenRegion)
1515
}

aws/s3_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func TestCreateAndDestroyS3Bucket(t *testing.T) {
1515
logger := log.NewLogger("TestCreateAndDestroyS3Bucket")
1616

1717
// SETUP
18-
region := GetRandomRegion()
18+
region := GetRandomRegion(nil)
1919
id := util.UniqueId()
2020
logger.Printf("Random values selected. Region = %s, Id = %s\n", region, id)
2121

@@ -32,7 +32,7 @@ func TestAssertS3BucketExistsNoFalseNegative(t *testing.T) {
3232
logger := log.NewLogger("TestAssertS3BucketExists")
3333

3434
// SETUP
35-
region := GetRandomRegion()
35+
region := GetRandomRegion(nil)
3636
s3BucketName := "gruntwork-terratest-" + strings.ToLower(util.UniqueId())
3737
logger.Printf("Random values selected. Region = %s, s3BucketName = %s\n", region, s3BucketName)
3838

@@ -54,7 +54,7 @@ func TestAssertS3BucketExistsNoFalsePositive(t *testing.T) {
5454
logger := log.NewLogger("TestAssertS3BucketExists")
5555

5656
// SETUP
57-
region := GetRandomRegion()
57+
region := GetRandomRegion(nil)
5858
s3BucketName := "gruntwork-terratest-" + strings.ToLower(util.UniqueId())
5959
logger.Printf("Random values selected. Region = %s, s3BucketName = %s\n", region, s3BucketName)
6060

destroy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func Destroy(ao *ApplyOptions, rand *RandomResourceCollection) (string, error) {
1717
}
1818

1919
logger.Println("Running terraform destroy...")
20-
output, err := destroyHelper(ao, ao.generateTfStateFileName(rand))
20+
output, err := destroyHelper(ao, ao.getTfStateFileName())
2121
if err != nil {
2222
return output, fmt.Errorf("Failed to terraform destroy: %s", err.Error())
2323
}

glide.lock

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

terraform/apply.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,30 @@ func ApplyAndGetOutput(terraformPath string, vars map[string]string, logger *log
3333

3434
// Regrettably Terraform has many bugs. Often, just re-running terraform apply will resolve the issue.
3535
// This function declares which Terraform error messages warrant an automatic retry and does the retry.
36-
func ApplyAndGetOutputWithRetry(terraformPath string, vars map[string]string, logger *log.Logger) (string, error) {
36+
func ApplyAndGetOutputWithRetry(terraformPath string, vars map[string]string, errors map[string]string, logger *log.Logger) (string, error) {
3737
output, err := ApplyAndGetOutput(terraformPath, vars, logger)
3838
if err != nil {
3939
logger.Printf("Terraform apply failed with error: %s\n", err.Error())
4040

41-
// Check for all Terraform errors
41+
// Check for terraform errors that apply to all terraform templates.
4242
if strings.Contains(output, TF_ERROR_DIFFS_DIDNT_MATCH_DURING_APPLY) {
4343
logger.Printf("Terraform apply failed with the error '%s'. %s\n", TF_ERROR_DIFFS_DIDNT_MATCH_DURING_APPLY, TF_ERROR_DIFFS_DIDNT_MATCH_DURING_APPLY_MSG)
44-
return ApplyAndGetOutput(terraformPath, vars, logger)
45-
} else if strings.Contains(output, TF_ERROR_EIP_DOES_NOT_HAVE_ATTRIBUTE_ID) {
46-
logger.Printf("Terraform apply failed with the error '%s'. %s\n", TF_ERROR_EIP_DOES_NOT_HAVE_ATTRIBUTE_ID, TF_ERROR_EIP_DOES_NOT_HAVE_ATTRIBUTE_ID_MSG)
47-
return ApplyAndGetOutput(terraformPath, vars, logger)
48-
} else {
49-
return output, err
44+
retryOutput, err := ApplyAndGetOutput(terraformPath, vars, logger)
45+
return output + "**TERRAFORM-RETRY**\n" + retryOutput, err
5046
}
47+
48+
// Check for terraform errors that are specific to this template.
49+
for errorText, errorTextMsg := range errors {
50+
if strings.Contains(output, errorText) {
51+
logger.Printf("Terraform apply failed with the error '%s' but this error was expected and warrants a terraform apply retry. Further details: %s\n", errorText, errorTextMsg)
52+
retryOutput, err := ApplyAndGetOutput(terraformPath, vars, logger)
53+
return output + "**TERRAFORM-RETRY**\n" + retryOutput, err
54+
55+
}
56+
}
57+
58+
logger.Printf("Terraform failed with an error we didn't expect: %s", err.Error())
59+
return output, err
5160
}
5261

5362
return output, nil

0 commit comments

Comments
 (0)