Merge pull request #19222 from hashicorp/jbardin/timeout

provider timeouts
This commit is contained in:
James Bardin 2018-10-30 14:56:59 -04:00 committed by GitHub
commit 28a881a670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 358 additions and 14 deletions

View File

@ -20,6 +20,7 @@ func Provider() terraform.ResourceProvider {
"test_resource": testResource(), "test_resource": testResource(),
"test_resource_gh12183": testResourceGH12183(), "test_resource_gh12183": testResourceGH12183(),
"test_resource_with_custom_diff": testResourceCustomDiff(), "test_resource_with_custom_diff": testResourceCustomDiff(),
"test_resource_timeout": testResourceTimeout(),
}, },
DataSourcesMap: map[string]*schema.Resource{ DataSourcesMap: map[string]*schema.Resource{
"test_data_source": testDataSource(), "test_data_source": testDataSource(),

View File

@ -0,0 +1,120 @@
package test
import (
"fmt"
"time"
"github.com/hashicorp/terraform/helper/schema"
)
func testResourceTimeout() *schema.Resource {
return &schema.Resource{
Create: testResourceTimeoutCreate,
Read: testResourceTimeoutRead,
Update: testResourceTimeoutUpdate,
Delete: testResourceTimeoutDelete,
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(time.Second),
Update: schema.DefaultTimeout(time.Second),
Delete: schema.DefaultTimeout(time.Second),
},
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"create_delay": {
Type: schema.TypeString,
Optional: true,
},
"read_delay": {
Type: schema.TypeString,
Optional: true,
},
"update_delay": {
Type: schema.TypeString,
Optional: true,
},
"delete_delay": {
Type: schema.TypeString,
Optional: true,
},
},
}
}
func testResourceTimeoutCreate(d *schema.ResourceData, meta interface{}) error {
delayString := d.Get("create_delay").(string)
var delay time.Duration
var err error
if delayString != "" {
delay, err = time.ParseDuration(delayString)
if err != nil {
return err
}
}
if delay > d.Timeout(schema.TimeoutCreate) {
return fmt.Errorf("timeout while creating resource")
}
d.SetId("testId")
return testResourceRead(d, meta)
}
func testResourceTimeoutRead(d *schema.ResourceData, meta interface{}) error {
delayString := d.Get("read_delay").(string)
var delay time.Duration
var err error
if delayString != "" {
delay, err = time.ParseDuration(delayString)
if err != nil {
return err
}
}
if delay > d.Timeout(schema.TimeoutRead) {
return fmt.Errorf("timeout while reading resource")
}
return nil
}
func testResourceTimeoutUpdate(d *schema.ResourceData, meta interface{}) error {
delayString := d.Get("update_delay").(string)
var delay time.Duration
var err error
if delayString != "" {
delay, err = time.ParseDuration(delayString)
if err != nil {
return err
}
}
if delay > d.Timeout(schema.TimeoutUpdate) {
return fmt.Errorf("timeout while updating resource")
}
return nil
}
func testResourceTimeoutDelete(d *schema.ResourceData, meta interface{}) error {
delayString := d.Get("delete_delay").(string)
var delay time.Duration
var err error
if delayString != "" {
delay, err = time.ParseDuration(delayString)
if err != nil {
return err
}
}
if delay > d.Timeout(schema.TimeoutDelete) {
return fmt.Errorf("timeout while deleting resource")
}
d.SetId("")
return nil
}

View File

@ -0,0 +1,91 @@
package test
import (
"regexp"
"strings"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestResourceTimeout_create(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_timeout" "foo" {
create_delay = "2s"
timeouts {
create = "1s"
}
}
`),
ExpectError: regexp.MustCompile("timeout while creating resource"),
},
},
})
}
func TestResourceTimeout_update(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_timeout" "foo" {
update_delay = "1s"
timeouts {
update = "1s"
}
}
`),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_timeout" "foo" {
update_delay = "2s"
timeouts {
update = "1s"
}
}
`),
ExpectError: regexp.MustCompile("timeout while updating resource"),
},
},
})
}
func TestResourceTimeout_read(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_timeout" "foo" {
}
`),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_timeout" "foo" {
read_delay = "30m"
}
`),
ExpectError: regexp.MustCompile("timeout while reading resource"),
},
// we need to remove the read_delay so that the resource can be
// destroyed in the final step, but expect an error here from the
// pre-existing delay.
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_timeout" "foo" {
}
`),
ExpectError: regexp.MustCompile("timeout while reading resource"),
},
},
})
}

