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:
parent
e5e37b0025
commit
2fe5976aec
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue