helper/schema: Honor ConfigMode when building core schema

This makes some slight adjustments to the shape of the schema we
present to Terraform Core without affecting how it is consumed by the
SDK and thus the provider. This mechanism is designed specifically to
avoid changing how the schema is interpreted by the SDK itself or by the
provider, so that prior behavior can be preserved in Terraform v0.11 mode.

This also includes a new rule that Computed-only (i.e. not also Optional)
schemas _always_ map to attributes, because that is a better mapping of
the intent: they are object values to be used in expressions. Nested
blocks conceptually represent nested objects that are in some sense
independent of what they are embedded in, and so they cannot themselves be
computed.
This commit is contained in:
Martin Atkins 2019-03-08 16:31:22 -08:00
parent a6d322edec
commit 4de0b33097
5 changed files with 150 additions and 19 deletions

View File

@ -33,6 +33,7 @@ func Provider() terraform.ResourceProvider {
"test_resource_list_set": testResourceListSet(),
"test_resource_map": testResourceMap(),
"test_resource_computed_set": testResourceComputedSet(),
"test_resource_config_mode": testResourceConfigMode(),
"test_resource_nested_id": testResourceNestedId(),
},
DataSourcesMap: map[string]*schema.Resource{

View File

@ -0,0 +1,60 @@
package test
import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
)
func testResourceConfigMode() *schema.Resource {
return &schema.Resource{
Create: testResourceConfigModeCreate,
Read: testResourceConfigModeRead,
Delete: testResourceConfigModeDelete,
Update: testResourceConfigModeUpdate,
Schema: map[string]*schema.Schema{
"resource_as_attr": {
Type: schema.TypeList,
ConfigMode: schema.SchemaConfigModeAttr,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
},
}
}
func testResourceConfigModeCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("placeholder")
return testResourceConfigModeRead(d, meta)
}
func testResourceConfigModeRead(d *schema.ResourceData, meta interface{}) error {
const k = "resource_as_attr"
if l, ok := d.Get(k).([]interface{}); !ok {
return fmt.Errorf("%s should appear as []interface{}, not %T", k, l)
} else {
for i, item := range l {
if _, ok := item.(map[string]interface{}); !ok {
return fmt.Errorf("%s[%d] should appear as map[string]interface{}, not %T", k, i, item)
}
}
}
return nil
}
func testResourceConfigModeUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceConfigModeRead(d, meta)
}
func testResourceConfigModeDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -0,0 +1,61 @@
package test
import (
"strings"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestResourceConfigMode(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_config_mode" "foo" {
resource_as_attr = [
{
foo = "resource_as_attr 0"
},
{
foo = "resource_as_attr 1"
},
]
}
`),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("test_resource_config_mode.foo", "resource_as_attr.#", "2"),
resource.TestCheckResourceAttr("test_resource_config_mode.foo", "resource_as_attr.0.foo", "resource_as_attr 0"),
resource.TestCheckResourceAttr("test_resource_config_mode.foo", "resource_as_attr.1.foo", "resource_as_attr 1"),
),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_config_mode" "foo" {
resource_as_attr = [
{
foo = "resource_as_attr 0 updated"
},
]
}
`),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("test_resource_config_mode.foo", "resource_as_attr.#", "1"),
resource.TestCheckResourceAttr("test_resource_config_mode.foo", "resource_as_attr.0.foo", "resource_as_attr 0 updated"),
),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_config_mode" "foo" {
resource_as_attr = []
}
`),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("test_resource_config_mode.foo", "resource_as_attr.#", "0"),
),
},
},
})
}

View File

@ -54,14 +54,27 @@ func (m schemaMap) CoreConfigSchema() *configschema.Block {
continue
}
}
switch schema.Elem.(type) {
case *Schema, ValueType:
switch schema.ConfigMode {
case SchemaConfigModeAttr:
ret.Attributes[name] = schema.coreConfigSchemaAttribute()
case *Resource:
case SchemaConfigModeBlock:
ret.BlockTypes[name] = schema.coreConfigSchemaBlock()
default:
// Should never happen for a valid schema
panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", schema.Elem))
default: // SchemaConfigModeAuto, or any other invalid value
if schema.Computed && !schema.Optional {
// Computed-only schemas are always handled as attributes,
// because they never appear in configuration.
ret.Attributes[name] = schema.coreConfigSchemaAttribute()
continue
}
switch schema.Elem.(type) {
case *Schema, ValueType:
ret.Attributes[name] = schema.coreConfigSchemaAttribute()
case *Resource:
ret.BlockTypes[name] = schema.coreConfigSchemaBlock()
default:
// Should never happen for a valid schema
panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", schema.Elem))
}
}
}
@ -181,9 +194,10 @@ func (s *Schema) coreConfigSchemaType() cty.Type {
// common one so we'll just shim it.
elemType = (&Schema{Type: set}).coreConfigSchemaType()
case *Resource:
// In practice we don't actually use this for normal schema
// construction because we construct a NestedBlock in that
// case instead. See schemaMap.CoreConfigSchema.
// By default we construct a NestedBlock in this case, but this
// behavior is selected either for computed-only schemas or
// when ConfigMode is explicitly SchemaConfigModeBlock.
// See schemaMap.CoreConfigSchema for the exact rules.
elemType = set.coreConfigSchema().ImpliedType()
default:
if set != nil {

View File

@ -298,19 +298,14 @@ func TestSchemaMapCoreConfigSchema(t *testing.T) {
},
},
testResource(&configschema.Block{
Attributes: map[string]*configschema.Attribute{},
BlockTypes: map[string]*configschema.NestedBlock{
Attributes: map[string]*configschema.Attribute{
"list": {
Nesting: configschema.NestingList,
Block: configschema.Block{},
MinItems: 0,
MaxItems: 0,
Type: cty.List(cty.EmptyObject),
Computed: true,
},
"set": {
Nesting: configschema.NestingSet,
Block: configschema.Block{},
MinItems: 0,
MaxItems: 0,
Type: cty.Set(cty.EmptyObject),
Computed: true,
},
},
}),