View File

@ -412,20 +412,22 @@ func (s *GRPCProviderServer) ReadResource(_ context.Context, req *proto.ReadReso
// helper/schema should always copy the ID over, but do it again just to be safe // helper/schema should always copy the ID over, but do it again just to be safe
newInstanceState.Attributes["id"] = newInstanceState.ID newInstanceState.Attributes["id"] = newInstanceState.ID
newConfigVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Attributes, block.ImpliedType()) newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Attributes, block.ImpliedType())
if err != nil { if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil return resp, nil
} }
newConfigMP, err := msgpack.Marshal(newConfigVal, block.ImpliedType()) newStateVal = copyTimeoutValues(newStateVal, stateVal)
newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
if err != nil { if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil return resp, nil
} }
resp.NewState = &proto.DynamicValue{ resp.NewState = &proto.DynamicValue{
Msgpack: newConfigMP, Msgpack: newStateMP,
} }
return resp, nil return resp, nil
@ -461,9 +463,10 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
return resp, nil return resp, nil
} }
} }
priorState.Meta = priorPrivate priorState.Meta = priorPrivate
// turn the propsed state into a legacy configuration // turn the proposed state into a legacy configuration
config := terraform.NewResourceConfigShimmed(proposedNewStateVal, block) config := terraform.NewResourceConfigShimmed(proposedNewStateVal, block)
diff, err := s.provider.SimpleDiff(info, priorState, config) diff, err := s.provider.SimpleDiff(info, priorState, config)
@ -488,6 +491,8 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
return resp, nil return resp, nil
} }
plannedStateVal = copyTimeoutValues(plannedStateVal, proposedNewStateVal)
plannedMP, err := msgpack.Marshal(plannedStateVal, block.ImpliedType()) plannedMP, err := msgpack.Marshal(plannedStateVal, block.ImpliedType())
if err != nil { if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
@ -498,12 +503,14 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
} }
// the Meta field gets encoded into PlannedPrivate // the Meta field gets encoded into PlannedPrivate
if diff.Meta != nil {
plannedPrivate, err := json.Marshal(diff.Meta) plannedPrivate, err := json.Marshal(diff.Meta)
if err != nil { if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil return resp, nil
} }
resp.PlannedPrivate = plannedPrivate resp.PlannedPrivate = plannedPrivate
}
// collect the attributes that require instance replacement, and convert // collect the attributes that require instance replacement, and convert
// them to cty.Paths. // them to cty.Paths.
@ -594,7 +601,10 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
Meta: make(map[string]interface{}), Meta: make(map[string]interface{}),
} }
} }
if private != nil {
diff.Meta = private diff.Meta = private
}
newInstanceState, err := s.provider.Apply(info, priorState, diff) newInstanceState, err := s.provider.Apply(info, priorState, diff)
if err != nil { if err != nil {
@ -614,6 +624,8 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
} }
} }
newStateVal = copyTimeoutValues(newStateVal, plannedStateVal)
newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType()) newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
if err != nil { if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
@ -726,6 +738,8 @@ func (s *GRPCProviderServer) ReadDataSource(_ context.Context, req *proto.ReadDa
return resp, nil return resp, nil
} }
newStateVal = copyTimeoutValues(newStateVal, configVal)
newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType()) newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
if err != nil { if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
@ -770,3 +784,28 @@ func pathToAttributePath(path cty.Path) *proto.AttributePath {
return &proto.AttributePath{Steps: steps} return &proto.AttributePath{Steps: steps}
} }
// helper/schema throws away timeout values from the config and stores them in
// the Private/Meta fields. we need to copy those values into the planned state
// so that core doesn't see a perpetual diff with the timeout block.
func copyTimeoutValues(to cty.Value, from cty.Value) cty.Value {
// if `from` is null, then there are no attributes, and if `to` is null we
// are planning to remove it altogether.
if from.IsNull() || to.IsNull() {
return to
}
fromAttrs := from.AsValueMap()
timeouts, ok := fromAttrs[schema.TimeoutsConfigKey]
// no timeouts to copy
// timeouts shouldn't be unknown, but don't copy possibly invalid values
if !ok || timeouts.IsNull() || !timeouts.IsWhollyKnown() {
return to
}
toAttrs := to.AsValueMap()
toAttrs[schema.TimeoutsConfigKey] = timeouts
return cty.ObjectVal(toAttrs)
}

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
@ -388,8 +389,8 @@ func TestApplyResourceChange(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// A propsed state with only the ID unknown will produce a nil diff, and // A proposed state with only the ID unknown will produce a nil diff, and
// should return the propsed state value. // should return the proposed state value.
plannedVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ plannedVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String), "id": cty.UnknownVal(cty.String),
})) }))
@ -595,3 +596,44 @@ func TestPrepareProviderConfig(t *testing.T) {
}) })
} }
} }
func TestGetSchemaTimeouts(t *testing.T) {
r := &schema.Resource{
SchemaVersion: 4,
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(time.Second),
Read: schema.DefaultTimeout(2 * time.Second),
Update: schema.DefaultTimeout(3 * time.Second),
Default: schema.DefaultTimeout(10 * time.Second),
},
Schema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeInt,
Optional: true,
},
},
}
// verify that the timeouts appear in the schema as defined
block := r.CoreConfigSchema()
timeoutsBlock := block.BlockTypes["timeouts"]
if timeoutsBlock == nil {
t.Fatal("missing timeouts in schema")
}
if timeoutsBlock.Attributes["create"] == nil {
t.Fatal("missing create timeout in schema")
}
if timeoutsBlock.Attributes["read"] == nil {
t.Fatal("missing read timeout in schema")
}
if timeoutsBlock.Attributes["update"] == nil {
t.Fatal("missing update timeout in schema")
}
if d := timeoutsBlock.Attributes["delete"]; d != nil {
t.Fatalf("unexpected delete timeout in schema: %#v", d)
}
if timeoutsBlock.Attributes["default"] == nil {
t.Fatal("missing default timeout in schema")
}
}

