diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 2ac38803aa..3e5358b0d6 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -162,13 +162,30 @@ type Schema struct { // used to wrap a complex structure, however less than one instance would // cause instability. // - // PromoteSingle, if true, will allow single elements to be standalone - // and promote them to a list. For example "foo" would be promoted to - // ["foo"] automatically. This is primarily for legacy reasons and the - // ambiguity is not recommended for new usage. Promotion is only allowed - // for primitive element types. - MaxItems int - MinItems int + // If the field Optional is set to true then MinItems is ignored and thus + // effectively zero. + // + // If MaxItems is 1, you may optionally also set AsSingle in order to have + // Terraform v0.12 or later treat a TypeList or TypeSet as if it were a + // single value. It will remain a list or set in Terraform v0.10 and v0.11. + // Enabling this for an existing attribute after you've made at least one + // v0.12-compatible provider release is a breaking change. AsSingle is + // likely to misbehave when used with deeply-nested set structures due to + // the imprecision of set diffs, so be sure to test it thoroughly, + // including updates that change the set members at all levels. AsSingle + // exists primarily to be used in conjunction with ConfigMode when forcing + // a nested resource to be treated as an attribute, so it can be considered + // an attribute of object type rather than of list/set of object. + MaxItems int + MinItems int + AsSingle bool + + // PromoteSingle originally allowed for a single element to be assigned + // where a primitive list was expected, but this no longer works from + // Terraform v0.12 onwards (Terraform Core will require a list to be set + // regardless of what this is set to) and so only applies to Terraform v0.11 + // and earlier, and so should be used only to retain this functionality + // for those still using v0.11 with a provider that formerly used this. PromoteSingle bool // The following fields are only valid for a TypeSet type. @@ -775,6 +792,15 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro } } + if v.AsSingle { + if v.MaxItems != 1 { + return fmt.Errorf("%s: MaxItems must be 1 when AsSingle is set", k) + } + if v.Type != TypeList && v.Type != TypeSet { + return fmt.Errorf("%s: AsSingle can be used only with TypeList and TypeSet schemas", k) + } + } + // Computed-only field if v.Computed && !v.Optional { if v.ValidateFunc != nil { diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 52d2a1961a..938681cb7a 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -3890,6 +3890,68 @@ func TestSchemaMap_InternalValidate(t *testing.T) { }, true, // in *schema.Resource with ConfigMode of attribute, so must also have ConfigMode of attribute }, + + "AsSingle okay": { + map[string]*Schema{ + "block": &Schema{ + Type: TypeList, + Optional: true, + MaxItems: 1, + AsSingle: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "sub": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{}, + }, + }, + }, + }, + }, + false, + }, + + "AsSingle without MaxItems": { + map[string]*Schema{ + "block": &Schema{ + Type: TypeList, + Optional: true, + AsSingle: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "sub": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{}, + }, + }, + }, + }, + }, + true, // MaxItems must be 1 when AsSingle is set + }, + + "AsSingle on primitive type": { + map[string]*Schema{ + "block": &Schema{ + Type: TypeString, + Optional: true, + MaxItems: 1, + AsSingle: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "sub": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{}, + }, + }, + }, + }, + }, + true, // Unexpected error occurred: block: MaxItems and MinItems are only supported on lists or sets + }, } for tn, tc := range cases {