Skip to content
103 changes: 103 additions & 0 deletions helper/schema/resource_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import (
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/go-cty/cty/gocty"

"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/hcl2shim"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugin/convert"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

Expand Down Expand Up @@ -59,6 +63,105 @@ type getResult struct {
Schema *Schema
}

// TfTypeIdentityState returns the identity data as a tftypes.Value.
func (d *ResourceData) TfTypeIdentityState() (*tftypes.Value, error) {
s := schemaMap(d.identitySchema).CoreConfigSchema()

state := d.State()

if state == nil {
return nil, fmt.Errorf("state is nil, call SetId() on ResourceData first")
}

stateVal, err := hcl2shim.HCL2ValueFromFlatmap(state.Identity, s.ImpliedType())
if err != nil {
return nil, fmt.Errorf("converting identity flatmap to cty value: %+v", err)
}

return convert.ToTfValue(stateVal)
}

// TfTypeResourceState returns the resource data as a tftypes.Value.
func (d *ResourceData) TfTypeResourceState() (*tftypes.Value, error) {
s := schemaMap(d.schema).CoreConfigSchema()

// The CoreConfigSchema method on schemaMaps doesn't automatically handle adding the id
// attribute or timeouts like the method on Resource does
if _, ok := s.Attributes["id"]; !ok {
s.Attributes["id"] = &configschema.Attribute{
Type: cty.String,
Optional: true,
Computed: true,
}
}

_, timeoutsAttr := s.Attributes[TimeoutsConfigKey]
_, timeoutsBlock := s.BlockTypes[TimeoutsConfigKey]

if d.timeouts != nil && !timeoutsAttr && !timeoutsBlock {
timeouts := configschema.Block{
Attributes: map[string]*configschema.Attribute{},
}

if d.timeouts.Create != nil {
timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}

if d.timeouts.Read != nil {
timeouts.Attributes[TimeoutRead] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}

if d.timeouts.Update != nil {
timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}

if d.timeouts.Delete != nil {
timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}

if d.timeouts.Default != nil {
timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{
Type: cty.String,
Optional: true,
}
}

if len(timeouts.Attributes) != 0 {
s.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{
Nesting: configschema.NestingSingle,
Block: timeouts,
}
}
}

state := d.State()
if state == nil {
return nil, fmt.Errorf("state is nil, call SetId() on ResourceData first")
}

// Although we handle adding/omitting timeouts to the schema depending on how it's been defined on the resource
// we don't process or convert the timeout values since they reside in Meta and aren't needed for the purposes
// of this function and in the context of a List.
stateVal, err := hcl2shim.HCL2ValueFromFlatmap(state.Attributes, s.ImpliedType())
if err != nil {
return nil, fmt.Errorf("converting resource state flatmap to cty value: %+v", err)
}

return convert.ToTfValue(stateVal)
}

// Get returns the data for the given key, or nil if the key doesn't exist
// in the schema.
//
Expand Down
178 changes: 178 additions & 0 deletions helper/schema/resource_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-cty/cty"

"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
Expand Down Expand Up @@ -4314,6 +4315,183 @@ func TestResourceDataIdentity_no_schema(t *testing.T) {
}
}

func TestResourceData_TfTypeIdentityState(t *testing.T) {
d := &ResourceData{
identitySchema: map[string]*Schema{
"foo": {
Type: TypeString,
RequiredForImport: true,
},
},
}

d.SetId("baz") // just required to be able to call .State()

identity, err := d.Identity()
if err != nil {
t.Fatalf("err: %s", err)
}

err = identity.Set("foo", "bar")
if err != nil {
t.Fatalf("err: %s", err)
}

expectedIdentity := tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"foo": tftypes.String,
}}, map[string]tftypes.Value{
"foo": tftypes.NewValue(tftypes.String, "bar"),
})

tfTypeIdentity, err := d.TfTypeIdentityState()
if err != nil {
t.Fatalf("err: %s", err)
}

if !tfTypeIdentity.Equal(expectedIdentity) {
t.Fatalf("expected tftype value of identity to be %+v, got %+v", expectedIdentity, tfTypeIdentity)
}
}

func TestResourceData_TfTypeResourceState(t *testing.T) {
cases := []struct {
d *ResourceData
expected tftypes.Value
}{
{
d: &ResourceData{
schema: map[string]*Schema{
"location": {
Type: TypeString,
Optional: true,
},
},
timeouts: timeoutForValues(30, 5, 30, 5, 5),
},
expected: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"location": tftypes.String,
"id": tftypes.String,
"timeouts": tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"create": tftypes.String,
"delete": tftypes.String,
"read": tftypes.String,
"update": tftypes.String,
"default": tftypes.String,
},
},
}}, map[string]tftypes.Value{
"location": tftypes.NewValue(tftypes.String, "westeurope"),
"id": tftypes.NewValue(tftypes.String, "baz"),
"timeouts": tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"create": tftypes.String,
"default": tftypes.String,
"delete": tftypes.String,
"read": tftypes.String,
"update": tftypes.String,
},
}, map[string]tftypes.Value{
"create": tftypes.NewValue(tftypes.String, nil),
"default": tftypes.NewValue(tftypes.String, nil),
"delete": tftypes.NewValue(tftypes.String, nil),
"read": tftypes.NewValue(tftypes.String, nil),
"update": tftypes.NewValue(tftypes.String, nil),
}),
}),
},
{
d: &ResourceData{
schema: map[string]*Schema{
"location": {
Type: TypeString,
Optional: true,
},
},
},
expected: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"location": tftypes.String,
"id": tftypes.String,
}}, map[string]tftypes.Value{
"location": tftypes.NewValue(tftypes.String, "westeurope"),
"id": tftypes.NewValue(tftypes.String, "baz"),
}),
},
{
d: &ResourceData{
schema: map[string]*Schema{
"location": {
Type: TypeString,
Optional: true,
},
},
timeouts: &ResourceTimeout{},
},
expected: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"location": tftypes.String,
"id": tftypes.String,
}}, map[string]tftypes.Value{
"location": tftypes.NewValue(tftypes.String, "westeurope"),
"id": tftypes.NewValue(tftypes.String, "baz"),
}),
},
{
d: &ResourceData{
schema: map[string]*Schema{
"location": {
Type: TypeString,
Optional: true,
},
},
timeouts: &ResourceTimeout{
Create: DefaultTimeout(30 * time.Minute),
},
},
expected: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"location": tftypes.String,
"id": tftypes.String,
"timeouts": tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"create": tftypes.String,
},
},
}}, map[string]tftypes.Value{
"location": tftypes.NewValue(tftypes.String, "westeurope"),
"id": tftypes.NewValue(tftypes.String, "baz"),
"timeouts": tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"create": tftypes.String,
},
}, map[string]tftypes.Value{
"create": tftypes.NewValue(tftypes.String, nil),
}),
}),
},
}

for _, tc := range cases {
tc.d.SetId("baz") // just required to be able to call .State()

if err := tc.d.Set("location", "westeurope"); err != nil {
t.Fatalf("err: %s", err)
}

tfTypeIdentity, err := tc.d.TfTypeResourceState()
if err != nil {
t.Fatalf("err: %s", err)
}

if !tfTypeIdentity.Equal(tc.expected) {
t.Fatalf("expected tftype value of identity to be %+v, got %+v", tc.expected, tfTypeIdentity)
}
}
}

func testPtrTo(raw interface{}) interface{} {
return &raw
}
Loading
Loading