Skip to content

Commit

Permalink
helper/schema: Add configurable Timeouts (#12311)
Browse files Browse the repository at this point in the history
* helper/schema: Add custom Timeout block for resources

* refactor DefaultTimeout to suuport multiple types. Load meta in Refresh from Instance State

* update vpc but it probably wont last anyway

* refactor test into table test for more cases

* rename constant keys

* refactor configdecode

* remove VPC demo

* remove comments

* remove more comments

* refactor some

* rename timeKeys to timeoutKeys

* remove note

* documentation/resources: Document the Timeout block

* document timeouts

* have a test case that covers 'hours'

* restore a System default timeout of 20 minutes, instead of 0

* restore system default timeout of 20 minutes, refactor tests, add test method to handle system default

* rename timeout key constants

* test applying timeout to state

* refactor test

* Add resource Diff test

* clarify docs

* update to use constants
  • Loading branch information
catsby committed Mar 2, 2017
1 parent e5e37b0 commit 2fe5976
Show file tree
Hide file tree
Showing 11 changed files with 989 additions and 10 deletions.
14 changes: 10 additions & 4 deletions builtin/providers/aws/resource_aws_db_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ func resourceAwsDbInstance() *schema.Resource {
State: resourceAwsDbInstanceImport,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(40 * time.Minute),
Update: schema.DefaultTimeout(80 * time.Minute),
Delete: schema.DefaultTimeout(40 * time.Minute),
},

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Expand Down Expand Up @@ -480,7 +486,7 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
"maintenance", "renaming", "rebooting", "upgrading"},
Target: []string{"available"},
Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta),
Timeout: 40 * time.Minute,
Timeout: d.Timeout(schema.TimeoutCreate),
MinTimeout: 10 * time.Second,
Delay: 30 * time.Second, // Wait 30 secs before starting
}
Expand Down Expand Up @@ -638,7 +644,7 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
"maintenance", "renaming", "rebooting", "upgrading", "configuring-enhanced-monitoring"},
Target: []string{"available"},
Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta),
Timeout: 40 * time.Minute,
Timeout: d.Timeout(schema.TimeoutCreate),
MinTimeout: 10 * time.Second,
Delay: 30 * time.Second, // Wait 30 secs before starting
}
Expand Down Expand Up @@ -811,7 +817,7 @@ func resourceAwsDbInstanceDelete(d *schema.ResourceData, meta interface{}) error
"modifying", "deleting", "available"},
Target: []string{},
Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta),
Timeout: 40 * time.Minute,
Timeout: d.Timeout(schema.TimeoutDelete),
MinTimeout: 10 * time.Second,
Delay: 30 * time.Second, // Wait 30 secs before starting
}
Expand Down Expand Up @@ -978,7 +984,7 @@ func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error
"maintenance", "renaming", "rebooting", "upgrading", "configuring-enhanced-monitoring", "moving-to-vpc"},
Target: []string{"available"},
Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta),
Timeout: 80 * time.Minute,
Timeout: d.Timeout(schema.TimeoutUpdate),
MinTimeout: 10 * time.Second,
Delay: 30 * time.Second, // Wait 30 secs before starting
}
Expand Down
57 changes: 56 additions & 1 deletion helper/schema/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package schema
import (
"errors"
"fmt"
"log"
"strconv"

"github.com/hashicorp/terraform/terraform"
Expand Down Expand Up @@ -94,6 +95,15 @@ type Resource struct {
// This is a private interface for now, for use by DataSourceResourceShim,
// and not for general use. (But maybe later...)
deprecationMessage string

// Timeouts allow users to specify specific time durations in which an
// operation should time out, to allow them to extend an action to suit their
// usage. For example, a user may specify a large Creation timeout for their
// AWS RDS Instance due to it's size, or restoring from a snapshot.
// Resource implementors must enable Timeout support by adding the allowed
// actions (Create, Read, Update, Delete, Default) to the Resource struct, and
// accessing them in the matching methods.
Timeouts *ResourceTimeout
}

// See Resource documentation.
Expand Down Expand Up @@ -125,6 +135,18 @@ func (r *Resource) Apply(
return s, err
}

// Instance Diff shoould have the timeout info, need to copy it over to the
// ResourceData meta
rt := ResourceTimeout{}
if _, ok := d.Meta[TimeoutKey]; ok {
if err := rt.DiffDecode(d); err != nil {
log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
}
} else {
log.Printf("[DEBUG] No meta timeoutkey found in Apply()")
}
data.timeouts = &rt

if s == nil {
// The Terraform API dictates that this should never happen, but
// it doesn't hurt to be safe in this case.
Expand All @@ -150,6 +172,8 @@ func (r *Resource) Apply(

// Reset the data to be stateless since we just destroyed
data, err = schemaMap(r.Schema).Data(nil, d)
// data was reset, need to re-apply the parsed timeouts
data.timeouts = &rt
if err != nil {
return nil, err
}
Expand All @@ -176,7 +200,28 @@ func (r *Resource) Apply(
func (r *Resource) Diff(
s *terraform.InstanceState,
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
return schemaMap(r.Schema).Diff(s, c)

t := &ResourceTimeout{}
err := t.ConfigDecode(r, c)

if err != nil {
return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
}

instanceDiff, err := schemaMap(r.Schema).Diff(s, c)
if err != nil {
return instanceDiff, err
}

if instanceDiff != nil {
if err := t.DiffEncode(instanceDiff); err != nil {
log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
}
} else {
log.Printf("[DEBUG] Instance Diff is nil in Diff()")
}

return instanceDiff, err
}

// Validate validates the resource configuration against the schema.
Expand Down Expand Up @@ -226,10 +271,19 @@ func (r *Resource) Refresh(
return nil, nil
}

rt := ResourceTimeout{}
if _, ok := s.Meta[TimeoutKey]; ok {
if err := rt.StateDecode(s); err != nil {
log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
}
}

if r.Exists != nil {
// Make a copy of data so that if it is modified it doesn't
// affect our Read later.
data, err := schemaMap(r.Schema).Data(s, nil)
data.timeouts = &rt

if err != nil {
return s, err
}
Expand All @@ -252,6 +306,7 @@ func (r *Resource) Refresh(
}

data, err := schemaMap(r.Schema).Data(s, nil)
data.timeouts = &rt
if err != nil {
return s, err
}
Expand Down
47 changes: 42 additions & 5 deletions helper/schema/resource_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"reflect"
"strings"
"sync"
"time"

"github.com/hashicorp/terraform/terraform"
)
Expand All @@ -19,11 +20,12 @@ import (
// The most relevant methods to take a look at are Get, Set, and Partial.
type ResourceData struct {
// Settable (internally)
schema map[string]*Schema
config *terraform.ResourceConfig
state *terraform.InstanceState
diff *terraform.InstanceDiff
meta map[string]interface{}
schema map[string]*Schema
config *terraform.ResourceConfig
state *terraform.InstanceState
diff *terraform.InstanceDiff
meta map[string]interface{}
timeouts *ResourceTimeout

// Don't set
multiReader *MultiLevelFieldReader
Expand Down Expand Up @@ -250,6 +252,12 @@ func (d *ResourceData) State() *terraform.InstanceState {
return nil
}

if d.timeouts != nil {
if err := d.timeouts.StateEncode(&result); err != nil {
log.Printf("[ERR] Error encoding Timeout meta to Instance State: %s", err)
}
}

// Look for a magic key in the schema that determines we skip the
// integrity check of fields existing in the schema, allowing dynamic
// keys to be created.
Expand Down Expand Up @@ -331,6 +339,35 @@ func (d *ResourceData) State() *terraform.InstanceState {
return &result
}

// Timeout returns the data for the given timeout key
// Returns a duration of 20 minutes for any key not found, or not found and no default.
func (d *ResourceData) Timeout(key string) time.Duration {
key = strings.ToLower(key)

var timeout *time.Duration
switch key {
case TimeoutCreate:
timeout = d.timeouts.Create
case TimeoutRead:
timeout = d.timeouts.Read
case TimeoutUpdate:
timeout = d.timeouts.Update
case TimeoutDelete:
timeout = d.timeouts.Delete
}

if timeout != nil {
return *timeout
}

if d.timeouts.Default != nil {
return *d.timeouts.Default
}

// Return system default of 20 minutes
return 20 * time.Minute
}

func (d *ResourceData) init() {
// Initialize the field that will store our new state
var copyState terraform.InstanceState
Expand Down
92 changes: 92 additions & 0 deletions helper/schema/resource_data_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package schema

import (
"fmt"
"math"
"reflect"
"testing"
"time"

"github.com/hashicorp/terraform/terraform"
)
Expand Down Expand Up @@ -1080,6 +1082,78 @@ func TestResourceDataGetOk(t *testing.T) {
}
}

func TestResourceDataTimeout(t *testing.T) {
cases := []struct {
Name string
Rd *ResourceData
Expected *ResourceTimeout
}{
{
Name: "Basic example default",
Rd: &ResourceData{timeouts: timeoutForValues(10, 3, 0, 15, 0)},
Expected: expectedTimeoutForValues(10, 3, 0, 15, 0),
},
{
Name: "Resource and config match update, create",
Rd: &ResourceData{timeouts: timeoutForValues(10, 0, 3, 0, 0)},
Expected: expectedTimeoutForValues(10, 0, 3, 0, 0),
},
{
Name: "Resource provides default",
Rd: &ResourceData{timeouts: timeoutForValues(10, 0, 0, 0, 7)},
Expected: expectedTimeoutForValues(10, 7, 7, 7, 7),
},
{
Name: "Resource provides default and delete",
Rd: &ResourceData{timeouts: timeoutForValues(10, 0, 0, 15, 7)},
Expected: expectedTimeoutForValues(10, 7, 7, 15, 7),
},
{
Name: "Resource provides default, config overwrites other values",
Rd: &ResourceData{timeouts: timeoutForValues(10, 3, 0, 0, 13)},
Expected: expectedTimeoutForValues(10, 3, 13, 13, 13),
},
}

keys := timeoutKeys()
for i, c := range cases {
t.Run(fmt.Sprintf("%d-%s", i, c.Name), func(t *testing.T) {

for _, k := range keys {
got := c.Rd.Timeout(k)
var ex *time.Duration
switch k {
case TimeoutCreate:
ex = c.Expected.Create
case TimeoutRead:
ex = c.Expected.Read
case TimeoutUpdate:
ex = c.Expected.Update
case TimeoutDelete:
ex = c.Expected.Delete
case TimeoutDefault:
ex = c.Expected.Default
}

if got > 0 && ex == nil {
t.Fatalf("Unexpected value in (%s), case %d check 1:\n\texpected: %#v\n\tgot: %#v", k, i, ex, got)
}
if got == 0 && ex != nil {
t.Fatalf("Unexpected value in (%s), case %d check 2:\n\texpected: %#v\n\tgot: %#v", k, i, *ex, got)
}

// confirm values
if ex != nil {
if got != *ex {
t.Fatalf("Timeout %s case (%d) expected (%#v), got (%#v)", k, i, *ex, got)
}
}
}

})
}
}

func TestResourceDataHasChange(t *testing.T) {
cases := []struct {
Schema map[string]*Schema
Expand Down Expand Up @@ -3081,6 +3155,24 @@ func TestResourceDataSetConnInfo(t *testing.T) {
}
}

func TestResourceDataSetMeta_Timeouts(t *testing.T) {
d := &ResourceData{}
d.SetId("foo")

rt := ResourceTimeout{
Create: DefaultTimeout(7 * time.Minute),
}

d.timeouts = &rt

expected := expectedForValues(7, 0, 0, 0, 0)

actual := d.State()
if !reflect.DeepEqual(actual.Meta[TimeoutKey], expected) {
t.Fatalf("Bad Meta_timeout match:\n\texpected: %#v\n\tgot: %#v", expected, actual.Meta[TimeoutKey])
}
}

func TestResourceDataSetId(t *testing.T) {
d := &ResourceData{}
d.SetId("foo")
Expand Down
Loading

0 comments on commit 2fe5976

Please sign in to comment.