Skip to content

Commit f982e31

Browse files
authored
list: add conversion functions for cty values to tftypes values (#1508)
* add conversion functions for cty values to tftypes values * handle nil primitives * add conversion function for cty type to tftype type * add tests for value and type conversion functions * headers * rename empty value method, check is value is Unknown and expand testing * add tests for resource data tftype receiver funcs * thread through error handling * fix issue when adding timeouts to schema and handle unknown values
1 parent 66313d0 commit f982e31

File tree

4 files changed

+1107
-0
lines changed

4 files changed

+1107
-0
lines changed

helper/schema/resource_data.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import (
1515
"github.com/hashicorp/go-cty/cty"
1616
"github.com/hashicorp/go-cty/cty/gocty"
1717

18+
"github.com/hashicorp/terraform-plugin-go/tftypes"
1819
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
20+
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema"
21+
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/hcl2shim"
22+
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugin/convert"
1923
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
2024
)
2125

@@ -59,6 +63,105 @@ type getResult struct {
5963
Schema *Schema
6064
}
6165

66+
// TfTypeIdentityState returns the identity data as a tftypes.Value.
67+
func (d *ResourceData) TfTypeIdentityState() (*tftypes.Value, error) {
68+
s := schemaMap(d.identitySchema).CoreConfigSchema()
69+
70+
state := d.State()
71+
72+
if state == nil {
73+
return nil, fmt.Errorf("state is nil, call SetId() on ResourceData first")
74+
}
75+
76+
stateVal, err := hcl2shim.HCL2ValueFromFlatmap(state.Identity, s.ImpliedType())
77+
if err != nil {
78+
return nil, fmt.Errorf("converting identity flatmap to cty value: %+v", err)
79+
}
80+
81+
return convert.ToTfValue(stateVal)
82+
}
83+
84+
// TfTypeResourceState returns the resource data as a tftypes.Value.
85+
func (d *ResourceData) TfTypeResourceState() (*tftypes.Value, error) {
86+
s := schemaMap(d.schema).CoreConfigSchema()
87+
88+
// The CoreConfigSchema method on schemaMaps doesn't automatically handle adding the id
89+
// attribute or timeouts like the method on Resource does
90+
if _, ok := s.Attributes["id"]; !ok {
91+
s.Attributes["id"] = &configschema.Attribute{
92+
Type: cty.String,
93+
Optional: true,
94+
Computed: true,
95+
}
96+
}
97+
98+
_, timeoutsAttr := s.Attributes[TimeoutsConfigKey]
99+
_, timeoutsBlock := s.BlockTypes[TimeoutsConfigKey]
100+
101+
if d.timeouts != nil && !timeoutsAttr && !timeoutsBlock {
102+
timeouts := configschema.Block{
103+
Attributes: map[string]*configschema.Attribute{},
104+
}
105+
106+
if d.timeouts.Create != nil {
107+
timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{
108+
Type: cty.String,
109+
Optional: true,
110+
}
111+
}
112+
113+
if d.timeouts.Read != nil {
114+
timeouts.Attributes[TimeoutRead] = &configschema.Attribute{
115+
Type: cty.String,
116+
Optional: true,
117+
}
118+
}
119+
120+
if d.timeouts.Update != nil {
121+
timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{
122+
Type: cty.String,
123+
Optional: true,
124+
}
125+
}
126+
127+
if d.timeouts.Delete != nil {
128+
timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{
129+
Type: cty.String,
130+
Optional: true,
131+
}
132+
}
133+
134+
if d.timeouts.Default != nil {
135+
timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{
136+
Type: cty.String,
137+
Optional: true,
138+
}
139+
}
140+
141+
if len(timeouts.Attributes) != 0 {
142+
s.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{
143+
Nesting: configschema.NestingSingle,
144+
Block: timeouts,
145+
}
146+
}
147+
}
148+
149+
state := d.State()
150+
if state == nil {
151+
return nil, fmt.Errorf("state is nil, call SetId() on ResourceData first")
152+
}
153+
154+
// Although we handle adding/omitting timeouts to the schema depending on how it's been defined on the resource
155+
// we don't process or convert the timeout values since they reside in Meta and aren't needed for the purposes
156+
// of this function and in the context of a List.
157+
stateVal, err := hcl2shim.HCL2ValueFromFlatmap(state.Attributes, s.ImpliedType())
158+
if err != nil {
159+
return nil, fmt.Errorf("converting resource state flatmap to cty value: %+v", err)
160+
}
161+
162+
return convert.ToTfValue(stateVal)
163+
}
164+
62165
// Get returns the data for the given key, or nil if the key doesn't exist
63166
// in the schema.
64167
//

helper/schema/resource_data_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/google/go-cmp/cmp"
1414
"github.com/hashicorp/go-cty/cty"
1515

16+
"github.com/hashicorp/terraform-plugin-go/tftypes"
1617
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1718
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
1819
)
@@ -4314,6 +4315,183 @@ func TestResourceDataIdentity_no_schema(t *testing.T) {
43144315
}
43154316
}
43164317

