diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 2bfaebaec..cfe3c6a33 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -99,7 +99,13 @@ type Schema struct { // TypeList, and represents what the element type is. If it is *Schema, // the element type is just a simple value. If it is *Resource, the // element type is a complex structure, potentially with its own lifecycle. - Elem interface{} + // + // MaxItems defines a maximum amount of items that can exist within a + // TypeSet or TypeList. Specific use cases would be if a TypeSet is being + // used to wrap a complex structure, however more than one instance would + // cause instability. + Elem interface{} + MaxItems int // The following fields are only valid for a TypeSet type. // @@ -541,6 +547,10 @@ func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error { "%s: Elem must have only Type set", k) } } + } else { + if v.MaxItems > 0 { + return fmt.Errorf("%s: MaxItems is only supported on lists or sets", k) + } } if v.ValidateFunc != nil { @@ -1049,6 +1059,12 @@ func (m schemaMap) validateList( "%s: should be a list", k)} } + // Validate length + if schema.MaxItems > 0 && rawV.Len() > schema.MaxItems { + return nil, []error{fmt.Errorf( + "%s: attribute supports %d item maximum, config has %d declared", k, schema.MaxItems, rawV.Len())} + } + // Now build the []interface{} raws := make([]interface{}, rawV.Len()) for i, _ := range raws { diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 5ddbf4a24..94c9c57ff 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -3647,3 +3647,94 @@ func TestSchemaMap_Validate(t *testing.T) { } } } + +func TestSchemaSet_ValidateMaxItems(t *testing.T) { + cases := map[string]struct { + Schema map[string]*Schema + State *terraform.InstanceState + Config map[string]interface{} + ConfigVariables map[string]string + Diff *terraform.InstanceDiff + Err bool + Errors []error + }{ + "#0": { + Schema: map[string]*Schema{ + "aliases": &Schema{ + Type: TypeSet, + Optional: true, + MaxItems: 1, + Elem: &Schema{Type: TypeString}, + }, + }, + State: nil, + Config: map[string]interface{}{ + "aliases": []interface{}{"foo", "bar"}, + }, + Diff: nil, + Err: true, + Errors: []error{ + fmt.Errorf("aliases: attribute supports 1 item maximum, config has 2 declared"), + }, + }, + "#1": { + Schema: map[string]*Schema{ + "aliases": &Schema{ + Type: TypeSet, + Optional: true, + Elem: &Schema{Type: TypeString}, + }, + }, + State: nil, + Config: map[string]interface{}{ + "aliases": []interface{}{"foo", "bar"}, + }, + Diff: nil, + Err: false, + Errors: nil, + }, + "#2": { + Schema: map[string]*Schema{ + "aliases": &Schema{ + Type: TypeSet, + Optional: true, + MaxItems: 1, + Elem: &Schema{Type: TypeString}, + }, + }, + State: nil, + Config: map[string]interface{}{ + "aliases": []interface{}{"foo"}, + }, + Diff: nil, + Err: false, + Errors: nil, + }, + } + + for tn, tc := range cases { + c, err := config.NewRawConfig(tc.Config) + if err != nil { + t.Fatalf("%q: err: %s", tn, err) + } + _, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c)) + + if len(es) > 0 != tc.Err { + if len(es) == 0 { + t.Errorf("%q: no errors", tn) + } + + for _, e := range es { + t.Errorf("%q: err: %s", tn, e) + } + + t.FailNow() + } + + if tc.Errors != nil { + if !reflect.DeepEqual(es, tc.Errors) { + t.Fatalf("%q: expected: %q\ngot: %q", tn, tc.Errors, es) + } + } + } +}