provider/aws: Add Sweeper setup, Sweepers for DB Option Group, Key Pair (#14773)

* provider/aws: Add Sweeper setup, Sweepers for DB Option Group, Key Pair

* provider/google: Add sweeper for any leaked databases
* more recursion and added LC sweeper, to test out the Dependency path

* implement a dependency example

* implement sweep-run flag to filter runs

* stub a test for TestMain

* test for multiple -sweep-run list
This commit is contained in:
Clint 2017-06-06 10:34:17 -05:00 committed by GitHub
parent 3d1e60b504
commit 372a80bc42
9 changed files with 705 additions and 0 deletions

View File

@ -0,0 +1,37 @@
package aws
import (
"fmt"
"os"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestMain(m *testing.M) {
resource.TestMain(m)
}
// sharedClientForRegion returns a common AWSClient setup needed for the sweeper
// functions for a given region
func sharedClientForRegion(region string) (interface{}, error) {
if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
return nil, fmt.Errorf("empty AWS_ACCESS_KEY_ID")
}
if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
return nil, fmt.Errorf("empty AWS_SECRET_ACCESS_KEY")
}
conf := &Config{
Region: region,
}
// configures a default client for the region, using the above env vars
client, err := conf.Client()
if err != nil {
return nil, fmt.Errorf("error getting AWS client")
}
return client, nil
}

View File