4318+
func TestResourceData_TfTypeIdentityState(t *testing.T) {
4319+
d := &ResourceData{
4320+
identitySchema: map[string]*Schema{
4321+
"foo": {
4322+
Type: TypeString,
4323+
RequiredForImport: true,
4324+
},
4325+
},
4326+
}
4327+
4328+
d.SetId("baz") // just required to be able to call .State()
4329+
4330+
identity, err := d.Identity()
4331+
if err != nil {
4332+
t.Fatalf("err: %s", err)
4333+
}
4334+
4335+
err = identity.Set("foo", "bar")
4336+
if err != nil {
4337+
t.Fatalf("err: %s", err)
4338+
}
4339+
4340+
expectedIdentity := tftypes.NewValue(tftypes.Object{
4341+
AttributeTypes: map[string]tftypes.Type{
4342+
"foo": tftypes.String,
4343+
}}, map[string]tftypes.Value{
4344+
"foo": tftypes.NewValue(tftypes.String, "bar"),
4345+
})
4346+
4347+
tfTypeIdentity, err := d.TfTypeIdentityState()
4348+
if err != nil {
4349+
t.Fatalf("err: %s", err)
4350+
}
4351+
4352+
if !tfTypeIdentity.Equal(expectedIdentity) {
4353+
t.Fatalf("expected tftype value of identity to be %+v, got %+v", expectedIdentity, tfTypeIdentity)
4354+
}
4355+
}
4356+
4357+
func TestResourceData_TfTypeResourceState(t *testing.T) {
4358+
cases := []struct {
4359+
d *ResourceData
4360+
expected tftypes.Value
4361+
}{
4362+
{
4363+
d: &ResourceData{
4364+
schema: map[string]*Schema{
4365+
"location": {
4366+
Type: TypeString,
4367+
Optional: true,
4368+
},
4369+
},
4370+
timeouts: timeoutForValues(30, 5, 30, 5, 5),
4371+
},
4372+
expected: tftypes.NewValue(tftypes.Object{
4373+
AttributeTypes: map[string]tftypes.Type{
4374+
"location": tftypes.String,
4375+
"id": tftypes.String,
4376+
"timeouts": tftypes.Object{
4377+
AttributeTypes: map[string]tftypes.Type{
4378+
"create": tftypes.String,
4379+
"delete": tftypes.String,
4380+
"read": tftypes.String,
4381+
"update": tftypes.String,
4382+
"default": tftypes.String,
4383+
},
4384+
},
4385+
}}, map[string]tftypes.Value{
4386+
"location": tftypes.NewValue(tftypes.String, "westeurope"),
4387+
"id": tftypes.NewValue(tftypes.String, "baz"),
4388+
"timeouts": tftypes.NewValue(tftypes.Object{
4389+
AttributeTypes: map[string]tftypes.Type{
4390+
"create": tftypes.String,
4391+
"default": tftypes.String,
4392+
"delete": tftypes.String,
4393+
"read": tftypes.String,
4394+
"update": tftypes.String,
4395+
},
4396+
}, map[string]tftypes.Value{
4397+
"create": tftypes.NewValue(tftypes.String, nil),
4398+
"default": tftypes.NewValue(tftypes.String, nil),
4399+
"delete": tftypes.NewValue(tftypes.String, nil),
4400+
"read": tftypes.NewValue(tftypes.String, nil),
4401+
"update": tftypes.NewValue(tftypes.String, nil),
4402+
}),
4403+
}),
4404+
},
4405+
{
4406+
d: &ResourceData{
4407+
schema: map[string]*Schema{
4408+
"location": {
4409+
Type: TypeString,
4410+
Optional: true,
4411+
},
4412+
},
4413+
},
4414+
expected: tftypes.NewValue(tftypes.Object{
4415+
AttributeTypes: map[string]tftypes.Type{
4416+
"location": tftypes.String,
4417+
"id": tftypes.String,
4418+
}}, map[string]tftypes.Value{
4419+
"location": tftypes.NewValue(tftypes.String, "westeurope"),
4420+
"id": tftypes.NewValue(tftypes.String, "baz"),
4421+
}),
4422+
},
4423+
{
4424+
d: &ResourceData{
4425+
schema: map[string]*Schema{
4426+
"location": {
4427+
Type: TypeString,
4428+
Optional: true,
4429+
},
4430+
},
4431+
timeouts: &ResourceTimeout{},
4432+
},
4433+
expected: tftypes.NewValue(tftypes.Object{
4434+
AttributeTypes: map[string]tftypes.Type{
4435+
"location": tftypes.String,
4436+
"id": tftypes.String,
4437+
}}, map[string]tftypes.Value{
4438+
"location": tftypes.NewValue(tftypes.String, "westeurope"),
4439+
"id": tftypes.NewValue(tftypes.String, "baz"),
4440+
}),
4441+
},
4442+
{
4443+
d: &ResourceData{
4444+
schema: map[string]*Schema{
4445+
"location": {
4446+
Type: TypeString,
4447+
Optional: true,
4448+
},
4449+
},
4450+
timeouts: &ResourceTimeout{
4451+
Create: DefaultTimeout(30 * time.Minute),
4452+
},
4453+
},
4454+
expected: tftypes.NewValue(tftypes.Object{
4455+
AttributeTypes: map[string]tftypes.Type{
4456+
"location": tftypes.String,
4457+
"id": tftypes.String,
4458+
"timeouts": tftypes.Object{
4459+
AttributeTypes: map[string]tftypes.Type{
4460+
"create": tftypes.String,
4461+
},
4462+
},
4463+
}}, map[string]tftypes.Value{
4464+
"location": tftypes.NewValue(tftypes.String, "westeurope"),
4465+
"id": tftypes.NewValue(tftypes.String, "baz"),
4466+
"timeouts": tftypes.NewValue(tftypes.Object{
4467+
AttributeTypes: map[string]tftypes.Type{
4468+
"create": tftypes.String,
4469+
},
4470+
}, map[string]tftypes.Value{
4471+
"create": tftypes.NewValue(tftypes.String, nil),
4472+
}),
4473+
}),
4474+
},
4475+
}
4476+
4477+
for _, tc := range cases {
4478+
tc.d.SetId("baz") // just required to be able to call .State()
4479+
4480+
if err := tc.d.Set("location", "westeurope"); err != nil {
4481+
t.Fatalf("err: %s", err)
4482+
}
4483+
4484+
tfTypeIdentity, err := tc.d.TfTypeResourceState()
4485+
if err != nil {
4486+
t.Fatalf("err: %s", err)
4487+
}
4488+
4489+
if !tfTypeIdentity.Equal(tc.expected) {
4490+
t.Fatalf("expected tftype value of identity to be %+v, got %+v", tc.expected, tfTypeIdentity)
4491+
}
4492+
}
4493+
}
4494+
43174495
func testPtrTo(raw interface{}) interface{} {
43184496
return &raw
43194497
}

0 commit comments

Comments
 (0)