diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 331c524582..daf598d1e0 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -683,21 +683,21 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro } if len(v.ConflictsWith) > 0 { - err := checkKeysAgainstSchemaFlags(k, v.ConflictsWith, topSchemaMap) + err := checkKeysAgainstSchemaFlags(k, v.ConflictsWith, topSchemaMap, v) if err != nil { return fmt.Errorf("ConflictsWith: %+v", err) } } if len(v.ExactlyOneOf) > 0 { - err := checkKeysAgainstSchemaFlags(k, v.ExactlyOneOf, topSchemaMap) + err := checkKeysAgainstSchemaFlags(k, v.ExactlyOneOf, topSchemaMap, v) if err != nil { return fmt.Errorf("ExactlyOneOf: %+v", err) } } if len(v.AtLeastOneOf) > 0 { - err := checkKeysAgainstSchemaFlags(k, v.AtLeastOneOf, topSchemaMap) + err := checkKeysAgainstSchemaFlags(k, v.AtLeastOneOf, topSchemaMap, v) if err != nil { return fmt.Errorf("AtLeastOneOf: %+v", err) } @@ -809,14 +809,20 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro return nil } -func checkKeysAgainstSchemaFlags(k string, keys []string, topSchemaMap schemaMap) error { +func checkKeysAgainstSchemaFlags(k string, keys []string, topSchemaMap schemaMap, self *Schema) error { for _, key := range keys { parts := strings.Split(key, ".") sm := topSchemaMap var target *Schema for _, part := range parts { - // Skip index fields - if _, err := strconv.Atoi(part); err == nil { + // Skip index fields if 0 + partInt, err := strconv.Atoi(part) + + if err == nil { + if partInt != 0 { + return fmt.Errorf("%s configuration block reference (%s) can only use the .0. index for TypeList and MaxItems: 1 configuration blocks", k, key) + } + continue } @@ -825,13 +831,27 @@ func checkKeysAgainstSchemaFlags(k string, keys []string, topSchemaMap schemaMap return fmt.Errorf("%s references unknown attribute (%s) at part (%s)", k, key, part) } - if subResource, ok := target.Elem.(*Resource); ok { - sm = schemaMap(subResource.Schema) + subResource, ok := target.Elem.(*Resource) + + if !ok { + continue + } + + if target.Type == TypeSet || target.MaxItems != 1 { + return fmt.Errorf("%s configuration block reference (%s) can only be used with TypeList and MaxItems: 1 configuration blocks", k, key) } + + sm = schemaMap(subResource.Schema) } + if target == nil { return fmt.Errorf("%s cannot find target attribute (%s), sm: %#v", k, key, sm) } + + if target == self { + return fmt.Errorf("%s cannot reference self (%s)", k, key) + } + if target.Required { return fmt.Errorf("%s cannot contain Required attribute (%s)", k, key) } diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 1f9f773ec4..5bb8ff48a5 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -3307,6 +3307,268 @@ func TestSchemaMap_InternalValidate(t *testing.T) { true, }, + "ConflictsWith list index syntax with self reference": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + }, + }, + }, + }, + true, + }, + + "ConflictsWith list index syntax with list configuration block existing attribute": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeList, + Optional: true, + MaxItems: 1, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + }, + false, + }, + + "ConflictsWith list index syntax with list configuration block missing attribute": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.missing_attr"}, + }, + }, + true, + }, + + "ConflictsWith list index syntax with list configuration block missing MaxItems": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.missing_attr"}, + }, + }, + true, + }, + + "ConflictsWith list index syntax with set configuration block existing attribute": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + }, + true, + }, + + "ConflictsWith list index syntax with set configuration block missing attribute": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.missing_attr"}, + }, + }, + true, + }, + + "ConflictsWith map key syntax with list configuration block existing attribute": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.nested_attr"}, + }, + }, + true, + }, + + "ConflictsWith map key syntax with list configuration block self reference": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + ConflictsWith: []string{"config_block_attr.nested_attr"}, + }, + }, + }, + }, + }, + true, + }, + + "ConflictsWith map key syntax with set configuration block existing attribute": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.nested_attr"}, + }, + }, + true, + }, + + "ConflictsWith map key syntax with set configuration block self reference": { + map[string]*Schema{ + "config_block_attr": { + Type: TypeSet, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "nested_attr": { + Type: TypeString, + Optional: true, + ConflictsWith: []string{"config_block_attr.nested_attr"}, + }, + }, + }, + }, + }, + true, + }, + + "ConflictsWith map key syntax with map attribute": { + map[string]*Schema{ + "map_attr": { + Type: TypeMap, + Optional: true, + Elem: &Schema{Type: TypeString}, + }, + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"map_attr.some_key"}, + }, + }, + true, + }, + + "ConflictsWith string syntax with map attribute": { + map[string]*Schema{ + "map_attr": { + Type: TypeMap, + Optional: true, + Elem: &Schema{Type: TypeString}, + }, + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"map_attr"}, + }, + }, + false, + }, + + "ConflictsWith string syntax with self reference": { + map[string]*Schema{ + "test": { + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"test"}, + }, + }, + true, + }, + "Sub-resource invalid": { map[string]*Schema{ "foo": { @@ -5402,6 +5664,401 @@ func TestSchemaMapDeepCopy(t *testing.T) { } } +func TestValidateConflictingAttributes(t *testing.T) { + cases := map[string]struct { + Key string + Schema *Schema + Config map[string]interface{} + Err bool + }{ + "root attribute self conflicting": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"self"}, + }, + Config: map[string]interface{}{ + "self": true, + }, + Err: true, + }, + + "root attribute conflicting unconfigured self unconfigured": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"root_attr"}, + }, + Config: map[string]interface{}{}, + Err: false, + }, + + "root attribute conflicting unconfigured self unknown": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"root_attr"}, + }, + Config: map[string]interface{}{ + "self": hcl2shim.UnknownVariableValue, + }, + Err: false, + }, + + "root attribute conflicting unconfigured self known": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"root_attr"}, + }, + Config: map[string]interface{}{ + "self": true, + }, + Err: false, + }, + + "root attribute conflicting unknown self unconfigured": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"root_attr"}, + }, + Config: map[string]interface{}{ + "root_attr": hcl2shim.UnknownVariableValue, + }, + Err: false, + }, + + "root attribute conflicting unknown self unknown": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"root_attr"}, + }, + Config: map[string]interface{}{ + "root_attr": hcl2shim.UnknownVariableValue, + "self": hcl2shim.UnknownVariableValue, + }, + Err: false, + }, + + "root attribute conflicting unknown self known": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"root_attr"}, + }, + Config: map[string]interface{}{ + "root_attr": hcl2shim.UnknownVariableValue, + "self": true, + }, + Err: false, + }, + + "root attribute conflicting known self unconfigured": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"root_attr"}, + }, + Config: map[string]interface{}{ + "root_attr": true, + }, + Err: true, + }, + + "root attribute conflicting known self unknown": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"root_attr"}, + }, + Config: map[string]interface{}{ + "root_attr": true, + "self": hcl2shim.UnknownVariableValue, + }, + Err: true, + }, + + "root attribute conflicting known self known": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"root_attr"}, + }, + Config: map[string]interface{}{ + "root_attr": true, + "self": true, + }, + Err: true, + }, + + "configuration block attribute list index syntax self conflicting": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.self"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "self": true, + }, + }, + }, + Err: true, + }, + + "configuration block attribute list index syntax conflicting unconfigured self unconfigured": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + Config: map[string]interface{}{}, + Err: false, + }, + + "configuration block attribute list index syntax conflicting unconfigured self unknown": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "self": hcl2shim.UnknownVariableValue, + }, + }, + }, + Err: false, + }, + + "configuration block attribute list index syntax conflicting unconfigured self known": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "self": true, + }, + }, + }, + Err: false, + }, + + "configuration block attribute list index syntax conflicting unknown self unconfigured": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "nested_attr": hcl2shim.UnknownVariableValue, + }, + }, + }, + Err: false, + }, + + "configuration block attribute list index syntax conflicting unknown self unknown": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "nested_attr": hcl2shim.UnknownVariableValue, + "self": hcl2shim.UnknownVariableValue, + }, + }, + }, + Err: false, + }, + + "configuration block attribute list index syntax conflicting unknown self known": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "nested_attr": hcl2shim.UnknownVariableValue, + "self": true, + }, + }, + }, + Err: false, + }, + + "configuration block attribute list index syntax conflicting known self unconfigured": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "nested_attr": true, + }, + }, + }, + Err: true, + }, + + "configuration block attribute list index syntax conflicting known self unknown": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "nested_attr": true, + "self": hcl2shim.UnknownVariableValue, + }, + }, + }, + Err: true, + }, + + "configuration block attribute list index syntax conflicting known self known": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.0.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "nested_attr": true, + "self": true, + }, + }, + }, + Err: true, + }, + + "configuration block attribute map key syntax self conflicting": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.self"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "self": true, + }, + }, + }, + Err: false, + }, + + "configuration block attribute map key syntax conflicting known self unconfigured": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "nested_attr": true, + }, + }, + }, + Err: false, + }, + + "configuration block attribute map key syntax conflicting known self unknown": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "nested_attr": true, + "self": hcl2shim.UnknownVariableValue, + }, + }, + }, + Err: false, + }, + + "configuration block attribute map key syntax conflicting known self known": { + Key: "self", + Schema: &Schema{ + Type: TypeBool, + Optional: true, + ConflictsWith: []string{"config_block_attr.nested_attr"}, + }, + Config: map[string]interface{}{ + "config_block_attr": []interface{}{ + map[string]interface{}{ + "nested_attr": true, + "self": true, + }, + }, + }, + Err: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + c := terraform.NewResourceConfigRaw(tc.Config) + + err := validateConflictingAttributes(tc.Key, tc.Schema, c) + if err == nil && tc.Err { + t.Fatalf("expected error") + } + + if err != nil && !tc.Err { + t.Fatalf("didn't expect error, got error: %+v", err) + } + }) + } + +} + func TestValidateExactlyOneOfAttributes(t *testing.T) { cases := map[string]struct { Key string