@ -3,11 +3,13 @@ package aws
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"reflect" "reflect"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"testing" "testing"
"time"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
@ -18,6 +20,72 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func init() {
resource.AddTestSweepers("aws_autoscaling_group", &resource.Sweeper{
Name: "aws_autoscaling_group",
F: testSweepAutoscalingGroups,
})
}
func testSweepAutoscalingGroups(region string) error {
client, err := sharedClientForRegion(region)
if err != nil {
return fmt.Errorf("error getting client: %s", err)
}
conn := client.(*AWSClient).autoscalingconn
resp, err := conn.DescribeAutoScalingGroups(&autoscaling.DescribeAutoScalingGroupsInput{})
if err != nil {
return fmt.Errorf("Error retrieving launch configuration: %s", err)
}
if len(resp.AutoScalingGroups) == 0 {
log.Print("[DEBUG] No aws autoscaling groups to sweep")
return nil
}
for _, asg := range resp.AutoScalingGroups {
var testOptGroup bool
for _, testName := range []string{"foobar", "terraform-"} {
if strings.HasPrefix(*asg.AutoScalingGroupName, testName) {
testOptGroup = true
}
}
if !testOptGroup {
continue
}
deleteopts := autoscaling.DeleteAutoScalingGroupInput{
AutoScalingGroupName: asg.AutoScalingGroupName,
ForceDelete: aws.Bool(true),
}
err = resource.Retry(5*time.Minute, func() *resource.RetryError {
if _, err := conn.DeleteAutoScalingGroup(&deleteopts); err != nil {
if awserr, ok := err.(awserr.Error); ok {
switch awserr.Code() {
case "InvalidGroup.NotFound":
return nil
case "ResourceInUse", "ScalingActivityInProgress":
return resource.RetryableError(awserr)
}
}
// Didn't recognize the error, so shouldn't retry.
return resource.NonRetryableError(err)
}
// Successful delete
return nil
})
if err != nil {
return err
}
}
return nil
}
func TestAccAWSAutoScalingGroup_basic(t *testing.T) { func TestAccAWSAutoScalingGroup_basic(t *testing.T) {
var group autoscaling.Group var group autoscaling.Group
var lc autoscaling.LaunchConfiguration var lc autoscaling.LaunchConfiguration

View File

@ -2,8 +2,11 @@ package aws
import ( import (
"fmt" "fmt"
"log"
"regexp" "regexp"
"strings"
"testing" "testing"
"time"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
@ -13,6 +16,64 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func init() {
resource.AddTestSweepers("aws_db_option_group", &resource.Sweeper{
Name: "aws_db_option_group",
F: testSweepDbOptionGroups,
})
}
func testSweepDbOptionGroups(region string) error {
client, err := sharedClientForRegion(region)
if err != nil {
return fmt.Errorf("error getting client: %s", err)
}
conn := client.(*AWSClient).rdsconn
opts := rds.DescribeOptionGroupsInput{}
resp, err := conn.DescribeOptionGroups(&opts)
if err != nil {
return fmt.Errorf("error describing DB Option Groups in Sweeper: %s", err)
}
for _, og := range resp.OptionGroupsList {
var testOptGroup bool
for _, testName := range []string{"option-group-test-terraform-", "tf-test"} {
if strings.HasPrefix(*og.OptionGroupName, testName) {
testOptGroup = true
}
}
if !testOptGroup {
continue
}
deleteOpts := &rds.DeleteOptionGroupInput{
OptionGroupName: og.OptionGroupName,
}
ret := resource.Retry(1*time.Minute, func() *resource.RetryError {
_, err := conn.DeleteOptionGroup(deleteOpts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "InvalidOptionGroupStateFault" {
log.Printf("[DEBUG] AWS believes the RDS Option Group is still in use, retrying")
return resource.RetryableError(awsErr)
}
}
return resource.NonRetryableError(err)
}
return nil
})
if ret != nil {
return fmt.Errorf("Error Deleting DB Option Group (%s) in Sweeper: %s", *og.OptionGroupName, ret)
}
}
return nil
}
func TestAccAWSDBOptionGroup_basic(t *testing.T) { func TestAccAWSDBOptionGroup_basic(t *testing.T) {
var v rds.OptionGroup var v rds.OptionGroup
rName := fmt.Sprintf("option-group-test-terraform-%s", acctest.RandString(5)) rName := fmt.Sprintf("option-group-test-terraform-%s", acctest.RandString(5))

View File

@ -2,6 +2,7 @@ package aws
import ( import (
"fmt" "fmt"
"log"
"strings" "strings"
"testing" "testing"
@ -12,6 +13,47 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func init() {
resource.AddTestSweepers("aws_key_pair", &resource.Sweeper{
Name: "aws_key_pair",
F: testSweepKeyPairs,
})
}
func testSweepKeyPairs(region string) error {
client, err := sharedClientForRegion(region)
if err != nil {
return fmt.Errorf("error getting client: %s", err)
}
ec2conn := client.(*AWSClient).ec2conn
log.Printf("Destroying the tmp keys in (%s)", client.(*AWSClient).region)
resp, err := ec2conn.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{
Filters: []*ec2.Filter{
&ec2.Filter{
Name: aws.String("key-name"),
Values: []*string{aws.String("tmp-key*")},
},
},
})
if err != nil {
return fmt.Errorf("Error describing key pairs in Sweeper: %s", err)
}
keyPairs := resp.KeyPairs
for _, d := range keyPairs {
_, err := ec2conn.DeleteKeyPair(&ec2.DeleteKeyPairInput{
KeyName: d.KeyName,
})
if err != nil {
return fmt.Errorf("Error deleting key pairs in Sweeper: %s", err)
}
}
return nil
}
func TestAccAWSKeyPair_basic(t *testing.T) { func TestAccAWSKeyPair_basic(t *testing.T) {
var conf ec2.KeyPairInfo var conf ec2.KeyPairInfo

View File

@ -2,6 +2,7 @@ package aws
import ( import (
"fmt" "fmt"
"log"
"math/rand" "math/rand"
"strings" "strings"
"testing" "testing"
@ -16,6 +17,61 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func init() {
resource.AddTestSweepers("aws_launch_configuration", &resource.Sweeper{
Name: "aws_launch_configuration",
Dependencies: []string{"aws_autoscaling_group"},
F: testSweepLaunchConfigurations,
})
}
func testSweepLaunchConfigurations(region string) error {
client, err := sharedClientForRegion(region)
if err != nil {
return fmt.Errorf("error getting client: %s", err)
}
autoscalingconn := client.(*AWSClient).autoscalingconn
resp, err := autoscalingconn.DescribeLaunchConfigurations(&autoscaling.DescribeLaunchConfigurationsInput{})
if err != nil {
return fmt.Errorf("Error retrieving launch configuration: %s", err)
}
if len(resp.LaunchConfigurations) == 0 {
log.Print("[DEBUG] No aws launch configurations to sweep")
return nil
}
for _, lc := range resp.LaunchConfigurations {
var testOptGroup bool
for _, testName := range []string{"terraform-", "foobar"} {
if strings.HasPrefix(*lc.LaunchConfigurationName, testName) {
testOptGroup = true
}
}
if !testOptGroup {
continue
}
_, err := autoscalingconn.DeleteLaunchConfiguration(
&autoscaling.DeleteLaunchConfigurationInput{
LaunchConfigurationName: lc.LaunchConfigurationName,
})
if err != nil {
autoscalingerr, ok := err.(awserr.Error)
if ok && (autoscalingerr.Code() == "InvalidConfiguration.NotFound" || autoscalingerr.Code() == "ValidationError") {
log.Printf("[DEBUG] Launch configuration (%s) not found", *lc.LaunchConfigurationName)
return nil
}
return err
}
}
return nil
}
func TestAccAWSLaunchConfiguration_basic(t *testing.T) { func TestAccAWSLaunchConfiguration_basic(t *testing.T) {
var conf autoscaling.LaunchConfiguration var conf autoscaling.LaunchConfiguration

View File

@ -0,0 +1,35 @@
package google
import (
"fmt"
"os"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestMain(m *testing.M) {
resource.TestMain(m)
}
// sharedConfigForRegion returns a common config setup needed for the sweeper
// functions for a given region
func sharedConfigForRegion(region string) (*Config, error) {
project := os.Getenv("GOOGLE_PROJECT")
if project == "" {
return nil, fmt.Errorf("empty GOOGLE_PROJECT")
}
creds := os.Getenv("GOOGLE_CREDENTIALS")
if creds == "" {
return nil, fmt.Errorf("empty GOOGLE_CREDENTIALS")
}
conf := &Config{
Credentials: creds,
Region: region,
Project: project,
}
return conf, nil
}

View File

@ -9,6 +9,7 @@ package google
import ( import (
"fmt" "fmt"
"log"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -20,6 +21,105 @@ import (
"google.golang.org/api/sqladmin/v1beta4" "google.golang.org/api/sqladmin/v1beta4"
) )
func init() {
resource.AddTestSweepers("gcp_sql_db_instance", &resource.Sweeper{
Name: "gcp_sql_db_instance",
F: testSweepDatabases,
})
}
func testSweepDatabases(region string) error {
config, err := sharedConfigForRegion(region)
if err != nil {
return fmt.Errorf("error getting shared config for region: %s", err)
}
err = config.loadAndValidate()
if err != nil {
log.Fatalf("error loading: %s", err)
}
found, err := config.clientSqlAdmin.Instances.List(config.Project).Do()
if err != nil {
log.Fatalf("error listing databases: %s", err)
}
if len(found.Items) == 0 {
log.Printf("No databases found")
return nil
}
for _, d := range found.Items {
var testDbInstance bool
for _, testName := range []string{"tf-lw-", "sqldatabasetest"} {
// only destroy instances we know to fit our test naming pattern
if strings.HasPrefix(d.Name, testName) {
testDbInstance = true
}
}
if !testDbInstance {
continue
}
log.Printf("Destroying SQL Instance (%s)", d.Name)
// replicas need to be stopped and destroyed before destroying a master
// instance. The ordering slice tracks replica databases for a given master
// and we call destroy on them before destroying the master
var ordering []string
for _, replicaName := range d.ReplicaNames {
// need to stop replication before being able to destroy a database
op, err := config.clientSqlAdmin.Instances.StopReplica(config.Project, replicaName).Do()
if err != nil {
return fmt.Errorf("error, failed to stop replica instance (%s) for instance (%s): %s", replicaName, d.Name, err)
}
err = sqladminOperationWait(config, op, "Stop Replica")
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
log.Printf("Replication operation not found")
} else {
return err
}
}
ordering = append(ordering, replicaName)
}
// ordering has a list of replicas (or none), now add the primary to the end
ordering = append(ordering, d.Name)
for _, db := range ordering {
// destroy instances, replicas first
op, err := config.clientSqlAdmin.Instances.Delete(config.Project, db).Do()
if err != nil {
if strings.Contains(err.Error(), "409") {
// the GCP api can return a 409 error after the delete operation
// reaches a successful end
log.Printf("Operation not found, got 409 response")
continue
}
return fmt.Errorf("Error, failed to delete instance %s: %s", db, err)
}
err = sqladminOperationWait(config, op, "Delete Instance")
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
log.Printf("SQL instance not found")
continue
}
return err
}
}
}
return nil
}
func TestAccGoogleSqlDatabaseInstance_basic(t *testing.T) { func TestAccGoogleSqlDatabaseInstance_basic(t *testing.T) {
var instance sqladmin.DatabaseInstance var instance sqladmin.DatabaseInstance
databaseID := acctest.RandInt() databaseID := acctest.RandInt()

View File

@ -1,6 +1,7 @@
package resource package resource
import ( import (
"flag"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -20,6 +21,153 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
// flagSweep is a flag available when running tests on the command line. It
// contains a comma seperated list of regions to for the sweeper functions to
// run in. This flag bypasses the normal Test path and instead runs functions designed to
// clean up any leaked resources a testing environment could have created. It is
// a best effort attempt, and relies on Provider authors to implement "Sweeper"
// methods for resources.
// Adding Sweeper methods with AddTestSweepers will
// construct a list of sweeper funcs to be called here. We iterate through
// regions provided by the sweep flag, and for each region we iterate through the
// tests, and exit on any errors. At time of writing, sweepers are ran
// sequentially, however they can list dependencies to be ran first. We track
// the sweepers that have been ran, so as to not run a sweeper twice for a given
// region.
//
// WARNING:
// Sweepers are designed to be destructive. You should not use the -sweep flag
// in any environment that is not strictly a test environment. Resources will be
// destroyed.
var flagSweep = flag.String("sweep", "", "List of Regions to run available Sweepers")
var flagSweepRun = flag.String("sweep-run", "", "Comma seperated list of Sweeper Tests to run")
var sweeperFuncs map[string]*Sweeper
// map of sweepers that have ran, and the success/fail status based on any error
// raised
var sweeperRunList map[string]bool
// type SweeperFunc is a signature for a function that acts as a sweeper. It
// accepts a string for the region that the sweeper is to be ran in. This
// function must be able to construct a valid client for that region.
type SweeperFunc func(r string) error
type Sweeper struct {
// Name for sweeper. Must be unique to be ran by the Sweeper Runner
Name string
// Dependencies list the const names of other Sweeper functions that must be ran
// prior to running this Sweeper. This is an ordered list that will be invoked
// recursively at the helper/resource level
Dependencies []string
// Sweeper function that when invoked sweeps the Provider of specific
// resources
F SweeperFunc
}
func init() {
sweeperFuncs = make(map[string]*Sweeper)
}
// AddTestSweepers function adds a given name and Sweeper configuration
// pair to the internal sweeperFuncs map. Invoke this function to register a
// resource sweeper to be available for running when the -sweep flag is used
// with `go test`. Sweeper names must be unique to help ensure a given sweeper
// is only ran once per run.
func AddTestSweepers(name string, s *Sweeper) {
if _, ok := sweeperFuncs[name]; ok {
log.Fatalf("[ERR] Error adding (%s) to sweeperFuncs: function already exists in map", name)
}
sweeperFuncs[name] = s
}
func TestMain(m *testing.M) {
flag.Parse()
if *flagSweep != "" {
// parse flagSweep contents for regions to run
regions := strings.Split(*flagSweep, ",")
// get filtered list of sweepers to run based on sweep-run flag
sweepers := filterSweepers(*flagSweepRun, sweeperFuncs)
for _, region := range regions {
region = strings.TrimSpace(region)
// reset sweeperRunList for each region
sweeperRunList = map[string]bool{}
log.Printf("[DEBUG] Running Sweepers for region (%s):\n", region)
for _, sweeper := range sweepers {
if err := runSweeperWithRegion(region, sweeper); err != nil {
log.Fatalf("[ERR] error running (%s): %s", sweeper.Name, err)
}
}
log.Printf("Sweeper Tests ran:\n")
for s, _ := range sweeperRunList {
fmt.Printf("\t- %s\n", s)
}
}
} else {
os.Exit(m.Run())
}
}
// filterSweepers takes a comma seperated string listing the names of sweepers
// to be ran, and returns a filtered set from the list of all of sweepers to
// run based on the names given.
func filterSweepers(f string, source map[string]*Sweeper) map[string]*Sweeper {
filterSlice := strings.Split(strings.ToLower(f), ",")
if len(filterSlice) == 1 && filterSlice[0] == "" {
// if the filter slice is a single element of "" then no sweeper list was
// given, so just return the full list
return source
}
sweepers := make(map[string]*Sweeper)
for name, sweeper := range source {
for _, s := range filterSlice {
if strings.Contains(strings.ToLower(name), s) {
sweepers[name] = sweeper
}
}
}
return sweepers
}
// runSweeperWithRegion recieves a sweeper and a region, and recursively calls
// itself with that region for every dependency found for that sweeper. If there
// are no dependencies, invoke the contained sweeper fun with the region, and
// add the success/fail status to the sweeperRunList.
func runSweeperWithRegion(region string, s *Sweeper) error {
for _, dep := range s.Dependencies {
if depSweeper, ok := sweeperFuncs[dep]; ok {
log.Printf("[DEBUG] Sweeper (%s) has dependency (%s), running..", s.Name, dep)
if err := runSweeperWithRegion(region, depSweeper); err != nil {
return err
}
} else {
log.Printf("[DEBUG] Sweeper (%s) has dependency (%s), but that sweeper was not found", s.Name, dep)
}
}
if _, ok := sweeperRunList[s.Name]; ok {
log.Printf("[DEBUG] Sweeper (%s) already ran in region (%s)", s.Name, region)
return nil
}
runE := s.F(region)
if runE == nil {
sweeperRunList[s.Name] = true
} else {
sweeperRunList[s.Name] = false
}
return runE
}
const TestEnvVar = "TF_ACC" const TestEnvVar = "TF_ACC"
// TestProvider can be implemented by any ResourceProvider to provide custom // TestProvider can be implemented by any ResourceProvider to provide custom

View File

@ -2,9 +2,12 @@ package resource
import ( import (
"errors" "errors"
"flag"
"fmt" "fmt"
"os" "os"
"reflect"
"regexp" "regexp"
"sort"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -619,3 +622,158 @@ func testProvider() *terraform.MockResourceProvider {
const testConfigStr = ` const testConfigStr = `
resource "test_instance" "foo" {} resource "test_instance" "foo" {}
` `
func TestTest_Main(t *testing.T) {
flag.Parse()
if *flagSweep == "" {
// Tests for the TestMain method used for Sweepers will panic without the -sweep
// flag specified. Mock the value for now
*flagSweep = "us-east-1"
}
cases := []struct {
Name string
Sweepers map[string]*Sweeper
ExpectedRunList []string
SweepRun string
}{
{
Name: "normal",
Sweepers: map[string]*Sweeper{
"aws_dummy": &Sweeper{
Name: "aws_dummy",
F: mockSweeperFunc,
},
},
ExpectedRunList: []string{"aws_dummy"},
},
{
Name: "with dep",
Sweepers: map[string]*Sweeper{
"aws_dummy": &Sweeper{
Name: "aws_dummy",
F: mockSweeperFunc,
},
"aws_top": &Sweeper{
Name: "aws_top",
Dependencies: []string{"aws_sub"},
F: mockSweeperFunc,
},
"aws_sub": &Sweeper{
Name: "aws_sub",
F: mockSweeperFunc,
},
},
ExpectedRunList: []string{"aws_dummy", "aws_sub", "aws_top"},
},
{
Name: "with filter",
Sweepers: map[string]*Sweeper{
"aws_dummy": &Sweeper{
Name: "aws_dummy",
F: mockSweeperFunc,
},
"aws_top": &Sweeper{
Name: "aws_top",
Dependencies: []string{"aws_sub"},
F: mockSweeperFunc,
},
"aws_sub": &Sweeper{
Name: "aws_sub",
F: mockSweeperFunc,
},
},
ExpectedRunList: []string{"aws_dummy"},
SweepRun: "aws_dummy",
},
{
Name: "with two filters",
Sweepers: map[string]*Sweeper{
"aws_dummy": &Sweeper{
Name: "aws_dummy",
F: mockSweeperFunc,
},
"aws_top": &Sweeper{
Name: "aws_top",
Dependencies: []string{"aws_sub"},
F: mockSweeperFunc,
},
"aws_sub": &Sweeper{
Name: "aws_sub",
F: mockSweeperFunc,
},
},
ExpectedRunList: []string{"aws_dummy", "aws_sub"},
SweepRun: "aws_dummy,aws_sub",
},
{
Name: "with dep and filter",
Sweepers: map[string]*Sweeper{
"aws_dummy": &Sweeper{
Name: "aws_dummy",
F: mockSweeperFunc,
},
"aws_top": &Sweeper{
Name: "aws_top",
Dependencies: []string{"aws_sub"},
F: mockSweeperFunc,
},
"aws_sub": &Sweeper{
Name: "aws_sub",
F: mockSweeperFunc,
},
},
ExpectedRunList: []string{"aws_top", "aws_sub"},
SweepRun: "aws_top",
},
{
Name: "filter and none",
Sweepers: map[string]*Sweeper{
"aws_dummy": &Sweeper{
Name: "aws_dummy",
F: mockSweeperFunc,
},
"aws_top": &Sweeper{
Name: "aws_top",
Dependencies: []string{"aws_sub"},
F: mockSweeperFunc,
},
"aws_sub": &Sweeper{
Name: "aws_sub",
F: mockSweeperFunc,
},
},
SweepRun: "none",
},
}
for _, tc := range cases {
// reset sweepers
sweeperFuncs = map[string]*Sweeper{}
t.Run(tc.Name, func(t *testing.T) {
for n, s := range tc.Sweepers {
AddTestSweepers(n, s)
}
*flagSweepRun = tc.SweepRun
TestMain(&testing.M{})
// get list of tests ran from sweeperRunList keys
var keys []string
for k, _ := range sweeperRunList {
keys = append(keys, k)
}
sort.Strings(keys)
sort.Strings(tc.ExpectedRunList)
if !reflect.DeepEqual(keys, tc.ExpectedRunList) {
t.Fatalf("Expected keys mismatch, expected:\n%#v\ngot:\n%#v\n", tc.ExpectedRunList, keys)
}
})
}
}
func mockSweeperFunc(s string) error {
return nil
}