Merge pull request #3965 from hashicorp/b-aws-sg-rules-v2-race
provider/aws: serialize SG rule access to fix race condition
This commit is contained in:
commit
a211fc3469
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/hashcode"
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||||||
|
"github.com/hashicorp/terraform/helper/mutexkv"
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
|
||||||
|
@ -321,3 +322,6 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
|
||||||
|
|
||||||
return config.Client()
|
return config.Client()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a global MutexKV for use within this plugin.
|
||||||
|
var awsMutexKV = mutexkv.NewMutexKV()
|
||||||
|
|
|
@ -84,8 +84,11 @@ func resourceAwsSecurityGroupRule() *schema.Resource {
|
||||||
func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
conn := meta.(*AWSClient).ec2conn
|
conn := meta.(*AWSClient).ec2conn
|
||||||
sg_id := d.Get("security_group_id").(string)
|
sg_id := d.Get("security_group_id").(string)
|
||||||
sg, err := findResourceSecurityGroup(conn, sg_id)
|
|
||||||
|
|
||||||
|
awsMutexKV.Lock(sg_id)
|
||||||
|
defer awsMutexKV.Unlock(sg_id)
|
||||||
|
|
||||||
|
sg, err := findResourceSecurityGroup(conn, sg_id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -249,8 +252,11 @@ func resourceAwsSecurityGroupRuleRead(d *schema.ResourceData, meta interface{})
|
||||||
func resourceAwsSecurityGroupRuleDelete(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsSecurityGroupRuleDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
conn := meta.(*AWSClient).ec2conn
|
conn := meta.(*AWSClient).ec2conn
|
||||||
sg_id := d.Get("security_group_id").(string)
|
sg_id := d.Get("security_group_id").(string)
|
||||||
sg, err := findResourceSecurityGroup(conn, sg_id)
|
|
||||||
|
|
||||||
|
awsMutexKV.Lock(sg_id)
|
||||||
|
defer awsMutexKV.Unlock(sg_id)
|
||||||
|
|
||||||
|
sg, err := findResourceSecurityGroup(conn, sg_id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package aws
|
package aws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -339,7 +340,24 @@ func TestAccAWSSecurityGroupRule_PartialMatching_Source(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccAWSSecurityGroupRule_Race(t *testing.T) {
|
||||||
|
var group ec2.SecurityGroup
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckAWSSecurityGroupRuleDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccAWSSecurityGroupRuleRace,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckAWSSecurityGroupRuleExists("aws_security_group.race", &group),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAccCheckAWSSecurityGroupRuleDestroy(s *terraform.State) error {
|
func testAccCheckAWSSecurityGroupRuleDestroy(s *terraform.State) error {
|
||||||
|
@ -718,3 +736,41 @@ resource "aws_security_group_rule" "other_ingress" {
|
||||||
security_group_id = "${aws_security_group.web.id}"
|
security_group_id = "${aws_security_group.web.id}"
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
var testAccAWSSecurityGroupRuleRace = func() string {
|
||||||
|
var b bytes.Buffer
|
||||||
|
iterations := 50
|
||||||
|
b.WriteString(fmt.Sprintf(`
|
||||||
|
resource "aws_vpc" "default" {
|
||||||
|
cidr_block = "10.0.0.0/16"
|
||||||
|
tags { Name = "tf-sg-rule-race" }
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_security_group" "race" {
|
||||||
|
name = "tf-sg-rule-race-group-%d"
|
||||||
|
vpc_id = "${aws_vpc.default.id}"
|
||||||
|
}
|
||||||
|
`, genRandInt()))
|
||||||
|
for i := 1; i < iterations; i++ {
|
||||||
|
b.WriteString(fmt.Sprintf(`
|
||||||
|
resource "aws_security_group_rule" "ingress%d" {
|
||||||
|
security_group_id = "${aws_security_group.race.id}"
|
||||||
|
type = "ingress"
|
||||||
|
from_port = %d
|
||||||
|
to_port = %d
|
||||||
|
protocol = "tcp"
|
||||||
|
cidr_blocks = ["10.0.0.%d/32"]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_security_group_rule" "egress%d" {
|
||||||
|
security_group_id = "${aws_security_group.race.id}"
|
||||||
|
type = "egress"
|
||||||
|
from_port = %d
|
||||||
|
to_port = %d
|
||||||
|
protocol = "tcp"
|
||||||
|
cidr_blocks = ["10.0.0.%d/32"]
|
||||||
|
}
|
||||||
|
`, i, i, i, i, i, i, i, i))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}()
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package mutexkv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MutexKV is a simple key/value store for arbitrary mutexes. It can be used to
|
||||||
|
// serialize changes across arbitrary collaborators that share knowledge of the
|
||||||
|
// keys they must serialize on.
|
||||||
|
//
|
||||||
|
// The initial use case is to let aws_security_group_rule resources serialize
|
||||||
|
// their access to individual security groups based on SG ID.
|
||||||
|
type MutexKV struct {
|
||||||
|
lock sync.Mutex
|
||||||
|
store map[string]*sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locks the mutex for the given key. Caller is responsible for calling Unlock
|
||||||
|
// for the same key
|
||||||
|
func (m *MutexKV) Lock(key string) {
|
||||||
|
log.Printf("[DEBUG] Locking %q", key)
|
||||||
|
m.get(key).Lock()
|
||||||
|
log.Printf("[DEBUG] Locked %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock the mutex for the given key. Caller must have called Lock for the same key first
|
||||||
|
func (m *MutexKV) Unlock(key string) {
|
||||||
|
log.Printf("[DEBUG] Unlocking %q", key)
|
||||||
|
m.get(key).Unlock()
|
||||||
|
log.Printf("[DEBUG] Unlocked %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a mutex for the given key, no guarantee of its lock status
|
||||||
|
func (m *MutexKV) get(key string) *sync.Mutex {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
mutex, ok := m.store[key]
|
||||||
|
if !ok {
|
||||||
|
mutex = &sync.Mutex{}
|
||||||
|
m.store[key] = mutex
|
||||||
|
}
|
||||||
|
return mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a properly initalized MutexKV
|
||||||
|
func NewMutexKV() *MutexKV {
|
||||||
|
return &MutexKV{
|
||||||
|
store: make(map[string]*sync.Mutex),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package mutexkv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMutexKVLock(t *testing.T) {
|
||||||
|
mkv := NewMutexKV()
|
||||||
|
|
||||||
|
mkv.Lock("foo")
|
||||||
|
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
mkv.Lock("foo")
|
||||||
|
close(doneCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-doneCh:
|
||||||
|
t.Fatal("Second lock was able to be taken. This shouldn't happen.")
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMutexKVUnlock(t *testing.T) {
|
||||||
|
mkv := NewMutexKV()
|
||||||
|
|
||||||
|
mkv.Lock("foo")
|
||||||
|
mkv.Unlock("foo")
|
||||||
|
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
mkv.Lock("foo")
|
||||||
|
close(doneCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-doneCh:
|
||||||
|
// pass
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
t.Fatal("Second lock blocked after unlock. This shouldn't happen.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMutexKVDifferentKeys(t *testing.T) {
|
||||||
|
mkv := NewMutexKV()
|
||||||
|
|
||||||
|
mkv.Lock("foo")
|
||||||
|
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
mkv.Lock("bar")
|
||||||
|
close(doneCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-doneCh:
|
||||||
|
// pass
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
t.Fatal("Second lock on a different key blocked. This shouldn't happen.")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue