process state even after provider.Apply errors

Terraform core expects a sane state even when the provider returns an
error. Make sure at the prior state is always the default value to
return, and then alway attempt to process any state returned by
provider.Apply.
This commit is contained in:
James Bardin 2019-02-11 15:35:46 -05:00
parent 6eb7bfbdfb
commit 1bfc27817e
3 changed files with 77 additions and 4 deletions

View File

@ -1,6 +1,7 @@
package test package test
import ( import (
"errors"
"fmt" "fmt"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
@ -123,6 +124,11 @@ func testResource() *schema.Resource {
}, },
}, },
}, },
"apply_error": {
Type: schema.TypeString,
Optional: true,
Description: "return and error during apply",
},
}, },
} }
} }
@ -130,6 +136,11 @@ func testResource() *schema.Resource {
func testResourceCreate(d *schema.ResourceData, meta interface{}) error { func testResourceCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId") d.SetId("testId")
errMsg, _ := d.Get("apply_error").(string)
if errMsg != "" {
return errors.New(errMsg)
}
// Required must make it through to Create // Required must make it through to Create
if _, ok := d.GetOk("required"); !ok { if _, ok := d.GetOk("required"); !ok {
return fmt.Errorf("Missing attribute 'required', but it's required!") return fmt.Errorf("Missing attribute 'required', but it's required!")
@ -156,6 +167,10 @@ func testResourceRead(d *schema.ResourceData, meta interface{}) error {
} }
func testResourceUpdate(d *schema.ResourceData, meta interface{}) error { func testResourceUpdate(d *schema.ResourceData, meta interface{}) error {
errMsg, _ := d.Get("apply_error").(string)
if errMsg != "" {
return errors.New(errMsg)
}
return nil return nil
} }

View File

@ -2,6 +2,7 @@ package test
import ( import (
"reflect" "reflect"
"regexp"
"strings" "strings"
"testing" "testing"
@ -624,3 +625,55 @@ resource "test_resource" "foo" {
}, },
}) })
} }
func TestResource_updateError(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource" "foo" {
required = "first"
required_map = {
a = "a"
}
}
`),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource" "foo" {
required = "second"
required_map = {
a = "a"
}
apply_error = "update_error"
}
`),
ExpectError: regexp.MustCompile("update_error"),
},
},
})
}
func TestResource_applyError(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource" "foo" {
required = "second"
required_map = {
a = "a"
}
apply_error = "apply_error"
}
`),
ExpectError: regexp.MustCompile("apply_error"),
},
},
})
}

View File

@ -656,7 +656,10 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
} }
func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) { func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) {
resp := &proto.ApplyResourceChange_Response{} resp := &proto.ApplyResourceChange_Response{
// Start with the existing state as a fallback
NewState: req.PriorState,
}
res := s.provider.ResourcesMap[req.TypeName] res := s.provider.ResourcesMap[req.TypeName]
block := res.CoreConfigSchema() block := res.CoreConfigSchema()
@ -753,15 +756,17 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
} }
newInstanceState, err := s.provider.Apply(info, priorState, diff) newInstanceState, err := s.provider.Apply(info, priorState, diff)
// we record the error here, but continue processing any returned state.
if err != nil { if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
} }
newStateVal := cty.NullVal(block.ImpliedType()) newStateVal := cty.NullVal(block.ImpliedType())
// always return a nul value for destroy // Always return a null value for destroy.
if newInstanceState == nil || destroy { // While this is usually indicated by a nil state, check for missing ID or
// attributes in the case of a provider failure.
if destroy || newInstanceState == nil || newInstanceState.Attributes == nil || newInstanceState.ID == "" {
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)