2017-10-04 20:27:51 +02:00
|
|
|
package schema
|
|
|
|
|
|
|
|
import (
|
2018-11-27 00:29:59 +01:00
|
|
|
"fmt"
|
2017-10-04 20:27:51 +02:00
|
|
|
"testing"
|
|
|
|
|
2018-07-12 02:59:32 +02:00
|
|
|
"github.com/google/go-cmp/cmp"
|
2017-10-04 20:27:51 +02:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
|
2018-07-05 19:33:29 +02:00
|
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
2017-10-04 20:27:51 +02:00
|
|
|
)
|
|
|
|
|
2018-07-12 02:59:32 +02:00
|
|
|
// add the implicit "id" attribute for test resources
|
|
|
|
func testResource(block *configschema.Block) *configschema.Block {
|
|
|
|
if block.Attributes == nil {
|
|
|
|
block.Attributes = make(map[string]*configschema.Attribute)
|
|
|
|
}
|
|
|
|
|
|
|
|
if block.BlockTypes == nil {
|
|
|
|
block.BlockTypes = make(map[string]*configschema.NestedBlock)
|
|
|
|
}
|
|
|
|
|
|
|
|
if block.Attributes["id"] == nil {
|
|
|
|
block.Attributes["id"] = &configschema.Attribute{
|
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
Computed: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return block
|
|
|
|
}
|
|
|
|
|
2017-10-04 20:27:51 +02:00
|
|
|
func TestSchemaMapCoreConfigSchema(t *testing.T) {
|
|
|
|
tests := map[string]struct {
|
|
|
|
Schema map[string]*Schema
|
|
|
|
Want *configschema.Block
|
|
|
|
}{
|
|
|
|
"empty": {
|
|
|
|
map[string]*Schema{},
|
2018-07-12 02:59:32 +02:00
|
|
|
testResource(&configschema.Block{}),
|
2017-10-04 20:27:51 +02:00
|
|
|
},
|
|
|
|
"primitives": {
|
|
|
|
map[string]*Schema{
|
|
|
|
"int": {
|
2018-03-16 18:43:35 +01:00
|
|
|
Type: TypeInt,
|
|
|
|
Required: true,
|
|
|
|
Description: "foo bar baz",
|
2017-10-04 20:27:51 +02:00
|
|
|
},
|
|
|
|
"float": {
|
|
|
|
Type: TypeFloat,
|
|
|
|
Optional: true,
|
|
|
|
},
|
|
|
|
"bool": {
|
|
|
|
Type: TypeBool,
|
|
|
|
Computed: true,
|
|
|
|
},
|
|
|
|
"string": {
|
|
|
|
Type: TypeString,
|
|
|
|
Optional: true,
|
|
|
|
Computed: true,
|
|
|
|
},
|
|
|
|
},
|
2018-07-12 02:59:32 +02:00
|
|
|
testResource(&configschema.Block{
|
2017-10-04 20:27:51 +02:00
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"int": {
|
2018-03-16 18:43:35 +01:00
|
|
|
Type: cty.Number,
|
|
|
|
Required: true,
|
|
|
|
Description: "foo bar baz",
|
2017-10-04 20:27:51 +02:00
|
|
|
},
|
|
|
|
"float": {
|
|
|
|
Type: cty.Number,
|
|
|
|
Optional: true,
|
|
|
|
},
|
|
|
|
"bool": {
|
|
|
|
Type: cty.Bool,
|
|
|
|
Computed: true,
|
|
|
|
},
|
|
|
|
"string": {
|
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
Computed: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{},
|
2018-07-12 02:59:32 +02:00
|
|
|
}),
|
2017-10-04 20:27:51 +02:00
|
|
|
},
|
|
|
|
"simple collections": {
|
|
|
|
map[string]*Schema{
|
|
|
|
"list": {
|
|
|
|
Type: TypeList,
|
|
|
|
Required: true,
|
|
|
|
Elem: &Schema{
|
|
|
|
Type: TypeInt,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Type: TypeSet,
|
|
|
|
Optional: true,
|
|
|
|
Elem: &Schema{
|
|
|
|
Type: TypeString,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"map": {
|
|
|
|
Type: TypeMap,
|
|
|
|
Optional: true,
|
|
|
|
Elem: &Schema{
|
|
|
|
Type: TypeBool,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"map_default_type": {
|
|
|
|
Type: TypeMap,
|
|
|
|
Optional: true,
|
|
|
|
// Maps historically don't have elements because we
|
|
|
|
// assumed they would be strings, so this needs to work
|
|
|
|
// for pre-existing schemas.
|
|
|
|
},
|
|
|
|
},
|
2018-07-12 02:59:32 +02:00
|
|
|
testResource(&configschema.Block{
|
2017-10-04 20:27:51 +02:00
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"list": {
|
|
|
|
Type: cty.List(cty.Number),
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Type: cty.Set(cty.String),
|
|
|
|
Optional: true,
|
|
|
|
},
|
|
|
|
"map": {
|
|
|
|
Type: cty.Map(cty.Bool),
|
|
|
|
Optional: true,
|
|
|
|
},
|
|
|
|
"map_default_type": {
|
|
|
|
Type: cty.Map(cty.String),
|
|
|
|
Optional: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{},
|
2018-07-12 02:59:32 +02:00
|
|
|
}),
|
2017-10-04 20:27:51 +02:00
|
|
|
},
|
2018-06-02 02:32:01 +02:00
|
|
|
"incorrectly-specified collections": {
|
|
|
|
// Historically we tolerated setting a type directly as the Elem
|
|
|
|
// attribute, rather than a Schema object. This is common enough
|
|
|
|
// in existing provider code that we must support it as an alias
|
|
|
|
// for a schema object with the given type.
|
|
|
|
map[string]*Schema{
|
|
|
|
"list": {
|
|
|
|
Type: TypeList,
|
|
|
|
Required: true,
|
|
|
|
Elem: TypeInt,
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Type: TypeSet,
|
|
|
|
Optional: true,
|
|
|
|
Elem: TypeString,
|
|
|
|
},
|
|
|
|
"map": {
|
|
|
|
Type: TypeMap,
|
|
|
|
Optional: true,
|
|
|
|
Elem: TypeBool,
|
|
|
|
},
|
|
|
|
},
|
2018-07-12 02:59:32 +02:00
|
|
|
testResource(&configschema.Block{
|
2018-06-02 02:32:01 +02:00
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"list": {
|
|
|
|
Type: cty.List(cty.Number),
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Type: cty.Set(cty.String),
|
|
|
|
Optional: true,
|
|
|
|
},
|
|
|
|
"map": {
|
|
|
|
Type: cty.Map(cty.Bool),
|
|
|
|
Optional: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{},
|
2018-07-12 02:59:32 +02:00
|
|
|
}),
|
2018-06-02 02:32:01 +02:00
|
|
|
},
|
2017-10-04 20:27:51 +02:00
|
|
|
"sub-resource collections": {
|
|
|
|
map[string]*Schema{
|
|
|
|
"list": {
|
|
|
|
Type: TypeList,
|
|
|
|
Required: true,
|
|
|
|
Elem: &Resource{
|
|
|
|
Schema: map[string]*Schema{},
|
|
|
|
},
|
|
|
|
MinItems: 1,
|
|
|
|
MaxItems: 2,
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Type: TypeSet,
|
|
|
|
Required: true,
|
|
|
|
Elem: &Resource{
|
|
|
|
Schema: map[string]*Schema{},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"map": {
|
|
|
|
Type: TypeMap,
|
|
|
|
Optional: true,
|
|
|
|
Elem: &Resource{
|
|
|
|
Schema: map[string]*Schema{},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2018-07-12 02:59:32 +02:00
|
|
|
testResource(&configschema.Block{
|
helper/schema: TypeMap of Resource is actually of TypeString
Historically helper/schema did not support non-primitive map attributes
because they cannot be represented unambiguously in flatmap. When we
initially implemented CoreConfigSchema here we mapped that situation to
a nested block of mode NestingMap, even though that'd never worked until
now, assuming that it'd be harmless because providers wouldn't be using
it.
It turns out that some providers are, in fact, incorrectly populating
a TypeMap schema with Elem: &schema.Resource, apparently under the false
assumption that it would constrain the keys allowed in the map. In
practice, helper/schema has just been ignoring this and treating such
attributes as map of string. (#20076)
In order to preserve the behavior of these existing incorrectly-specified
attribute definitions, here we mimic the helper/schema behavior by
presenting as an attribute of type map(string).
These attributes have also been shown in some documentation as nested
blocks (with no equals sign), so that'll need to be fixed in user
configurations as they upgrade to Terraform 0.12. However, the existing
upgrade tool rules will take care of that as a natural consequence of the
name being indicated as an attribute in the schema, rather than as a block
type.
This fixes #20076.
2019-01-25 02:41:30 +01:00
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
// This one becomes a string attribute because helper/schema
|
|
|
|
// doesn't actually support maps of resource. The given
|
|
|
|
// "Elem" is just ignored entirely here, which is important
|
|
|
|
// because that is also true of the helper/schema logic and
|
|
|
|
// existing providers rely on this being ignored for
|
|
|
|
// correct operation.
|
|
|
|
"map": {
|
|
|
|
Type: cty.Map(cty.String),
|
|
|
|
Optional: true,
|
|
|
|
},
|
|
|
|
},
|
2017-10-04 20:27:51 +02:00
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
|
|
"list": {
|
|
|
|
Nesting: configschema.NestingList,
|
|
|
|
Block: configschema.Block{},
|
|
|
|
MinItems: 1,
|
|
|
|
MaxItems: 2,
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Nesting: configschema.NestingSet,
|
|
|
|
Block: configschema.Block{},
|
|
|
|
MinItems: 1, // because schema is Required
|
|
|
|
},
|
|
|
|
},
|
2018-07-12 02:59:32 +02:00
|
|
|
}),
|
2017-10-04 20:27:51 +02:00
|
|
|
},
|
2018-11-27 00:44:27 +01:00
|
|
|
"sub-resource collections minitems+optional": {
|
|
|
|
// This particular case is an odd one where the provider gives
|
|
|
|
// conflicting information about whether a sub-resource is required,
|
|
|
|
// by marking it as optional but also requiring one item.
|
|
|
|
// Historically the optional-ness "won" here, and so we must
|
|
|
|
// honor that for compatibility with providers that relied on this
|
|
|
|
// undocumented interaction.
|
|
|
|
map[string]*Schema{
|
|
|
|
"list": {
|
|
|
|
Type: TypeList,
|
|
|
|
Optional: true,
|
|
|
|
Elem: &Resource{
|
|
|
|
Schema: map[string]*Schema{},
|
|
|
|
},
|
|
|
|
MinItems: 1,
|
|
|
|
MaxItems: 1,
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Type: TypeSet,
|
|
|
|
Optional: true,
|
|
|
|
Elem: &Resource{
|
|
|
|
Schema: map[string]*Schema{},
|
|
|
|
},
|
|
|
|
MinItems: 1,
|
|
|
|
MaxItems: 1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
testResource(&configschema.Block{
|
|
|
|
Attributes: map[string]*configschema.Attribute{},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
|
|
"list": {
|
|
|
|
Nesting: configschema.NestingList,
|
|
|
|
Block: configschema.Block{},
|
|
|
|
MinItems: 0,
|
|
|
|
MaxItems: 1,
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Nesting: configschema.NestingSet,
|
|
|
|
Block: configschema.Block{},
|
|
|
|
MinItems: 0,
|
|
|
|
MaxItems: 1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
"sub-resource collections minitems+computed": {
|
|
|
|
map[string]*Schema{
|
|
|
|
"list": {
|
|
|
|
Type: TypeList,
|
|
|
|
Computed: true,
|
|
|
|
Elem: &Resource{
|
|
|
|
Schema: map[string]*Schema{},
|
|
|
|
},
|
|
|
|
MinItems: 1,
|
|
|
|
MaxItems: 1,
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Type: TypeSet,
|
|
|
|
Computed: true,
|
|
|
|
Elem: &Resource{
|
|
|
|
Schema: map[string]*Schema{},
|
|
|
|
},
|
|
|
|
MinItems: 1,
|
|
|
|
MaxItems: 1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
testResource(&configschema.Block{
|
|
|
|
Attributes: map[string]*configschema.Attribute{},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
|
|
"list": {
|
|
|
|
Nesting: configschema.NestingList,
|
|
|
|
Block: configschema.Block{},
|
|
|
|
MinItems: 0,
|
|
|
|
MaxItems: 0,
|
|
|
|
},
|
|
|
|
"set": {
|
|
|
|
Nesting: configschema.NestingSet,
|
|
|
|
Block: configschema.Block{},
|
|
|
|
MinItems: 0,
|
|
|
|
MaxItems: 0,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
},
|
2017-10-04 20:27:51 +02:00
|
|
|
"nested attributes and blocks": {
|
|
|
|
map[string]*Schema{
|
|
|
|
"foo": {
|
|
|
|
Type: TypeList,
|
|
|
|
Required: true,
|
|
|
|
Elem: &Resource{
|
|
|
|
Schema: map[string]*Schema{
|
|
|
|
"bar": {
|
|
|
|
Type: TypeList,
|
|
|
|
Required: true,
|
|
|
|
Elem: &Schema{
|
|
|
|
Type: TypeList,
|
|
|
|
Elem: &Schema{
|
|
|
|
Type: TypeString,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"baz": {
|
|
|
|
Type: TypeSet,
|
|
|
|
Optional: true,
|
|
|
|
Elem: &Resource{
|
|
|
|
Schema: map[string]*Schema{},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2018-07-12 02:59:32 +02:00
|
|
|
testResource(&configschema.Block{
|
2017-10-04 20:27:51 +02:00
|
|
|
Attributes: map[string]*configschema.Attribute{},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
|
|
"foo": &configschema.NestedBlock{
|
|
|
|
Nesting: configschema.NestingList,
|
|
|
|
Block: configschema.Block{
|
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"bar": {
|
|
|
|
Type: cty.List(cty.List(cty.String)),
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
|
|
"baz": {
|
|
|
|
Nesting: configschema.NestingSet,
|
|
|
|
Block: configschema.Block{},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
MinItems: 1, // because schema is Required
|
|
|
|
},
|
|
|
|
},
|
2018-07-12 02:59:32 +02:00
|
|
|
}),
|
2017-10-04 20:27:51 +02:00
|
|
|
},
|
2017-10-04 20:38:04 +02:00
|
|
|
"sensitive": {
|
|
|
|
map[string]*Schema{
|
|
|
|
"string": {
|
|
|
|
Type: TypeString,
|
|
|
|
Optional: true,
|
|
|
|
Sensitive: true,
|
|
|
|
},
|
|
|
|
},
|
2018-07-12 02:59:32 +02:00
|
|
|
testResource(&configschema.Block{
|
2017-10-04 20:38:04 +02:00
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"string": {
|
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
Sensitive: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{},
|
2018-07-12 02:59:32 +02:00
|
|
|
}),
|
2017-10-04 20:38:04 +02:00
|
|
|
},
|
2018-11-27 00:29:59 +01:00
|
|
|
"conditionally required on": {
|
|
|
|
map[string]*Schema{
|
|
|
|
"string": {
|
|
|
|
Type: TypeString,
|
|
|
|
Required: true,
|
|
|
|
DefaultFunc: func() (interface{}, error) {
|
|
|
|
return nil, nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
testResource(&configschema.Block{
|
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"string": {
|
|
|
|
Type: cty.String,
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{},
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
"conditionally required off": {
|
|
|
|
map[string]*Schema{
|
|
|
|
"string": {
|
|
|
|
Type: TypeString,
|
|
|
|
Required: true,
|
|
|
|
DefaultFunc: func() (interface{}, error) {
|
|
|
|
// If we return a non-nil default then this overrides
|
|
|
|
// the "Required: true" for the purpose of building
|
|
|
|
// the core schema, so that core will ignore it not
|
|
|
|
// being set and let the provider handle it.
|
|
|
|
return "boop", nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
testResource(&configschema.Block{
|
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"string": {
|
|
|
|
Type: cty.String,
|
|
|
|
Optional: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{},
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
"conditionally required error": {
|
|
|
|
map[string]*Schema{
|
|
|
|
"string": {
|
|
|
|
Type: TypeString,
|
|
|
|
Required: true,
|
|
|
|
DefaultFunc: func() (interface{}, error) {
|
|
|
|
return nil, fmt.Errorf("placeholder error")
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
testResource(&configschema.Block{
|
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"string": {
|
|
|
|
Type: cty.String,
|
|
|
|
Optional: true, // Just so we can progress to provider-driven validation and return the error there
|
|
|
|
},
|
|
|
|
},
|
|
|
|
BlockTypes: map[string]*configschema.NestedBlock{},
|
|
|
|
}),
|
|
|
|
},
|
2017-10-04 20:27:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for name, test := range tests {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2018-07-19 19:53:31 +02:00
|
|
|
got := (&Resource{Schema: test.Schema}).CoreConfigSchema()
|
|
|
|
if !cmp.Equal(got, test.Want, equateEmpty, typeComparer) {
|
|
|
|
t.Error(cmp.Diff(got, test.Want, equateEmpty, typeComparer))
|
2017-10-04 20:27:51 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|