Revert "helper/schema: Implementation of the AsSingle mechanism"

This reverts commit 1987a92386.
This commit is contained in:
James Bardin 2019-04-08 15:43:32 -04:00
parent 4dbe6add77
commit 1a9c06d0f5
10 changed files with 25 additions and 936 deletions

View File

@ -18,7 +18,6 @@ func Provider() terraform.ResourceProvider {
},
ResourcesMap: map[string]*schema.Resource{
"test_resource": testResource(),
"test_resource_as_single": testResourceAsSingle(),
"test_resource_gh12183": testResourceGH12183(),
"test_resource_import_other": testResourceImportOther(),
"test_resource_with_custom_diff": testResourceCustomDiff(),

View File

@ -1,158 +0,0 @@
package test
import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
)
func testResourceAsSingle() *schema.Resource {
return &schema.Resource{
Create: testResourceAsSingleCreate,
Read: testResourceAsSingleRead,
Delete: testResourceAsSingleDelete,
Update: testResourceAsSingleUpdate,
Schema: map[string]*schema.Schema{
"list_resource_as_block": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
"list_resource_as_attr": {
Type: schema.TypeList,
ConfigMode: schema.SchemaConfigModeAttr,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
"list_primitive": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"set_resource_as_block": {
Type: schema.TypeSet,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
"set_resource_as_attr": {
Type: schema.TypeSet,
ConfigMode: schema.SchemaConfigModeAttr,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
"set_primitive": {
Type: schema.TypeSet,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}
func testResourceAsSingleCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("placeholder")
return testResourceAsSingleRead(d, meta)
}
func testResourceAsSingleRead(d *schema.ResourceData, meta interface{}) error {
for _, k := range []string{"list_resource_as_block", "list_resource_as_attr", "list_primitive"} {
v := d.Get(k)
if v == nil {
continue
}
if l, ok := v.([]interface{}); !ok {
return fmt.Errorf("%s should appear as []interface {}, not %T", k, v)
} else {
for i, item := range l {
switch k {
case "list_primitive":
if _, ok := item.(string); item != nil && !ok {
return fmt.Errorf("%s[%d] should appear as string, not %T", k, i, item)
}
default:
if _, ok := item.(map[string]interface{}); item != nil && !ok {
return fmt.Errorf("%s[%d] should appear as map[string]interface {}, not %T", k, i, item)
}
}
}
}
}
for _, k := range []string{"set_resource_as_block", "set_resource_as_attr", "set_primitive"} {
v := d.Get(k)
if v == nil {
continue
}
if s, ok := v.(*schema.Set); !ok {
return fmt.Errorf("%s should appear as *schema.Set, not %T", k, v)
} else {
for i, item := range s.List() {
switch k {
case "set_primitive":
if _, ok := item.(string); item != nil && !ok {
return fmt.Errorf("%s[%d] should appear as string, not %T", k, i, item)
}
default:
if _, ok := item.(map[string]interface{}); item != nil && !ok {
return fmt.Errorf("%s[%d] should appear as map[string]interface {}, not %T", k, i, item)
}
}
}
}
}
return nil
}
func testResourceAsSingleUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceAsSingleRead(d, meta)
}
func testResourceAsSingleDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,114 +0,0 @@
package test
import (
"strings"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceAsSingle(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_as_single" "foo" {
list_resource_as_block {
foo = "as block a"
}
list_resource_as_attr = {
foo = "as attr a"
}
list_primitive = "primitive a"
set_resource_as_block {
foo = "as block a"
}
set_resource_as_attr = {
foo = "as attr a"
}
set_primitive = "primitive a"
}
`),
Check: resource.ComposeTestCheckFunc(
func(s *terraform.State) error {
t.Log("state after initial create:\n", s.String())
return nil
},
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_block.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_block.0.foo", "as block a"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_attr.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_attr.0.foo", "as attr a"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_primitive.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_primitive.0", "primitive a"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_block.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_block.1417230722.foo", "as block a"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_attr.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_attr.2549052262.foo", "as attr a"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_primitive.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_primitive.247272358", "primitive a"),
),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_as_single" "foo" {
list_resource_as_block {
foo = "as block b"
}
list_resource_as_attr = {
foo = "as attr b"
}
list_primitive = "primitive b"
set_resource_as_block {
foo = "as block b"
}
set_resource_as_attr = {
foo = "as attr b"
}
set_primitive = "primitive b"
}
`),
Check: resource.ComposeTestCheckFunc(
func(s *terraform.State) error {
t.Log("state after update:\n", s.String())
return nil
},
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_block.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_block.0.foo", "as block b"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_attr.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_attr.0.foo", "as attr b"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_primitive.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_primitive.0", "primitive b"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_block.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_block.2136238657.foo", "as block b"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_attr.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_attr.3166838949.foo", "as attr b"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_primitive.#", "1"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_primitive.630210661", "primitive b"),
),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_as_single" "foo" {
}
`),
Check: resource.ComposeTestCheckFunc(
func(s *terraform.State) error {
t.Log("state after everything unset:\n", s.String())
return nil
},
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_block.#", "0"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_resource_as_attr.#", "0"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "list_primitive.#", "0"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_block.#", "0"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_resource_as_attr.#", "0"),
resource.TestCheckResourceAttr("test_resource_as_single.foo", "set_primitive.#", "0"),
),
},
},
})
}

View File

@ -275,12 +275,6 @@ func (s *GRPCProviderServer) UpgradeResourceState(_ context.Context, req *proto.
}
}
// NOTE WELL: The AsSingle mechanism cannot be automatically normalized here,
// so providers that use it must be ready to handle both normalized and
// unnormalized input in their upgrade codepaths. The _result_ of an upgrade
// should set a single-element list/set for any AsSingle element so that it
// can be normalized to a single value automatically on return.
// We first need to upgrade a flatmap state if it exists.
// There should never be both a JSON and Flatmap state in the request.
if req.RawState.Flatmap != nil {
@ -298,8 +292,6 @@ func (s *GRPCProviderServer) UpgradeResourceState(_ context.Context, req *proto.
return resp, nil
}
schema.FixupAsSingleConfigValueOut(jsonMap, s.provider.ResourcesMap[req.TypeName].Schema)
// now we need to turn the state into the default json representation, so
// that it can be re-decoded using the actual schema.
val, err := schema.JSONMapToStateValue(jsonMap, blockForShimming)
@ -468,7 +460,6 @@ func (s *GRPCProviderServer) ReadResource(_ context.Context, req *proto.ReadReso
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
// res.ShimInstanceStateFromValue result has already had FixupAsSingleInstanceStateIn applied
newInstanceState, err := res.RefreshWithoutUpgrade(instanceState, s.provider.Meta())
if err != nil {
@ -560,7 +551,6 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
// res.ShimInstanceStateFromValue result has already had FixupAsSingleInstanceStateIn applied
priorPrivate := make(map[string]interface{})
if len(req.PriorPrivate) > 0 {
if err := json.Unmarshal(req.PriorPrivate, &priorPrivate); err != nil {
@ -612,17 +602,7 @@ func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.Pl
}
// now we need to apply the diff to the prior state, so get the planned state
plannedAttrs, err := diff.Apply(priorState.Attributes, res.CoreConfigSchemaWhenShimmed())
schema.FixupAsSingleInstanceStateOut(
&terraform.InstanceState{Attributes: plannedAttrs},
s.provider.ResourcesMap[req.TypeName],
)
// We also fix up the diff for AsSingle here, but note that we intentionally
// do it _after_ diff.Apply (so that the state can have its own fixup applied)
// but before we deal with requiresNew below so that fixing up the diff
// also fixes up the requiresNew keys to match.
schema.FixupAsSingleInstanceDiffOut(diff, s.provider.ResourcesMap[req.TypeName])
plannedAttrs, err := diff.Apply(priorState.Attributes, block)
plannedStateVal, err := hcl2shim.HCL2ValueFromFlatmap(plannedAttrs, blockForShimming.ImpliedType())
if err != nil {
@ -763,7 +743,6 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
// res.ShimInstanceStateFromValue result has already had FixupAsSingleInstanceStateIn applied
private := make(map[string]interface{})
if len(req.PlannedPrivate) > 0 {
@ -785,10 +764,7 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
Destroy: true,
}
} else {
diff, err = schema.DiffFromValues(
priorStateVal, plannedStateVal,
stripResourceModifiers(res),
)
diff, err = schema.DiffFromValues(priorStateVal, plannedStateVal, stripResourceModifiers(res))
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
@ -802,10 +778,6 @@ func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.A
}
}
// NOTE WELL: schema.DiffFromValues has already effectively applied
// schema.FixupAsSingleInstanceDiffIn to the diff, so we need not (and must not)
// repeat that here.
// We need to fix any sets that may be using the "~" index prefix to
// indicate partially computed. The special sigil isn't really used except
// as a clue to visually indicate that the set isn't wholly known.
@ -934,8 +906,6 @@ func (s *GRPCProviderServer) ImportResourceState(_ context.Context, req *proto.I
// copy the ID again just to be sure it wasn't missed
is.Attributes["id"] = is.ID
schema.FixupAsSingleInstanceStateOut(is, s.provider.ResourcesMap[req.TypeName])
resourceType := is.Ephemeral.Type
if resourceType == "" {
resourceType = req.TypeName
@ -1014,7 +984,6 @@ func (s *GRPCProviderServer) ReadDataSource(_ context.Context, req *proto.ReadDa
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
schema.FixupAsSingleInstanceStateOut(newInstanceState, s.provider.DataSourcesMap[req.TypeName])
newStateVal, err := schema.StateValueFromInstanceState(newInstanceState, blockForShimming.ImpliedType())
if err != nil {

View File

@ -4,11 +4,13 @@ import (
"fmt"
"github.com/hashicorp/terraform/addrs"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
)
// shimState takes a new *states.State and reverts it to a legacy state for the provider ACC tests
@ -153,7 +155,6 @@ func shimmedAttributes(instance *states.ResourceInstanceObjectSrc, res *schema.R
}
instanceState, err := res.ShimInstanceStateFromValue(rio.Value)
// instanceState has already had the AsSingle fixup applied because ShimInstanceStateFromValue calls FixupAsSingleInstanceStateIn.
if err != nil {
return nil, err
}

View File

@ -1,286 +0,0 @@
package schema
import (
"sort"
"strings"
"github.com/hashicorp/terraform/terraform"
)
// FixupAsSingleResourceConfigIn modifies the given ResourceConfig in-place if
// any attributes in the schema have the AsSingle flag set, wrapping the given
// values for these in an extra level of slice so that they can be understood
// by legacy SDK code that'll be expecting to decode into a list/set.
func FixupAsSingleResourceConfigIn(rc *terraform.ResourceConfig, s map[string]*Schema) {
if rc == nil {
return
}
FixupAsSingleConfigValueIn(rc.Config, s)
}
// FixupAsSingleInstanceStateIn modifies the given InstanceState in-place if
// any attributes in the schema have the AsSingle flag set, adding additional
// index steps to the flatmap keys for these so that they can be understood
// by legacy SDK code that'll be expecting to decode into a list/set.
func FixupAsSingleInstanceStateIn(is *terraform.InstanceState, r *Resource) {
fixupAsSingleInstanceState(is, r.Schema, "", fixupAsSingleFlatmapKeysIn)
}
// FixupAsSingleInstanceStateOut modifies the given InstanceState in-place if
// any attributes in the schema have the AsSingle flag set, removing unneeded
// index steps from the flatmap keys for these so that they can be understood
// by the shim back to Terraform Core as a single nested value.
func FixupAsSingleInstanceStateOut(is *terraform.InstanceState, r *Resource) {
fixupAsSingleInstanceState(is, r.Schema, "", fixupAsSingleFlatmapKeysOut)
}
// FixupAsSingleInstanceDiffIn modifies the given InstanceDiff in-place if any
// attributes in the schema have the AsSingle flag set, adding additional index
// steps to the flatmap keys for these so that they can be understood by legacy
// SDK code that'll be expecting to decode into a list/set.
func FixupAsSingleInstanceDiffIn(id *terraform.InstanceDiff, r *Resource) {
fixupAsSingleInstanceDiff(id, r.Schema, "", fixupAsSingleAttrsMapKeysIn)
}
// FixupAsSingleInstanceDiffOut modifies the given InstanceDiff in-place if any
// attributes in the schema have the AsSingle flag set, removing unneeded index
// steps from the flatmap keys for these so that they can be understood by the
// shim back to Terraform Core as a single nested value.
func FixupAsSingleInstanceDiffOut(id *terraform.InstanceDiff, r *Resource) {
fixupAsSingleInstanceDiff(id, r.Schema, "", fixupAsSingleAttrsMapKeysOut)
}
// FixupAsSingleConfigValueIn modifies the given "config value" in-place if
// any attributes in the schema have the AsSingle flag set, wrapping the given
// values for these in an extra level of slice so that they can be understood
// by legacy SDK code that'll be expecting to decode into a list/set.
//
// "Config value" for the purpose of this function has the same meaning as for
// the hcl2shims: a map[string]interface{} using the same subset of Go value
// types that would be generated by HCL/HIL when decoding a configuration in
// Terraform v0.11.
func FixupAsSingleConfigValueIn(c map[string]interface{}, s map[string]*Schema) {
for k, as := range s {
if !as.AsSingle {
continue // Don't touch non-AsSingle values at all. This is explicitly opt-in.
}
v, ok := c[k]
if ok {
c[k] = []interface{}{v}
}
if nr, ok := as.Elem.(*Resource); ok {
// Recursively fixup nested attributes too
nm, ok := v.(map[string]interface{})
if !ok {
// Weird for a nested resource to not be a map, but we'll tolerate it rather than crashing
continue
}
FixupAsSingleConfigValueIn(nm, nr.Schema)
}
}
}
// FixupAsSingleConfigValueOut modifies the given "config value" in-place if
// any attributes in the schema have the AsSingle flag set, unwrapping the
// given values from their single-element slices so that they can be understood
// as a single object value by Terraform Core.
//
// This is the opposite of fixupAsSingleConfigValueIn.
func FixupAsSingleConfigValueOut(c map[string]interface{}, s map[string]*Schema) {
for k, as := range s {
if !as.AsSingle {
continue // Don't touch non-AsSingle values at all. This is explicitly opt-in.
}
sv, ok := c[k].([]interface{})
if ok && len(sv) != 0 { // Should always be a single-element slice, but if not we'll just leave it alone rather than crashing
c[k] = sv[0]
if nr, ok := as.Elem.(*Resource); ok {
// Recursively fixup nested attributes too
nm, ok := sv[0].(map[string]interface{})
if ok {
FixupAsSingleConfigValueOut(nm, nr.Schema)
}
}
}
}
}
func fixupAsSingleInstanceState(is *terraform.InstanceState, s map[string]*Schema, prefix string, fn func(map[string]string, string) string) {
if is == nil {
return
}
for k, as := range s {
if !as.AsSingle {
continue // Don't touch non-AsSingle values at all. This is explicitly opt-in.
}
nextPrefix := fn(is.Attributes, prefix+k+".")
if nr, ok := as.Elem.(*Resource); ok {
// Recursively fixup nested attributes too
fixupAsSingleInstanceState(is, nr.Schema, nextPrefix, fn)
}
}
}
func fixupAsSingleInstanceDiff(id *terraform.InstanceDiff, s map[string]*Schema, prefix string, fn func(map[string]*terraform.ResourceAttrDiff, string) string) {
if id == nil {
return
}
for k, as := range s {
if !as.AsSingle {
continue // Don't touch non-AsSingle values at all. This is explicitly opt-in.
}
nextPrefix := fn(id.Attributes, prefix+k+".")
if nr, ok := as.Elem.(*Resource); ok {
// Recursively fixup nested attributes too
fixupAsSingleInstanceDiff(id, nr.Schema, nextPrefix, fn)
}
}
}
// fixupAsSingleFlatmapKeysIn searches the given flatmap for all keys with
// the given prefix (which must end with a dot) and replaces them with keys
// where that prefix is followed by the dummy index "0." and, if any such
// keys are found, a ".#"-suffixed key is also added whose value is "1".
//
// This function will also replace an exact match of the given prefix with
// the trailing dot removed, to recognize values of primitive-typed attributes.
func fixupAsSingleFlatmapKeysIn(attrs map[string]string, prefix string) string {
ks := make([]string, 0, len(attrs))
for k := range attrs {
ks = append(ks, k)
}
sort.Strings(ks) // Makes no difference for valid input, but will ensure we handle invalid input deterministically
for _, k := range ks {
newK, countK := fixupAsSingleFlatmapKeyIn(k, prefix)
if _, exists := attrs[newK]; k != newK && !exists {
attrs[newK] = attrs[k]
delete(attrs, k)
}
if _, exists := attrs[countK]; countK != "" && !exists {
attrs[countK] = "1"
}
}
return prefix + "0."
}
// fixupAsSingleAttrsMapKeysIn searches the given AttrDiff map for all keys with
// the given prefix (which must end with a dot) and replaces them with keys
// where that prefix is followed by the dummy index "0." and, if any such
// keys are found, a ".#"-suffixed key is also added whose value is "1".
//
// This function will also replace an exact match of the given prefix with
// the trailing dot removed, to recognize values of primitive-typed attributes.
func fixupAsSingleAttrsMapKeysIn(attrs map[string]*terraform.ResourceAttrDiff, prefix string) string {
ks := make([]string, 0, len(attrs))
for k := range attrs {
ks = append(ks, k)
}
sort.Strings(ks) // Makes no difference for valid input, but will ensure we handle invalid input deterministically
for _, k := range ks {
newK, countK := fixupAsSingleFlatmapKeyIn(k, prefix)
if _, exists := attrs[newK]; k != newK && !exists {
attrs[newK] = attrs[k]
delete(attrs, k)
}
if _, exists := attrs[countK]; countK != "" && !exists {
attrs[countK] = &terraform.ResourceAttrDiff{
Old: "1", // One should _always_ be present, so this seems okay?
New: "1",
}
}
}
return prefix + "0."
}
func fixupAsSingleFlatmapKeyIn(k, prefix string) (string, string) {
exact := prefix[:len(prefix)-1]
switch {
case k == exact:
return exact + ".0", exact + ".#"
case strings.HasPrefix(k, prefix):
return prefix + "0." + k[len(prefix):], prefix + "#"
default:
return k, ""
}
}
// fixupAsSingleFlatmapKeysOut searches the given flatmap for all keys with
// the given prefix (which must end with a dot) and replaces them with keys
// where the following dot-separated label is removed, under the assumption that
// it's an index that is no longer needed and, if such a key is present, also
// remove the "count" key for the prefix, which is the prefix followed by "#".
func fixupAsSingleFlatmapKeysOut(attrs map[string]string, prefix string) string {
ks := make([]string, 0, len(attrs))
for k := range attrs {
ks = append(ks, k)
}
sort.Strings(ks) // Makes no difference for valid input, but will ensure we handle invalid input deterministically
for _, k := range ks {
newK := fixupAsSingleFlatmapKeyOut(k, prefix)
if newK != k && newK == "" {
delete(attrs, k)
} else if _, exists := attrs[newK]; newK != k && !exists {
attrs[newK] = attrs[k]
delete(attrs, k)
}
}
delete(attrs, prefix+"#") // drop the count key, if it's present
return prefix
}
// fixupAsSingleAttrsMapKeysOut searches the given AttrDiff map for all keys with
// the given prefix (which must end with a dot) and replaces them with keys
// where the following dot-separated label is removed, under the assumption that
// it's an index that is no longer needed and, if such a key is present, also
// remove the "count" key for the prefix, which is the prefix followed by "#".
func fixupAsSingleAttrsMapKeysOut(attrs map[string]*terraform.ResourceAttrDiff, prefix string) string {
ks := make([]string, 0, len(attrs))
for k := range attrs {
ks = append(ks, k)
}
sort.Strings(ks) // Makes no difference for valid input, but will ensure we handle invalid input deterministically
for _, k := range ks {
newK := fixupAsSingleFlatmapKeyOut(k, prefix)
if newK != k && newK == "" {
delete(attrs, k)
} else if _, exists := attrs[newK]; newK != k && !exists {
attrs[newK] = attrs[k]
delete(attrs, k)
}
}
delete(attrs, prefix+"#") // drop the count key, if it's present
return prefix
}
func fixupAsSingleFlatmapKeyOut(k, prefix string) string {
if strings.HasPrefix(k, prefix) {
remain := k[len(prefix):]
if remain == "#" {
// Don't need the count element anymore
return ""
}
dotIdx := strings.Index(remain, ".")
if dotIdx == -1 {
return prefix[:len(prefix)-1] // no follow-on attributes then
} else {
return prefix + remain[dotIdx+1:] // everything after the next dot
}
}
return k
}

View File

@ -1,239 +0,0 @@
package schema
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/terraform"
)
func TestFixupAsSingleInstanceStateInOut(t *testing.T) {
tests := map[string]struct {
AttrsIn map[string]string
AttrsOut map[string]string
Schema map[string]*Schema
}{
"empty": {
nil,
nil,
nil,
},
"simple": {
map[string]string{
"a": "a value",
},
map[string]string{
"a": "a value",
},
map[string]*Schema{
"a": {Type: TypeString, Optional: true},
},
},
"normal list of primitive, empty": {
map[string]string{
"a.%": "0",
},
map[string]string{
"a.%": "0",
},
map[string]*Schema{
"a": {
Type: TypeList,
Optional: true,
Elem: &Schema{Type: TypeString},
},
},
},
"normal list of primitive, single": {
map[string]string{
"a.%": "1",
"a.0": "hello",
},
map[string]string{
"a.%": "1",
"a.0": "hello",
},
map[string]*Schema{
"a": {
Type: TypeList,
Optional: true,
Elem: &Schema{Type: TypeString},
},
},
},
"AsSingle list of primitive": {
map[string]string{
"a": "hello",
},
map[string]string{
"a.#": "1",
"a.0": "hello",
},
map[string]*Schema{
"a": {
Type: TypeList,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &Schema{Type: TypeString},
},
},
},
"AsSingle list of resource": {
map[string]string{
"a.b": "hello",
},
map[string]string{
"a.#": "1",
"a.0.b": "hello",
},
map[string]*Schema{
"a": {
Type: TypeList,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &Resource{
Schema: map[string]*Schema{
"b": {
Type: TypeString,
Optional: true,
},
},
},
},
},
},
"AsSingle list of resource with nested primitive list": {
map[string]string{
"a.b.#": "1",
"a.b.0": "hello",
},
map[string]string{
"a.#": "1",
"a.0.b.#": "1",
"a.0.b.0": "hello",
},
map[string]*Schema{
"a": {
Type: TypeList,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &Resource{
Schema: map[string]*Schema{
"b": {
Type: TypeList,
Optional: true,
Elem: &Schema{Type: TypeString},
},
},
},
},
},
},
"AsSingle list of resource with nested AsSingle primitive list": {
map[string]string{
"a.b": "hello",
},
map[string]string{
"a.#": "1",
"a.0.b.#": "1",
"a.0.b.0": "hello",
},
map[string]*Schema{
"a": {
Type: TypeList,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &Resource{
Schema: map[string]*Schema{
"b": {
Type: TypeList,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &Schema{Type: TypeString},
},
},
},
},
},
},
"AsSingle list of resource with nested AsSingle resource list": {
map[string]string{
"a.b.c": "hello",
},
map[string]string{
"a.#": "1",
"a.0.b.#": "1",
"a.0.b.0.c": "hello",
},
map[string]*Schema{
"a": {
Type: TypeList,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &Resource{
Schema: map[string]*Schema{
"b": {
Type: TypeList,
Optional: true,
MaxItems: 1,
AsSingle: true,
Elem: &Resource{
Schema: map[string]*Schema{
"c": {
Type: TypeString,
Optional: true,
},
},
},
},
},
},
},
},
},
}
copyMap := func(m map[string]string) map[string]string {
if m == nil {
return nil
}
ret := make(map[string]string, len(m))
for k, v := range m {
ret[k] = v
}
return ret
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Run("In", func(t *testing.T) {
input := copyMap(test.AttrsIn)
is := &terraform.InstanceState{
Attributes: input,
}
r := &Resource{Schema: test.Schema}
FixupAsSingleInstanceStateIn(is, r)
if !cmp.Equal(is.Attributes, test.AttrsOut) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v\n\n%s", input, is.Attributes, test.AttrsOut, cmp.Diff(test.AttrsOut, is.Attributes))
}
})
t.Run("Out", func(t *testing.T) {
input := copyMap(test.AttrsOut)
is := &terraform.InstanceState{
Attributes: input,
}
r := &Resource{Schema: test.Schema}
FixupAsSingleInstanceStateOut(is, r)
if !cmp.Equal(is.Attributes, test.AttrsIn) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v\n\n%s", input, is.Attributes, test.AttrsIn, cmp.Diff(test.AttrsIn, is.Attributes))
}
})
})
}
}

View File

@ -22,35 +22,6 @@ import (
// This method presumes a schema that passes InternalValidate, and so may
// panic or produce an invalid result if given an invalid schemaMap.
func (m schemaMap) CoreConfigSchema() *configschema.Block {
return m.coreConfigSchema(true, true)
}
// CoreConfigSchemaForShimming is a variant of CoreConfigSchema that returns
// the schema that should be used when applying our shimming behaviors.
//
// In particular, it ignores the SkipCoreTypeCheck flag on any legacy schemas,
// since the shims live on the SDK side and so they need to see the full
// type information that we'd normally hide from Terraform Core when skipping
// type checking over there.
func (m schemaMap) CoreConfigSchemaForShimming() *configschema.Block {
return m.coreConfigSchema(true, false)
}
// CoreConfigSchemaWhenShimmed is a variant of CoreConfigSchema that returns
// the schema as it would appear when working with data structures that have
// already been shimmed to the legacy form.
//
// In particular, it ignores the AsSingle flag on any legacy schemas and behaves
// as if they were really lists/sets instead, thus giving a description of
// the shape of the data structure after the AsSingle fixup has been applied.
//
// This should be used with care only in unusual situations where we need to
// work with an already-shimmed value using a new-style schema.
func (m schemaMap) CoreConfigSchemaWhenShimmed() *configschema.Block {
return m.coreConfigSchema(false, false)
}
func (m schemaMap) coreConfigSchema(asSingle, skipCoreCheck bool) *configschema.Block {
if len(m) == 0 {
// We return an actual (empty) object here, rather than a nil,
// because a nil result would mean that we don't have a schema at
@ -65,7 +36,7 @@ func (m schemaMap) coreConfigSchema(asSingle, skipCoreCheck bool) *configschema.
for name, schema := range m {
if schema.Elem == nil {
ret.Attributes[name] = schema.coreConfigSchemaAttribute(asSingle, skipCoreCheck)
ret.Attributes[name] = schema.coreConfigSchemaAttribute()
continue
}
if schema.Type == TypeMap {
@ -79,27 +50,27 @@ func (m schemaMap) coreConfigSchema(asSingle, skipCoreCheck bool) *configschema.
sch.Elem = &Schema{
Type: TypeString,
}
ret.Attributes[name] = sch.coreConfigSchemaAttribute(asSingle, skipCoreCheck)
ret.Attributes[name] = sch.coreConfigSchemaAttribute()
continue
}
}
switch schema.ConfigMode {
case SchemaConfigModeAttr:
ret.Attributes[name] = schema.coreConfigSchemaAttribute(asSingle, skipCoreCheck)
ret.Attributes[name] = schema.coreConfigSchemaAttribute()
case SchemaConfigModeBlock:
ret.BlockTypes[name] = schema.coreConfigSchemaBlock(asSingle, skipCoreCheck)
ret.BlockTypes[name] = schema.coreConfigSchemaBlock()
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(asSingle, skipCoreCheck)
ret.Attributes[name] = schema.coreConfigSchemaAttribute()
continue
}
switch schema.Elem.(type) {
case *Schema, ValueType:
ret.Attributes[name] = schema.coreConfigSchemaAttribute(asSingle, skipCoreCheck)
ret.Attributes[name] = schema.coreConfigSchemaAttribute()
case *Resource:
ret.BlockTypes[name] = schema.coreConfigSchemaBlock(asSingle, skipCoreCheck)
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))
@ -114,7 +85,7 @@ func (m schemaMap) coreConfigSchema(asSingle, skipCoreCheck bool) *configschema.
// of a schema. This is appropriate only for primitives or collections whose
// Elem is an instance of Schema. Use coreConfigSchemaBlock for collections
// whose elem is a whole resource.
func (s *Schema) coreConfigSchemaAttribute(asSingle, skipCoreCheck bool) *configschema.Attribute {
func (s *Schema) coreConfigSchemaAttribute() *configschema.Attribute {
// The Schema.DefaultFunc capability adds some extra weirdness here since
// it can be combined with "Required: true" to create a sitution where
// required-ness is conditional. Terraform Core doesn't share this concept,
@ -145,7 +116,7 @@ func (s *Schema) coreConfigSchemaAttribute(asSingle, skipCoreCheck bool) *config
}
return &configschema.Attribute{
Type: s.coreConfigSchemaType(asSingle, skipCoreCheck),
Type: s.coreConfigSchemaType(),
Optional: opt,
Required: reqd,
Computed: s.Computed,
@ -157,9 +128,9 @@ func (s *Schema) coreConfigSchemaAttribute(asSingle, skipCoreCheck bool) *config
// coreConfigSchemaBlock prepares a configschema.NestedBlock representation of
// a schema. This is appropriate only for collections whose Elem is an instance
// of Resource, and will panic otherwise.
func (s *Schema) coreConfigSchemaBlock(asSingle, skipCoreCheck bool) *configschema.NestedBlock {
func (s *Schema) coreConfigSchemaBlock() *configschema.NestedBlock {
ret := &configschema.NestedBlock{}
if nested := schemaMap(s.Elem.(*Resource).Schema).coreConfigSchema(asSingle, skipCoreCheck); nested != nil {
if nested := s.Elem.(*Resource).coreConfigSchema(); nested != nil {
ret.Block = *nested
}
switch s.Type {
@ -177,14 +148,6 @@ func (s *Schema) coreConfigSchemaBlock(asSingle, skipCoreCheck bool) *configsche
ret.MinItems = s.MinItems
ret.MaxItems = s.MaxItems
if s.AsSingle && asSingle {
// In AsSingle mode, we artifically force a TypeList or TypeSet
// attribute in the SDK to be treated as a single block by Terraform Core.
// This must then be fixed up in the shim code (in helper/plugin) so
// that the SDK still sees the lists or sets it's expecting.
ret.Nesting = configschema.NestingSingle
}
if s.Required && s.MinItems == 0 {
// configschema doesn't have a "required" representation for nested
// blocks, but we can fake it by requiring at least one item.
@ -210,15 +173,7 @@ func (s *Schema) coreConfigSchemaBlock(asSingle, skipCoreCheck bool) *configsche
// coreConfigSchemaType determines the core config schema type that corresponds
// to a particular schema's type.
func (s *Schema) coreConfigSchemaType(asSingle, skipCoreCheck bool) cty.Type {
if skipCoreCheck && s.SkipCoreTypeCheck {
// If we're preparing a schema for Terraform Core and the schema is
// asking us to skip the Core type-check then we'll tell core that this
// attribute is dynamically-typed, so it'll just pass through anything
// and let us validate it on the plugin side.
return cty.DynamicPseudoType
}
func (s *Schema) coreConfigSchemaType() cty.Type {
switch s.Type {
case TypeString:
return cty.String
@ -233,17 +188,17 @@ func (s *Schema) coreConfigSchemaType(asSingle, skipCoreCheck bool) cty.Type {
var elemType cty.Type
switch set := s.Elem.(type) {
case *Schema:
elemType = set.coreConfigSchemaType(asSingle, skipCoreCheck)
elemType = set.coreConfigSchemaType()
case ValueType:
// This represents a mistake in the provider code, but it's a
// common one so we'll just shim it.
elemType = (&Schema{Type: set}).coreConfigSchemaType(asSingle, skipCoreCheck)
elemType = (&Schema{Type: set}).coreConfigSchemaType()
case *Resource:
// 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 = schemaMap(set.Schema).coreConfigSchema(asSingle, skipCoreCheck).ImpliedType()
elemType = set.coreConfigSchema().ImpliedType()
default:
if set != nil {
// Should never happen for a valid schema
@ -253,13 +208,6 @@ func (s *Schema) coreConfigSchemaType(asSingle, skipCoreCheck bool) cty.Type {
// to be compatible with them.
elemType = cty.String
}
if s.AsSingle && asSingle {
// In AsSingle mode, we artifically force a TypeList or TypeSet
// attribute in the SDK to be treated as a single value by Terraform Core.
// This must then be fixed up in the shim code (in helper/plugin) so
// that the SDK still sees the lists or sets it's expecting.
return elemType
}
switch s.Type {
case TypeList:
return cty.List(elemType)
@ -281,34 +229,7 @@ func (s *Schema) coreConfigSchemaType(asSingle, skipCoreCheck bool) cty.Type {
// the resource's schema. CoreConfigSchema adds the implicitly required "id"
// attribute for top level resources if it doesn't exist.
func (r *Resource) CoreConfigSchema() *configschema.Block {
return r.coreConfigSchema(true, true)
}
// CoreConfigSchemaForShimming is a variant of CoreConfigSchema that returns
// the schema that should be used to apply shims on the SDK side.
//
// In particular, it ignores the SkipCoreTypeCheck flag on any legacy schemas
// and uses the real type information instead.
func (r *Resource) CoreConfigSchemaForShimming() *configschema.Block {
return r.coreConfigSchema(true, false)
}
// CoreConfigSchemaWhenShimmed is a variant of CoreConfigSchema that returns
// the schema as it would appear when working with data structures that have
// already been shimmed to the legacy form.
//
// In particular, it ignores the AsSingle flag on any legacy schemas and behaves
// as if they were really lists/sets instead, thus giving a description of
// the shape of the data structure after the AsSingle fixup has been applied.
//
// This should be used with care only in unusual situations where we need to
// work with an already-shimmed value using a new-style schema.
func (r *Resource) CoreConfigSchemaWhenShimmed() *configschema.Block {
return r.coreConfigSchema(false, false)
}
func (r *Resource) coreConfigSchema(asSingle, skipCoreCheck bool) *configschema.Block {
block := schemaMap(r.Schema).coreConfigSchema(asSingle, skipCoreCheck)
block := r.coreConfigSchema()
if block.Attributes == nil {
block.Attributes = map[string]*configschema.Attribute{}
@ -377,6 +298,10 @@ func (r *Resource) coreConfigSchema(asSingle, skipCoreCheck bool) *configschema.
return block
}
func (r *Resource) coreConfigSchema() *configschema.Block {
return schemaMap(r.Schema).CoreConfigSchema()
}
// CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema
// on the backends's schema.
func (r *Backend) CoreConfigSchema() *configschema.Block {

View File

@ -162,8 +162,6 @@ func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.Insta
// match those from the Schema.
s := terraform.NewInstanceStateShimmedFromValue(state, r.SchemaVersion)
FixupAsSingleInstanceStateIn(s, r)
// We now rebuild the state through the ResourceData, so that the set indexes
// match what helper/schema expects.
data, err := schemaMap(r.Schema).Data(s, nil)

View File

@ -14,10 +14,6 @@ import (
// derives a terraform.InstanceDiff to give to the legacy providers. This is
// used to take the states provided by the new ApplyResourceChange method and
// convert them to a state+diff required for the legacy Apply method.
//
// If the fixup function is non-nil, it will be called with the constructed
// shimmed InstanceState and ResourceConfig values to do any necessary in-place
// mutations before producing the diff.
func DiffFromValues(prior, planned cty.Value, res *Resource) (*terraform.InstanceDiff, error) {
return diffFromValues(prior, planned, res, nil)
}
@ -28,7 +24,6 @@ func DiffFromValues(prior, planned cty.Value, res *Resource) (*terraform.Instanc
// have already been done.
func diffFromValues(prior, planned cty.Value, res *Resource, cust CustomizeDiffFunc) (*terraform.InstanceDiff, error) {
instanceState, err := res.ShimInstanceStateFromValue(prior)
// The result of ShimInstanceStateFromValue already has FixupAsSingleInstanceStateIn applied
if err != nil {
return nil, err
}
@ -36,7 +31,6 @@ func diffFromValues(prior, planned cty.Value, res *Resource, cust CustomizeDiffF
configSchema := res.CoreConfigSchema()
cfg := terraform.NewResourceConfigShimmed(planned, configSchema)
FixupAsSingleResourceConfigIn(cfg, schemaMap(res.Schema))
diff, err := schemaMap(res.Schema).Diff(instanceState, cfg, cust, nil, false)
if err != nil {