diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 25ef5e169b..7c8bed10ce 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -634,6 +634,19 @@ func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error { return nil } +func (m schemaMap) markAsRemoved(k string, schema *Schema, diff *terraform.InstanceDiff) { + existingDiff, ok := diff.Attributes[k] + if ok { + existingDiff.NewRemoved = true + diff.Attributes[k] = schema.finalizeDiff(existingDiff) + return + } + + diff.Attributes[k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{ + NewRemoved: true, + }) +} + func (m schemaMap) diff( k string, schema *Schema, @@ -757,6 +770,7 @@ func (m schemaMap) diffList( switch t := schema.Elem.(type) { case *Resource: + countDiff, cOk := diff.GetAttribute(k + ".#") // This is a complex resource for i := 0; i < maxLen; i++ { for k2, schema := range t.Schema { @@ -765,6 +779,15 @@ func (m schemaMap) diffList( if err != nil { return err } + + // If parent list is being removed + // remove all subfields which were missed by the diff func + // We process these separately because type-specific diff functions + // lack the context (hierarchy of fields) + subKeyIsCount := strings.HasSuffix(subK, ".#") + if cOk && countDiff.New == "0" && !subKeyIsCount { + m.markAsRemoved(subK, schema, diff) + } } } case *Schema: diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 33ac26f763..7fa7f0835d 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -2651,6 +2651,274 @@ func TestSchemaMap_Diff(t *testing.T) { }, }, }, + + { + Name: "Removal of TypeList should cause nested Bool fields w/ Default to be removed too", + Schema: map[string]*Schema{ + "deployment_group_name": &Schema{ + Type: TypeString, + Required: true, + ForceNew: true, + }, + + "alarm_configuration": &Schema{ + Type: TypeList, + Optional: true, + MaxItems: 1, + Elem: &Resource{ + Schema: map[string]*Schema{ + "alarms": &Schema{ + Type: TypeSet, + Optional: true, + Set: HashString, + Elem: &Schema{Type: TypeString}, + }, + + "enabled": &Schema{ + Type: TypeBool, + Optional: true, + }, + + "ignore_poll_alarm_failure": &Schema{ + Type: TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, + }, + + State: &terraform.InstanceState{ + Attributes: map[string]string{ + "alarm_configuration.#": "1", + "alarm_configuration.0.alarms.#": "1", + "alarm_configuration.0.alarms.2356372769": "foo", + "alarm_configuration.0.enabled": "true", + "alarm_configuration.0.ignore_poll_alarm_failure": "false", + "deployment_group_name": "foo-group-32345345345", + }, + }, + + Config: map[string]interface{}{ + "deployment_group_name": "foo-group-32345345345", + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "alarm_configuration.#": &terraform.ResourceAttrDiff{ + Old: "1", + New: "0", + NewRemoved: false, + }, + "alarm_configuration.0.alarms": &terraform.ResourceAttrDiff{ + Old: "", + New: "", + NewRemoved: true, + }, + "alarm_configuration.0.alarms.#": &terraform.ResourceAttrDiff{ + Old: "1", + New: "0", + NewRemoved: false, + }, + "alarm_configuration.0.alarms.2356372769": &terraform.ResourceAttrDiff{ + Old: "foo", + New: "", + NewRemoved: true, + }, + "alarm_configuration.0.enabled": &terraform.ResourceAttrDiff{ + Old: "true", + New: "false", + NewRemoved: true, + }, + "alarm_configuration.0.ignore_poll_alarm_failure": &terraform.ResourceAttrDiff{ + Old: "", + New: "", + NewRemoved: true, + }, + }, + }, + }, + + { + Name: "Removal of TypeList should cause all empty nested String fields to be removed too", + Schema: map[string]*Schema{ + "bucket": { + Type: TypeString, + Required: true, + ForceNew: true, + }, + + "acl": { + Type: TypeString, + Default: "private", + Optional: true, + }, + + "website": { + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "index_document": { + Type: TypeString, + Optional: true, + }, + + "error_document": { + Type: TypeString, + Optional: true, + }, + + "redirect_all_requests_to": { + Type: TypeString, + Optional: true, + }, + + "routing_rules": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + }, + + State: &terraform.InstanceState{ + Attributes: map[string]string{ + "acl": "public-read", + "bucket": "tf-test-bucket-5011072831090096749", + "website.#": "1", + "website.0.error_document": "error.html", + "website.0.index_document": "index.html", + "website.0.redirect_all_requests_to": "", + }, + }, + + Config: map[string]interface{}{ + "acl": "public-read", + "bucket": "tf-test-bucket-5011072831090096749", + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "website.#": &terraform.ResourceAttrDiff{ + Old: "1", + New: "0", + NewRemoved: false, + }, + "website.0.index_document": &terraform.ResourceAttrDiff{ + Old: "index.html", + New: "", + NewRemoved: true, + }, + "website.0.error_document": &terraform.ResourceAttrDiff{ + Old: "error.html", + New: "", + NewRemoved: true, + }, + "website.0.redirect_all_requests_to": &terraform.ResourceAttrDiff{ + Old: "", + New: "", + NewRemoved: true, + }, + "website.0.routing_rules": &terraform.ResourceAttrDiff{ + Old: "", + New: "", + NewRemoved: true, + }, + }, + }, + }, + + { + Name: "Removal of TypeList should cause nested Int fields w/ Default to be removed too", + Schema: map[string]*Schema{ + "availability_zones": &Schema{ + Type: TypeSet, + Elem: &Schema{Type: TypeString}, + Optional: true, + Computed: true, + Set: HashString, + }, + + "access_logs": &Schema{ + Type: TypeList, + Optional: true, + MaxItems: 1, + Elem: &Resource{ + Schema: map[string]*Schema{ + "interval": &Schema{ + Type: TypeInt, + Optional: true, + Default: 60, + }, + "bucket": &Schema{ + Type: TypeString, + Required: true, + }, + "bucket_prefix": &Schema{ + Type: TypeString, + Optional: true, + }, + "enabled": &Schema{ + Type: TypeBool, + Optional: true, + Default: true, + }, + }, + }, + }, + }, + + State: &terraform.InstanceState{ + Attributes: map[string]string{ + "access_logs.#": "1", + "access_logs.0.bucket": "terraform-access-logs-bucket-5906065226840117876", + "access_logs.0.bucket_prefix": "", + "access_logs.0.enabled": "true", + "access_logs.0.interval": "5", + "availability_zones.#": "3", + "availability_zones.2050015877": "us-west-2c", + "availability_zones.221770259": "us-west-2b", + "availability_zones.2487133097": "us-west-2a", + }, + }, + + Config: map[string]interface{}{ + "availability_zones": []interface{}{"us-west-2a", "us-west-2b", "us-west-2c"}, + }, + + Diff: &terraform.InstanceDiff{ + Attributes: map[string]*terraform.ResourceAttrDiff{ + "access_logs.#": &terraform.ResourceAttrDiff{ + Old: "1", + New: "0", + NewRemoved: false, + }, + "access_logs.0.bucket": &terraform.ResourceAttrDiff{ + Old: "terraform-access-logs-bucket-5906065226840117876", + New: "", + NewRemoved: true, + }, + "access_logs.0.bucket_prefix": &terraform.ResourceAttrDiff{ + Old: "", + New: "", + NewRemoved: true, + }, + "access_logs.0.enabled": &terraform.ResourceAttrDiff{ + Old: "", + New: "", + NewRemoved: true, + }, + "access_logs.0.interval": &terraform.ResourceAttrDiff{ + Old: "5", + New: "60", + NewRemoved: true, + }, + }, + }, + }, } for i, tc := range cases {