helper/schema: Add configurable Timeouts (#12311)

* 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
This commit is contained in:
Clint 2017-03-02 11:07:49 -06:00 committed by GitHub
parent e5e37b0025
commit 2fe5976aec
11 changed files with 989 additions and 10 deletions

View File

@ -25,6 +25,12 @@ func resourceAwsDbInstance() *schema.Resource {
State: resourceAwsDbInstanceImport, 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{ Schema: map[string]*schema.Schema{
"name": { "name": {
Type: schema.TypeString, Type: schema.TypeString,
@ -480,7 +486,7 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
"maintenance", "renaming", "rebooting", "upgrading"}, "maintenance", "renaming", "rebooting", "upgrading"},
Target: []string{"available"}, Target: []string{"available"},
Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta), Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta),
Timeout: 40 * time.Minute, Timeout: d.Timeout(schema.TimeoutCreate),
MinTimeout: 10 * time.Second, MinTimeout: 10 * time.Second,
Delay: 30 * time.Second, // Wait 30 secs before starting 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"}, "maintenance", "renaming", "rebooting", "upgrading", "configuring-enhanced-monitoring"},
Target: []string{"available"}, Target: []string{"available"},
Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta), Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta),
Timeout: 40 * time.Minute, Timeout: d.Timeout(schema.TimeoutCreate),
MinTimeout: 10 * time.Second, MinTimeout: 10 * time.Second,
Delay: 30 * time.Second, // Wait 30 secs before starting Delay: 30 * time.Second, // Wait 30 secs before starting
} }
@ -811,7 +817,7 @@ func resourceAwsDbInstanceDelete(d *schema.ResourceData, meta interface{}) error
"modifying", "deleting", "available"}, "modifying", "deleting", "available"},
Target: []string{}, Target: []string{},
Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta), Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta),
Timeout: 40 * time.Minute, Timeout: d.Timeout(schema.TimeoutDelete),
MinTimeout: 10 * time.Second, MinTimeout: 10 * time.Second,
Delay: 30 * time.Second, // Wait 30 secs before starting 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"}, "maintenance", "renaming", "rebooting", "upgrading", "configuring-enhanced-monitoring", "moving-to-vpc"},
Target: []string{"available"}, Target: []string{"available"},
Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta), Refresh: resourceAwsDbInstanceStateRefreshFunc(d, meta),
Timeout: 80 * time.Minute, Timeout: d.Timeout(schema.TimeoutUpdate),
MinTimeout: 10 * time.Second, MinTimeout: 10 * time.Second,
Delay: 30 * time.Second, // Wait 30 secs before starting Delay: 30 * time.Second, // Wait 30 secs before starting
} }

View File

