diff --git a/helper/schema/resource.go b/helper/schema/resource.go index 7f5736bc6..f0e0515cd 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -41,10 +41,17 @@ type Resource struct { // If any errors occur during each of the operation, an error should be // returned. If a resource was partially updated, be careful to enable // partial state mode for ResourceData and use it accordingly. + // + // Exists is a function that is called to check if a resource still + // exists. If this returns false, then this will affect the diff + // accordingly. If this function isn't set, it will not be called. It + // is highly recommended to set it. The *ResourceData passed to Exists + // should _not_ be modified. Create CreateFunc Read ReadFunc Update UpdateFunc Delete DeleteFunc + Exists ExistsFunc } // See Resource documentation. @@ -59,6 +66,9 @@ type UpdateFunc func(*ResourceData, interface{}) error // See Resource documentation. type DeleteFunc func(*ResourceData, interface{}) error +// See Resource documentation. +type ExistsFunc func(*ResourceData, interface{}) (bool, error) + // Apply creates, updates, and/or deletes a resource. func (r *Resource) Apply( s *terraform.InstanceState, @@ -131,6 +141,23 @@ func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { func (r *Resource) Refresh( s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + if r.Exists != nil { + // Make a copy of data so that if it is modified it doesn't + // affect our Read later. + data, err := schemaMap(r.Schema).Data(s, nil) + if err != nil { + return s, err + } + + exists, err := r.Exists(data, meta) + if err != nil { + return s, err + } + if !exists { + return nil, nil + } + } + data, err := schemaMap(r.Schema).Data(s, nil) if err != nil { return s, err diff --git a/helper/schema/resource_test.go b/helper/schema/resource_test.go index 3c378673b..0c71abddf 100644 --- a/helper/schema/resource_test.go +++ b/helper/schema/resource_test.go @@ -410,3 +410,71 @@ func TestResourceRefresh_delete(t *testing.T) { t.Fatalf("bad: %#v", actual) } } + +func TestResourceRefresh_existsError(t *testing.T) { + r := &Resource{ + Schema: map[string]*Schema{ + "foo": &Schema{ + Type: TypeInt, + Optional: true, + }, + }, + } + + r.Exists = func(*ResourceData, interface{}) (bool, error) { + return false, fmt.Errorf("error") + } + + r.Read = func(d *ResourceData, m interface{}) error { + panic("shouldn't be called") + } + + s := &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "12", + }, + } + + actual, err := r.Refresh(s, 42) + if err == nil { + t.Fatalf("should error") + } + if !reflect.DeepEqual(actual, s) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestResourceRefresh_noExists(t *testing.T) { + r := &Resource{ + Schema: map[string]*Schema{ + "foo": &Schema{ + Type: TypeInt, + Optional: true, + }, + }, + } + + r.Exists = func(*ResourceData, interface{}) (bool, error) { + return false, nil + } + + r.Read = func(d *ResourceData, m interface{}) error { + panic("shouldn't be called") + } + + s := &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "12", + }, + } + + actual, err := r.Refresh(s, 42) + if err != nil { + t.Fatalf("err: %s", err) + } + if actual != nil { + t.Fatalf("should have no state") + } +} diff --git a/website/source/docs/plugins/provider.html.md b/website/source/docs/plugins/provider.html.md index 76c4bd003..232dd8c8c 100644 --- a/website/source/docs/plugins/provider.html.md +++ b/website/source/docs/plugins/provider.html.md @@ -158,6 +158,10 @@ The CRUD operations in more detail, along with their contracts: * `Delete` - This is called to delete the resource. Terraform guarantees an existing ID will be set. + * `Exists` - This is called to verify a resource still exists. It is + called prior to `Read`, and lowers the burden of `Read` to be able + to assume the resource exists. + ## Schemas Both providers and resources require a schema to be specified. The schema