Skip to content

Commit

Permalink
Prevent parallel creation of VPCs in the same region to protect again…
Browse files Browse the repository at this point in the history
…st race conditions in IP range assignment.
  • Loading branch information
andrewsomething committed Jan 13, 2021
1 parent b5f0bf4 commit d2feb02
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 1 deletion.
5 changes: 5 additions & 0 deletions digitalocean/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ package digitalocean

import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/digitalocean/terraform-provider-digitalocean/internal/mutexkv"
)

// Global MutexKV
var mutexKV = mutexkv.NewMutexKV()

// Provider returns a schema.Provider for DigitalOcean.
func Provider() *schema.Provider {
p := &schema.Provider{
Expand Down
9 changes: 8 additions & 1 deletion digitalocean/resource_digitalocean_vpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ func resourceDigitalOceanVPC() *schema.Resource {
func resourceDigitalOceanVPCCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*CombinedConfig).godoClient()

region := d.Get("region").(string)
vpcRequest := &godo.VPCCreateRequest{
Name: d.Get("name").(string),
RegionSlug: d.Get("region").(string),
RegionSlug: region,
}

if v, ok := d.GetOk("description"); ok {
Expand All @@ -92,6 +93,12 @@ func resourceDigitalOceanVPCCreate(ctx context.Context, d *schema.ResourceData,
vpcRequest.IPRange = v.(string)
}

// Prevent parallel creation of VPCs in the same region to protect
// against race conditions in IP range assignment.
key := fmt.Sprintf("resource_digitalocean_vpc/%s", region)
mutexKV.Lock(key)
defer mutexKV.Unlock(key)

log.Printf("[DEBUG] VPC create request: %#v", vpcRequest)
vpc, _, err := client.VPCs.Create(context.Background(), vpcRequest)
if err != nil {
Expand Down
53 changes: 53 additions & 0 deletions internal/mutexkv/mutexkv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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.
//
// Originally from: https://github.com/hashicorp/terraform-plugin-sdk/blob/v1.12.0/helper/mutexkv/mutexkv.go
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),
}
}
67 changes: 67 additions & 0 deletions internal/mutexkv/mutexkv_test.go
Original file line number Diff line number Diff line change
@@ -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.")
}
}

0 comments on commit d2feb02

Please sign in to comment.