@ -3,6 +3,7 @@ package schema
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"strconv" "strconv"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
@ -94,6 +95,15 @@ type Resource struct {
// This is a private interface for now, for use by DataSourceResourceShim, // This is a private interface for now, for use by DataSourceResourceShim,
// and not for general use. (But maybe later...) // and not for general use. (But maybe later...)
deprecationMessage string 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. // See Resource documentation.
@ -125,6 +135,18 @@ func (r *Resource) Apply(
return s, err 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 { if s == nil {
// The Terraform API dictates that this should never happen, but // The Terraform API dictates that this should never happen, but
// it doesn't hurt to be safe in this case. // 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 // Reset the data to be stateless since we just destroyed
data, err = schemaMap(r.Schema).Data(nil, d) data, err = schemaMap(r.Schema).Data(nil, d)
// data was reset, need to re-apply the parsed timeouts
data.timeouts = &rt
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -176,7 +200,28 @@ func (r *Resource) Apply(
func (r *Resource) Diff( func (r *Resource) Diff(
s *terraform.InstanceState, s *terraform.InstanceState,
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { 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. // Validate validates the resource configuration against the schema.
@ -226,10 +271,19 @@ func (r *Resource) Refresh(
return nil, nil 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 { if r.Exists != nil {
// Make a copy of data so that if it is modified it doesn't // Make a copy of data so that if it is modified it doesn't
// affect our Read later. // affect our Read later.
data, err := schemaMap(r.Schema).Data(s, nil) data, err := schemaMap(r.Schema).Data(s, nil)
data.timeouts = &rt
if err != nil { if err != nil {
return s, err return s, err
} }
@ -252,6 +306,7 @@ func (r *Resource) Refresh(
} }
data, err := schemaMap(r.Schema).Data(s, nil) data, err := schemaMap(r.Schema).Data(s, nil)
data.timeouts = &rt
if err != nil { if err != nil {
return s, err return s, err
} }

View File

@ -5,6 +5,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"sync" "sync"
"time"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -24,6 +25,7 @@ type ResourceData struct {
state *terraform.InstanceState state *terraform.InstanceState
diff *terraform.InstanceDiff diff *terraform.InstanceDiff
meta map[string]interface{} meta map[string]interface{}
timeouts *ResourceTimeout
// Don't set // Don't set
multiReader *MultiLevelFieldReader multiReader *MultiLevelFieldReader
@ -250,6 +252,12 @@ func (d *ResourceData) State() *terraform.InstanceState {
return nil 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 // Look for a magic key in the schema that determines we skip the
// integrity check of fields existing in the schema, allowing dynamic // integrity check of fields existing in the schema, allowing dynamic
// keys to be created. // keys to be created.
@ -331,6 +339,35 @@ func (d *ResourceData) State() *terraform.InstanceState {
return &result 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() { func (d *ResourceData) init() {
// Initialize the field that will store our new state // Initialize the field that will store our new state
var copyState terraform.InstanceState var copyState terraform.InstanceState

View File

@ -1,9 +1,11 @@
package schema package schema
import ( import (
"fmt"
"math" "math"
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/hashicorp/terraform/terraform" "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) { func TestResourceDataHasChange(t *testing.T) {
cases := []struct { cases := []struct {
Schema map[string]*Schema 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) { func TestResourceDataSetId(t *testing.T) {
d := &ResourceData{} d := &ResourceData{}
d.SetId("foo") d.SetId("foo")

View File

@ -5,7 +5,9 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"testing" "testing"
"time"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform" "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) { func TestResourceApply_destroy(t *testing.T) {
r := &Resource{ r := &Resource{
Schema: map[string]*Schema{ Schema: map[string]*Schema{

View File

@ -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
}

View File

@ -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
}

View File

@ -1327,6 +1327,9 @@ func (m schemaMap) validateObject(
if m, ok := raw.(map[string]interface{}); ok { if m, ok := raw.(map[string]interface{}); ok {
for subk, _ := range m { for subk, _ := range m {
if _, ok := schema[subk]; !ok { if _, ok := schema[subk]; !ok {
if subk == "timeout" {
continue
}
es = append(es, fmt.Errorf( es = append(es, fmt.Errorf(
"%s: invalid or unknown key: %s", k, subk)) "%s: invalid or unknown key: %s", k, subk))
} }

View File

@ -4773,6 +4773,23 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: false, 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 { for tn, tc := range cases {

View File

@ -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 which will match all attribute names. Using a partial string together with a
wildcard (e.g. `"rout*"`) is **not** supported. wildcard (e.g. `"rout*"`) is **not** supported.
<a id="timeouts"></a>
### 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`.
<a id="explicit-dependencies"></a> <a id="explicit-dependencies"></a>
### Explicit Dependencies ### Explicit Dependencies

View File

@ -144,6 +144,19 @@ On Oracle instances the following is exported additionally:
* `character_set_name` - The character set used on Oracle instances. * `character_set_name` - The character set used on Oracle instances.
<a id="timeouts"></a>
## 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 [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 [2]: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.Maintenance.html