diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go
index fca7a8227..0d1d60362 100644
--- a/builtin/providers/aws/resource_aws_db_instance.go
+++ b/builtin/providers/aws/resource_aws_db_instance.go
@@ -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,
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
diff --git a/helper/schema/resource.go b/helper/schema/resource.go
index 51ddb14dc..c8105588c 100644
--- a/helper/schema/resource.go
+++ b/helper/schema/resource.go
@@ -3,6 +3,7 @@ package schema
import (
"errors"
"fmt"
+ "log"
"strconv"
"github.com/hashicorp/terraform/terraform"
@@ -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.
@@ -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.
@@ -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
}
@@ -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.
@@ -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
}
@@ -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
}
diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go
index f20a6b386..b2bc8f6c7 100644
--- a/helper/schema/resource_data.go
+++ b/helper/schema/resource_data.go
@@ -5,6 +5,7 @@ import (
"reflect"
"strings"
"sync"
+ "time"
"github.com/hashicorp/terraform/terraform"
)
@@ -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
@@ -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.
@@ -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
diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go
index 40646dd7d..615a0f7f7 100644
--- a/helper/schema/resource_data_test.go
+++ b/helper/schema/resource_data_test.go
@@ -1,9 +1,11 @@
package schema
import (
+ "fmt"
"math"
"reflect"
"testing"
+ "time"
"github.com/hashicorp/terraform/terraform"
)
@@ -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
@@ -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")
diff --git a/helper/schema/resource_test.go b/helper/schema/resource_test.go
index 89cd8a46a..f98aa5c43 100644
--- a/helper/schema/resource_test.go
+++ b/helper/schema/resource_test.go
@@ -5,7 +5,9 @@ import (
"reflect"
"strconv"
"testing"
+ "time"
+ "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
@@ -62,6 +64,138 @@ func TestResourceApply_create(t *testing.T) {
}
}
+func TestResourceApply_Timeout_state(t *testing.T) {
+ r := &Resource{
+ SchemaVersion: 2,
+ Schema: map[string]*Schema{
+ "foo": &Schema{
+ Type: TypeInt,
+ Optional: true,
+ },
+ },
+ Timeouts: &ResourceTimeout{
+ Create: DefaultTimeout(40 * time.Minute),
+ Update: DefaultTimeout(80 * time.Minute),
+ Delete: DefaultTimeout(40 * time.Minute),
+ },
+ }
+
+ called := false
+ r.Create = func(d *ResourceData, m interface{}) error {
+ called = true
+ d.SetId("foo")
+ return nil
+ }
+
+ var s *terraform.InstanceState = nil
+
+ d := &terraform.InstanceDiff{
+ Attributes: map[string]*terraform.ResourceAttrDiff{
+ "foo": &terraform.ResourceAttrDiff{
+ New: "42",
+ },
+ },
+ }
+
+ diffTimeout := &ResourceTimeout{
+ Create: DefaultTimeout(40 * time.Minute),
+ Update: DefaultTimeout(80 * time.Minute),
+ Delete: DefaultTimeout(40 * time.Minute),
+ }
+
+ if err := diffTimeout.DiffEncode(d); err != nil {
+ t.Fatalf("Error encoding timeout to diff: %s", err)
+ }
+
+ actual, err := r.Apply(s, d, nil)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ if !called {
+ t.Fatal("not called")
+ }
+
+ expected := &terraform.InstanceState{
+ ID: "foo",
+ Attributes: map[string]string{
+ "id": "foo",
+ "foo": "42",
+ },
+ Meta: map[string]interface{}{
+ "schema_version": "2",
+ TimeoutKey: expectedForValues(40, 0, 80, 40, 0),
+ },
+ }
+
+ if !reflect.DeepEqual(actual, expected) {
+ t.Fatalf("Not equal in Timeout State:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta)
+ }
+}
+
+func TestResourceDiff_Timeout_diff(t *testing.T) {
+ r := &Resource{
+ Schema: map[string]*Schema{
+ "foo": &Schema{
+ Type: TypeInt,
+ Optional: true,
+ },
+ },
+ Timeouts: &ResourceTimeout{
+ Create: DefaultTimeout(40 * time.Minute),
+ Update: DefaultTimeout(80 * time.Minute),
+ Delete: DefaultTimeout(40 * time.Minute),
+ },
+ }
+
+ r.Create = func(d *ResourceData, m interface{}) error {
+ d.SetId("foo")
+ return nil
+ }
+
+ raw, err := config.NewRawConfig(
+ map[string]interface{}{
+ "foo": 42,
+ "timeout": []map[string]interface{}{
+ map[string]interface{}{
+ "create": "2h",
+ }},
+ })
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ var s *terraform.InstanceState = nil
+ conf := terraform.NewResourceConfig(raw)
+
+ actual, err := r.Diff(s, conf)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ expected := &terraform.InstanceDiff{
+ Attributes: map[string]*terraform.ResourceAttrDiff{
+ "foo": &terraform.ResourceAttrDiff{
+ New: "42",
+ },
+ },
+ }
+
+ diffTimeout := &ResourceTimeout{
+ Create: DefaultTimeout(120 * time.Minute),
+ Update: DefaultTimeout(80 * time.Minute),
+ Delete: DefaultTimeout(40 * time.Minute),
+ }
+
+ if err := diffTimeout.DiffEncode(expected); err != nil {
+ t.Fatalf("Error encoding timeout to diff: %s", err)
+ }
+
+ if !reflect.DeepEqual(actual, expected) {
+ t.Fatalf("Not equal in Timeout Diff:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta)
+ }
+}
+
func TestResourceApply_destroy(t *testing.T) {
r := &Resource{
Schema: map[string]*Schema{
diff --git a/helper/schema/resource_timeout.go b/helper/schema/resource_timeout.go
new file mode 100644
index 000000000..61f03a4ad
--- /dev/null
+++ b/helper/schema/resource_timeout.go
@@ -0,0 +1,233 @@
+package schema
+
+import (
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/hashicorp/terraform/terraform"
+ "github.com/mitchellh/copystructure"
+)
+
+const TimeoutKey = "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0"
+
+const (
+ TimeoutCreate = "create"
+ TimeoutRead = "read"
+ TimeoutUpdate = "update"
+ TimeoutDelete = "delete"
+ TimeoutDefault = "default"
+)
+
+func timeoutKeys() []string {
+ return []string{
+ TimeoutCreate,
+ TimeoutRead,
+ TimeoutUpdate,
+ TimeoutDelete,
+ TimeoutDefault,
+ }
+}
+
+// could be time.Duration, int64 or float64
+func DefaultTimeout(tx interface{}) *time.Duration {
+ var td time.Duration
+ switch raw := tx.(type) {
+ case time.Duration:
+ return &raw
+ case int64:
+ td = time.Duration(raw)
+ case float64:
+ td = time.Duration(int64(raw))
+ default:
+ log.Printf("[WARN] Unknown type in DefaultTimeout: %#v", tx)
+ }
+ return &td
+}
+
+type ResourceTimeout struct {
+ Create, Read, Update, Delete, Default *time.Duration
+}
+
+// ConfigDecode takes a schema and the configuration (available in Diff) and
+// validates, parses the timeouts into `t`
+func (t *ResourceTimeout) ConfigDecode(s *Resource, c *terraform.ResourceConfig) error {
+ if s.Timeouts != nil {
+ raw, err := copystructure.Copy(s.Timeouts)
+ if err != nil {
+ log.Printf("[DEBUG] Error with deep copy: %s", err)
+ }
+ *t = *raw.(*ResourceTimeout)
+ }
+
+ if raw, ok := c.Config["timeout"]; ok {
+ configTimeouts := raw.([]map[string]interface{})
+ for _, timeoutValues := range configTimeouts {
+ // loop through each Timeout given in the configuration and validate they
+ // the Timeout defined in the resource
+ for timeKey, timeValue := range timeoutValues {
+ // validate that we're dealing with the normal CRUD actions
+ var found bool
+ for _, key := range timeoutKeys() {
+ if timeKey == key {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ return fmt.Errorf("Unsupported Timeout configuration key found (%s)", timeKey)
+ }
+
+ // Get timeout
+ rt, err := time.ParseDuration(timeValue.(string))
+ if err != nil {
+ return fmt.Errorf("Error parsing Timeout for (%s): %s", timeKey, err)
+ }
+
+ var timeout *time.Duration
+ switch timeKey {
+ case TimeoutCreate:
+ timeout = t.Create
+ case TimeoutUpdate:
+ timeout = t.Update
+ case TimeoutRead:
+ timeout = t.Read
+ case TimeoutDelete:
+ timeout = t.Delete
+ case TimeoutDefault:
+ timeout = t.Default
+ }
+
+ // If the resource has not delcared this in the definition, then error
+ // with an unsupported message
+ if timeout == nil {
+ return unsupportedTimeoutKeyError(timeKey)
+ }
+
+ *timeout = rt
+ }
+ }
+ }
+
+ return nil
+}
+
+func unsupportedTimeoutKeyError(key string) error {
+ return fmt.Errorf("Timeout Key (%s) is not supported", key)
+}
+
+// DiffEncode, StateEncode, and MetaDecode are analogous to the Go stdlib JSONEncoder
+// interface: they encode/decode a timeouts struct from an instance diff, which is
+// where the timeout data is stored after a diff to pass into Apply.
+//
+// StateEncode encodes the timeout into the ResourceData's InstanceState for
+// saving to state
+//
+func (t *ResourceTimeout) DiffEncode(id *terraform.InstanceDiff) error {
+ return t.metaEncode(id)
+}
+
+func (t *ResourceTimeout) StateEncode(is *terraform.InstanceState) error {
+ return t.metaEncode(is)
+}
+
+// metaEncode encodes the ResourceTimeout into a map[string]interface{} format
+// and stores it in the Meta field of the interface it's given.
+// Assumes the interface is either *terraform.InstanceState or
+// *terraform.InstanceDiff, returns an error otherwise
+func (t *ResourceTimeout) metaEncode(ids interface{}) error {
+ m := make(map[string]interface{})
+
+ if t.Create != nil {
+ m[TimeoutCreate] = t.Create.Nanoseconds()
+ }
+ if t.Read != nil {
+ m[TimeoutRead] = t.Read.Nanoseconds()
+ }
+ if t.Update != nil {
+ m[TimeoutUpdate] = t.Update.Nanoseconds()
+ }
+ if t.Delete != nil {
+ m[TimeoutDelete] = t.Delete.Nanoseconds()
+ }
+ if t.Default != nil {
+ m[TimeoutDefault] = t.Default.Nanoseconds()
+ // for any key above that is nil, if default is specified, we need to
+ // populate it with the default
+ for _, k := range timeoutKeys() {
+ if _, ok := m[k]; !ok {
+ m[k] = t.Default.Nanoseconds()
+ }
+ }
+ }
+
+ // only add the Timeout to the Meta if we have values
+ if len(m) > 0 {
+ switch instance := ids.(type) {
+ case *terraform.InstanceDiff:
+ if instance.Meta == nil {
+ instance.Meta = make(map[string]interface{})
+ }
+ instance.Meta[TimeoutKey] = m
+ case *terraform.InstanceState:
+ if instance.Meta == nil {
+ instance.Meta = make(map[string]interface{})
+ }
+ instance.Meta[TimeoutKey] = m
+ default:
+ return fmt.Errorf("Error matching type for Diff Encode")
+ }
+ }
+
+ return nil
+}
+
+func (t *ResourceTimeout) StateDecode(id *terraform.InstanceState) error {
+ return t.metaDecode(id)
+}
+func (t *ResourceTimeout) DiffDecode(is *terraform.InstanceDiff) error {
+ return t.metaDecode(is)
+}
+
+func (t *ResourceTimeout) metaDecode(ids interface{}) error {
+ var rawMeta interface{}
+ var ok bool
+ switch rawInstance := ids.(type) {
+ case *terraform.InstanceDiff:
+ rawMeta, ok = rawInstance.Meta[TimeoutKey]
+ if !ok {
+ return nil
+ }
+ case *terraform.InstanceState:
+ rawMeta, ok = rawInstance.Meta[TimeoutKey]
+ if !ok {
+ return nil
+ }
+ default:
+ return fmt.Errorf("Unknown or unsupported type in metaDecode: %#v", ids)
+ }
+
+ times := rawMeta.(map[string]interface{})
+ if len(times) == 0 {
+ return nil
+ }
+
+ if v, ok := times[TimeoutCreate]; ok {
+ t.Create = DefaultTimeout(v)
+ }
+ if v, ok := times[TimeoutRead]; ok {
+ t.Read = DefaultTimeout(v)
+ }
+ if v, ok := times[TimeoutUpdate]; ok {
+ t.Update = DefaultTimeout(v)
+ }
+ if v, ok := times[TimeoutDelete]; ok {
+ t.Delete = DefaultTimeout(v)
+ }
+ if v, ok := times[TimeoutDefault]; ok {
+ t.Default = DefaultTimeout(v)
+ }
+
+ return nil
+}
diff --git a/helper/schema/resource_timeout_test.go b/helper/schema/resource_timeout_test.go
new file mode 100644
index 000000000..6e6b2604a
--- /dev/null
+++ b/helper/schema/resource_timeout_test.go
@@ -0,0 +1,352 @@
+package schema
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/hashicorp/terraform/config"
+ "github.com/hashicorp/terraform/terraform"
+)
+
+func TestResourceTimeout_ConfigDecode_badkey(t *testing.T) {
+ cases := []struct {
+ Name string
+ // what the resource has defined in source
+ ResourceDefaultTimeout *ResourceTimeout
+ // configuration provider by user in tf file
+ Config []map[string]interface{}
+ // what we expect the parsed ResourceTimeout to be
+ Expected *ResourceTimeout
+ // Should we have an error (key not defined in source)
+ ShouldErr bool
+ }{
+ {
+ Name: "Source does not define 'delete' key",
+ ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0),
+ Config: expectedConfigForValues(2, 0, 0, 1, 0),
+ Expected: timeoutForValues(10, 0, 5, 0, 0),
+ ShouldErr: true,
+ },
+ {
+ Name: "Config overrides create",
+ ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0),
+ Config: expectedConfigForValues(2, 0, 7, 0, 0),
+ Expected: timeoutForValues(2, 0, 7, 0, 0),
+ ShouldErr: false,
+ },
+ {
+ Name: "Config overrides create, default provided. Should still have zero values",
+ ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3),
+ Config: expectedConfigForValues(2, 0, 7, 0, 0),
+ Expected: timeoutForValues(2, 0, 7, 0, 3),
+ ShouldErr: false,
+ },
+ {
+ Name: "Use something besides 'minutes'",
+ ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3),
+ Config: []map[string]interface{}{
+ map[string]interface{}{
+ "create": "2h",
+ }},
+ Expected: timeoutForValues(120, 0, 5, 0, 3),
+ ShouldErr: false,
+ },
+ }
+
+ for i, c := range cases {
+ t.Run(fmt.Sprintf("%d-%s", i, c.Name), func(t *testing.T) {
+ r := &Resource{
+ Timeouts: c.ResourceDefaultTimeout,
+ }
+
+ raw, err := config.NewRawConfig(
+ map[string]interface{}{
+ "foo": "bar",
+ "timeout": c.Config,
+ })
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ conf := terraform.NewResourceConfig(raw)
+
+ timeout := &ResourceTimeout{}
+ decodeErr := timeout.ConfigDecode(r, conf)
+ if c.ShouldErr {
+ if decodeErr == nil {
+ t.Fatalf("ConfigDecode case (%d): Expected bad timeout key: %s", i, decodeErr)
+ }
+ // should error, err was not nil, continue
+ return
+ } else {
+ if decodeErr != nil {
+ // should not error, error was not nil, fatal
+ t.Fatalf("decodeError was not nil: %s", decodeErr)
+ }
+ }
+
+ if !reflect.DeepEqual(c.Expected, timeout) {
+ t.Fatalf("ConfigDecode match error case (%d), expected:\n%#v\ngot:\n%#v", i, c.Expected, timeout)
+ }
+ })
+ }
+}
+
+func TestResourceTimeout_ConfigDecode(t *testing.T) {
+ r := &Resource{
+ Timeouts: &ResourceTimeout{
+ Create: DefaultTimeout(10 * time.Minute),
+ Update: DefaultTimeout(5 * time.Minute),
+ },
+ }
+
+ raw, err := config.NewRawConfig(
+ map[string]interface{}{
+ "foo": "bar",
+ "timeout": []map[string]interface{}{
+ map[string]interface{}{
+ "create": "2m",
+ },
+ map[string]interface{}{
+ "update": "1m",
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ c := terraform.NewResourceConfig(raw)
+
+ timeout := &ResourceTimeout{}
+ err = timeout.ConfigDecode(r, c)
+ if err != nil {
+ t.Fatalf("Expected good timeout returned:, %s", err)
+ }
+
+ expected := &ResourceTimeout{
+ Create: DefaultTimeout(2 * time.Minute),
+ Update: DefaultTimeout(1 * time.Minute),
+ }
+
+ if !reflect.DeepEqual(timeout, expected) {
+ t.Fatalf("bad timeout decode, expected (%#v), got (%#v)", expected, timeout)
+ }
+}
+
+func TestResourceTimeout_DiffEncode_basic(t *testing.T) {
+ cases := []struct {
+ Timeout *ResourceTimeout
+ Expected map[string]interface{}
+ // Not immediately clear when an error would hit
+ ShouldErr bool
+ }{
+ // Two fields
+ {
+ Timeout: timeoutForValues(10, 0, 5, 0, 0),
+ Expected: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)},
+ ShouldErr: false,
+ },
+ // Two fields, one is Default
+ {
+ Timeout: timeoutForValues(10, 0, 0, 0, 7),
+ Expected: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)},
+ ShouldErr: false,
+ },
+ // All fields
+ {
+ Timeout: timeoutForValues(10, 3, 4, 1, 7),
+ Expected: map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)},
+ ShouldErr: false,
+ },
+ // No fields
+ {
+ Timeout: &ResourceTimeout{},
+ Expected: nil,
+ ShouldErr: false,
+ },
+ }
+
+ for _, c := range cases {
+ state := &terraform.InstanceDiff{}
+ err := c.Timeout.DiffEncode(state)
+ if err != nil && !c.ShouldErr {
+ t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta)
+ }
+
+ // should maybe just compare [TimeoutKey] but for now we're assuming only
+ // that in Meta
+ if !reflect.DeepEqual(state.Meta, c.Expected) {
+ t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta)
+ }
+ }
+ // same test cases but for InstanceState
+ for _, c := range cases {
+ state := &terraform.InstanceState{}
+ err := c.Timeout.StateEncode(state)
+ if err != nil && !c.ShouldErr {
+ t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta)
+ }
+
+ // should maybe just compare [TimeoutKey] but for now we're assuming only
+ // that in Meta
+ if !reflect.DeepEqual(state.Meta, c.Expected) {
+ t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta)
+ }
+ }
+}
+
+func TestResourceTimeout_MetaDecode_basic(t *testing.T) {
+ cases := []struct {
+ State *terraform.InstanceDiff
+ Expected *ResourceTimeout
+ // Not immediately clear when an error would hit
+ ShouldErr bool
+ }{
+ // Two fields
+ {
+ State: &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)}},
+ Expected: timeoutForValues(10, 0, 5, 0, 0),
+ ShouldErr: false,
+ },
+ // Two fields, one is Default
+ {
+ State: &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)}},
+ Expected: timeoutForValues(10, 7, 7, 7, 7),
+ ShouldErr: false,
+ },
+ // All fields
+ {
+ State: &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)}},
+ Expected: timeoutForValues(10, 3, 4, 1, 7),
+ ShouldErr: false,
+ },
+ // No fields
+ {
+ State: &terraform.InstanceDiff{},
+ Expected: &ResourceTimeout{},
+ ShouldErr: false,
+ },
+ }
+
+ for _, c := range cases {
+ rt := &ResourceTimeout{}
+ err := rt.DiffDecode(c.State)
+ if err != nil && !c.ShouldErr {
+ t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, rt)
+ }
+
+ // should maybe just compare [TimeoutKey] but for now we're assuming only
+ // that in Meta
+ if !reflect.DeepEqual(rt, c.Expected) {
+ t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, rt)
+ }
+ }
+}
+
+func timeoutForValues(create, read, update, del, def int) *ResourceTimeout {
+ rt := ResourceTimeout{}
+
+ if create != 0 {
+ rt.Create = DefaultTimeout(time.Duration(create) * time.Minute)
+ }
+ if read != 0 {
+ rt.Read = DefaultTimeout(time.Duration(read) * time.Minute)
+ }
+ if update != 0 {
+ rt.Update = DefaultTimeout(time.Duration(update) * time.Minute)
+ }
+ if del != 0 {
+ rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute)
+ }
+
+ if def != 0 {
+ rt.Default = DefaultTimeout(time.Duration(def) * time.Minute)
+ }
+
+ return &rt
+}
+
+// Generates a ResourceTimeout struct that should reflect the
+// d.Timeout("key") results
+func expectedTimeoutForValues(create, read, update, del, def int) *ResourceTimeout {
+ rt := ResourceTimeout{}
+
+ defaultValues := []*int{&create, &read, &update, &del, &def}
+ for _, v := range defaultValues {
+ if *v == 0 {
+ *v = 20
+ }
+ }
+
+ if create != 0 {
+ rt.Create = DefaultTimeout(time.Duration(create) * time.Minute)
+ }
+ if read != 0 {
+ rt.Read = DefaultTimeout(time.Duration(read) * time.Minute)
+ }
+ if update != 0 {
+ rt.Update = DefaultTimeout(time.Duration(update) * time.Minute)
+ }
+ if del != 0 {
+ rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute)
+ }
+
+ if def != 0 {
+ rt.Default = DefaultTimeout(time.Duration(def) * time.Minute)
+ }
+
+ return &rt
+}
+
+func expectedForValues(create, read, update, del, def int) map[string]interface{} {
+ ex := make(map[string]interface{})
+
+ if create != 0 {
+ ex["create"] = DefaultTimeout(time.Duration(create) * time.Minute).Nanoseconds()
+ }
+ if read != 0 {
+ ex["read"] = DefaultTimeout(time.Duration(read) * time.Minute).Nanoseconds()
+ }
+ if update != 0 {
+ ex["update"] = DefaultTimeout(time.Duration(update) * time.Minute).Nanoseconds()
+ }
+ if del != 0 {
+ ex["delete"] = DefaultTimeout(time.Duration(del) * time.Minute).Nanoseconds()
+ }
+
+ if def != 0 {
+ defNano := DefaultTimeout(time.Duration(def) * time.Minute).Nanoseconds()
+ ex["default"] = defNano
+
+ for _, k := range timeoutKeys() {
+ if _, ok := ex[k]; !ok {
+ ex[k] = defNano
+ }
+ }
+ }
+
+ return ex
+}
+
+func expectedConfigForValues(create, read, update, delete, def int) []map[string]interface{} {
+ ex := make([]map[string]interface{}, 0)
+
+ if create != 0 {
+ ex = append(ex, map[string]interface{}{"create": fmt.Sprintf("%dm", create)})
+ }
+ if read != 0 {
+ ex = append(ex, map[string]interface{}{"read": fmt.Sprintf("%dm", read)})
+ }
+ if update != 0 {
+ ex = append(ex, map[string]interface{}{"update": fmt.Sprintf("%dm", update)})
+ }
+ if delete != 0 {
+ ex = append(ex, map[string]interface{}{"delete": fmt.Sprintf("%dm", delete)})
+ }
+
+ if def != 0 {
+ ex = append(ex, map[string]interface{}{"default": fmt.Sprintf("%dm", def)})
+ }
+ return ex
+}
diff --git a/helper/schema/schema.go b/helper/schema/schema.go
index 7591721af..05d21c7ff 100644
--- a/helper/schema/schema.go
+++ b/helper/schema/schema.go
@@ -1327,6 +1327,9 @@ func (m schemaMap) validateObject(
if m, ok := raw.(map[string]interface{}); ok {
for subk, _ := range m {
if _, ok := schema[subk]; !ok {
+ if subk == "timeout" {
+ continue
+ }
es = append(es, fmt.Errorf(
"%s: invalid or unknown key: %s", k, subk))
}
diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go
index f335c8aea..4119b7ff5 100644
--- a/helper/schema/schema_test.go
+++ b/helper/schema/schema_test.go
@@ -4773,6 +4773,23 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: false,
},
+
+ "special timeout field": {
+ Schema: map[string]*Schema{
+ "availability_zone": &Schema{
+ Type: TypeString,
+ Optional: true,
+ Computed: true,
+ ForceNew: true,
+ },
+ },
+
+ Config: map[string]interface{}{
+ "timeout": "bar",
+ },
+
+ Err: false,
+ },
}
for tn, tc := range cases {
diff --git a/website/source/docs/configuration/resources.html.md b/website/source/docs/configuration/resources.html.md
index 0a89dded5..ab15b6744 100644
--- a/website/source/docs/configuration/resources.html.md
+++ b/website/source/docs/configuration/resources.html.md
@@ -97,6 +97,43 @@ Additionally you can also use a single entry with a wildcard (e.g. `"*"`)
which will match all attribute names. Using a partial string together with a
wildcard (e.g. `"rout*"`) is **not** supported.
+
+
+
+### Timeouts
+
+Individual Resources may provide a `timeout` block to enable users to configure the
+amount of time a specific operation is allowed to take before being considered
+an error. For example, the
+[aws_db_instance](/docs/providers/aws/r/db_instance.html#timeouts)
+resource provides configurable timeouts for the
+`create`, `update`, and `delete` operations. Any Resource that provies Timeouts
+will document the default values for that operation, and users can overwrite
+them in their configuration.
+
+Example overwriting the `create` and `delete` timeouts:
+
+```
+resource "aws_db_instance" "timeout_example" {
+ allocated_storage = 10
+ engine = "mysql"
+ engine_version = "5.6.17"
+ instance_class = "db.t1.micro"
+ name = "mydb"
+ [...]
+
+ timeout {
+ create = "60m"
+ delete = "2h"
+ }
+}
+```
+
+Individual Resources must opt-in to providing configurable Timeouts, and
+attempting to configure the timeout for a Resource that does not support
+Timeouts, or overwriting a specific action that the Resource does not specify as
+an option, will result in an error. Valid units of time are `s`, `m`, `h`.
+
### Explicit Dependencies
diff --git a/website/source/docs/providers/aws/r/db_instance.html.markdown b/website/source/docs/providers/aws/r/db_instance.html.markdown
index 900017941..eafba7417 100644
--- a/website/source/docs/providers/aws/r/db_instance.html.markdown
+++ b/website/source/docs/providers/aws/r/db_instance.html.markdown
@@ -144,6 +144,19 @@ On Oracle instances the following is exported additionally:
* `character_set_name` - The character set used on Oracle instances.
+
+
+## Timeouts
+
+`aws_db_instance` provides the following
+[Timeouts](/docs/configuration/resources.html#timeouts) configuration options:
+
+- `create` - (Default `40 minutes`) Used for Creating Instances, Replicas, and
+restoring from Snapshots
+- `update` - (Default `80 minutes`) Used for Database modifications
+- `delete` - (Default `40 minutes`) Used for destroying databases. This includes
+the time required to take snapshots
+
[1]: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Replication.html
[2]: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.Maintenance.html