Skip to content

Commit

Permalink
Merge pull request #19222 from hashicorp/jbardin/timeout
Browse files Browse the repository at this point in the history
provider timeouts
  • Loading branch information
jbardin authored Oct 30, 2018
2 parents ab88f8c + f153720 commit 28a881a
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 14 deletions.
1 change: 1 addition & 0 deletions builtin/providers/test/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func Provider() terraform.ResourceProvider {
"test_resource": testResource(),
"test_resource_gh12183": testResourceGH12183(),
"test_resource_with_custom_diff": testResourceCustomDiff(),
"test_resource_timeout": testResourceTimeout(),
},
DataSourcesMap: map[string]*schema.Resource{
"test_data_source": testDataSource(),
Expand Down
120 changes: 120 additions & 0 deletions builtin/providers/test/resource_timeout.go
Original file line number Diff line number Diff line change
@@ -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
}
91 changes: 91 additions & 0 deletions builtin/providers/test/resource_timeout_test.go
Original file line number Diff line number Diff line change
@@ -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"),
},
},
})
}
59 changes: 49 additions & 10 deletions helper/plugin/grpc_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
newInstanceState.Attributes["id"] = newInstanceState.ID

newConfigVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Attributes, block.ImpliedType())
newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Attributes, block.ImpliedType())
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}

newConfigMP, err := msgpack.Marshal(newConfigVal, block.ImpliedType())
newStateVal = copyTimeoutValues(newStateVal, stateVal)

newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}

resp.NewState = &proto.DynamicValue{
Msgpack: newConfigMP,
Msgpack: newStateMP,
}

return resp, nil
Expand Down Expand Up @@ -461,9 +463,10 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
return resp, nil
}
}

priorState.Meta = priorPrivate

// turn the propsed state into a legacy configuration
// turn the proposed state into a legacy configuration
config := terraform.NewResourceConfigShimmed(proposedNewStateVal, block)

diff, err := s.provider.SimpleDiff(info, priorState, config)
Expand All @@ -488,6 +491,8 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
return resp, nil
}

plannedStateVal = copyTimeoutValues(plannedStateVal, proposedNewStateVal)

plannedMP, err := msgpack.Marshal(plannedStateVal, block.ImpliedType())
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
Expand All @@ -498,12 +503,14 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
}

// the Meta field gets encoded into PlannedPrivate
plannedPrivate, err := json.Marshal(diff.Meta)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
if diff.Meta != nil {
plannedPrivate, err := json.Marshal(diff.Meta)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
resp.PlannedPrivate = plannedPrivate
}
resp.PlannedPrivate = plannedPrivate

// collect the attributes that require instance replacement, and convert
// them to cty.Paths.
Expand Down Expand Up @@ -594,7 +601,10 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
Meta: make(map[string]interface{}),
}
}
diff.Meta = private

if private != nil {
diff.Meta = private
}

newInstanceState, err := s.provider.Apply(info, priorState, diff)
if err != nil {
Expand All @@ -614,6 +624,8 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
}
}

newStateVal = copyTimeoutValues(newStateVal, plannedStateVal)

newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
Expand Down Expand Up @@ -726,6 +738,8 @@ func (s *GRPCProviderServer) ReadDataSource(_ context.Context, req *proto.ReadDa
return resp, nil
}

newStateVal = copyTimeoutValues(newStateVal, configVal)

newStateMP, err := msgpack.Marshal(newStateVal, block.ImpliedType())
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
Expand Down Expand Up @@ -770,3 +784,28 @@ func pathToAttributePath(path cty.Path) *proto.AttributePath {

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)
}
Loading

0 comments on commit 28a881a

Please sign in to comment.