View File

@ -77,7 +77,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep,
} }
// We need to keep a copy of the state prior to destroying // We need to keep a copy of the state prior to destroying
// such that destroy steps can verify their behaviour in the check // such that destroy steps can verify their behavior in the check
// function // function
stateBeforeApplication := state.DeepCopy() stateBeforeApplication := state.DeepCopy()

View File

@ -172,6 +172,57 @@ func (r *Resource) CoreConfigSchema() *configschema.Block {
} }
} }
_, timeoutsAttr := block.Attributes[TimeoutsConfigKey]
_, timeoutsBlock := block.BlockTypes[TimeoutsConfigKey]
// Insert configured timeout values into the schema, as long as the schema
// didn't define anything else by that name.
if r.Timeouts != nil && !timeoutsAttr && !timeoutsBlock {
timeouts := configschema.Block{
Attributes: map[string]*configschema.Attribute{},
}
if r.Timeouts.Create != nil {
timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
if r.Timeouts.Read != nil {
timeouts.Attributes[TimeoutRead] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
if r.Timeouts.Update != nil {
timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
if r.Timeouts.Delete != nil {
timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
if r.Timeouts.Default != nil {
timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}
block.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{
Nesting: configschema.NestingSingle,
Block: timeouts,
}
}
return block return block
} }

View File

@ -276,7 +276,7 @@ func newResourceConfigShimmedComputedKeys(obj cty.Value, schema *configschema.Bl
} }
blockVal := obj.GetAttr(typeName) blockVal := obj.GetAttr(typeName)
if !blockVal.IsKnown() || blockVal.IsNull() { if blockVal.IsNull() || !blockVal.IsKnown() {
continue continue
} }