英文:
Convert terraform resourceData of type map[string]interface{} to struct
问题
我正在创建一个自定义的 Terraform 提供程序,遇到了这个问题。
我试图将一个 schema.TypeList
字段转换为结构体,TypeList 大致如下所示:
"template": {
Type: schema.TypeList,
Required: true,
ForceNew: false,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"lists_test": {
Type: schema.TypeSet,
Required: true,
ForceNew: false,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"name_test": {
Type: schema.TypeString,
Required: true,
ForceNew: false,
},
},
},
},
而我试图对齐的结构体大致如下所示:
type TestStruct struct {
NameTest string `json:"name_test"`
ListsTests []string `json:"lists_test"`
}
我尝试了几种解决方案,例如尝试将其解组为 JSON。类似下面的代码:
template := d.Get("template").([]interface{})[0].(map[string]interface{})
templateStr, err := json.Marshal(template)
templateConverted := &TestStruct{}
json.Unmarshal(template, templateConverted)
然而,我得到了一个错误 json: unsupported type: SchemaSetFunc
,这可能是因为它试图编组一个 schema.Schema
类型而不是 map[string]interface{}
类型,这让我感到困惑。我还尝试使用 gohcl.DecodeBody
,但我放弃了这个想法,因为它的使用似乎更倾向于直接读取 tf 文件,而不是 *schema.ResourceData
类型。
有人遇到过处理这种情况的经验吗?任何帮助或建议都将不胜感激。谢谢!
英文:
I'm creating a custom terraform provider and I came across this issue.
I was trying to convert a schema.TypeList
field into a struct, the TypeList looks something like this:
"template": {
Type: schema.TypeList,
Required: true,
ForceNew: false,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"lists_test": {
Type: schema.TypeSet,
Required: true,
ForceNew: false,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"name_test": {
Type: schema.TypeString,
Required: true,
ForceNew: false,
},
},},
and the struct that I'm trying to align to looks something like this:
type TestStruct struct {
NameTest string `json:"name_test"`
ListsTests []string `json:"lists_test"`
}
I tried a couple of solutions, for instance I tried unmarshalling it to json. Something like below:
template := d.Get("template").([]interface{})[0].(map[string]interface{})
templateStr, err := json.Marshal(template)
templateConverted := &TestStruct{}
json.Unmarshal(template, templateConverted)
however, I'm getting an error json: unsupported type: SchemaSetFunc
, which is probably because it's trying to marshal a schema.Schema
type instead of map[string]interface{} type, which confuses me. I also tried to use gohcl.DecodeBody
but I abandoned the idea since it's usage seems more inclined into reading direct tf files rather than *schema.ResourceData
types.
Does anyone had the same experience dealing with this type of scenario? Any help or suggestion is appreciated. Thank you!
答案1
得分: 3
Terraform的旧版SDK(SDKv2)不是围绕解码为标记结构的范式设计的,而是期望您使用d.Get
并手动类型断言各个值,对于您的情况,可能如下所示:
raw := d.Get("template").([]interface{})[0].(map[string]interface{})
t := &TestStruct{
NameTest: raw["name_test"].(string),
ListsTests: make([]string, len(raw["lists_test"].([]interface{}))),
}
for i, itemRaw := range raw["lists_test"].([]interface{}) {
t.ListsTests[i] = itemRaw.(string)
}
大多数Terraform提供程序的惯用风格是为每个复杂类型的属性编写单独的函数,每个函数在目标平台的SDK中返回适当类型的对象。通常还会有一个相应的函数,用于在相反的方向上进行转换:给定目标平台的SDK中的对象,返回一个可以使用d.Set
分配给该属性的map[string]interface{}
。
然而,仅仅因为SDK中没有内置的处理方式,这并不意味着您不能使用其他更通用的库,这些库可用于任何Go程序。
一个示例库是github.com/mitchellh/mapstructure
,它正是为您考虑的目标而设计的:将某个接口类型的值使用反射尝试适配到标记结构类型上。
如果您想使用该库,您需要使用mapstructure:
来注释您的结构,而不是使用json:
,然后将您的raw
值传递给the mapstructure.Decode
函数:
raw := d.Get("template").([]interface{})[0].(map[string]interface{})
var t TestStruct
err := mapstructure.Decode(raw, &t)
由于SDKv2中的schema.ResourceData
抽象保证根据您定义的模式返回特定的数据类型,只要您的模式和目标类型匹配,您通常不会从mapstructure.Decode
中获得错误,但仍然建议检查错误,因为否则您的t
值可能不完全填充,导致下游出现混乱的错误行为。
这不是官方提供程序中使用的典型实现风格,但如果您发现这种风格更方便或更易于维护,以这种方式编写提供程序并没有真正的害处。
或者,如果您尚未深入使用SDKv2,您可能希望考虑改用Terraform插件框架。除了设计围绕现代Terraform的类型系统(而SDKv2是为Terraform v0.11及更早版本设计的)之外,它还支持更接近您目标的编程风格,具有诸如tfsdk.Plan.Get
和tfsdk.Plan.GetAttribute
之类的方法,可以直接解码为适当形状和适当标记的“正常”Go值。
我无法轻松地展示一个示例,因为这将假设以完全不同的方式编写的提供程序,但希望您可以从这两个函数的签名中看出它们的用法。在访问状态、配置和计划中有更多的评论和示例。
英文:
Terraform's older SDK (SDKv2) is not designed around the paradigm of decoding into a tagged structure, and instead expects you to use d.Get
and manually type-assert individual values, which in your case would perhaps look something like this:
raw := d.Get("template").([]interface{})[0].(map[string]interface{})
t := &TestStruct{
NameTest: raw["name_test"].(string),
ListsTests: make([]string, len(raw["lists_test"].([]interface{})),
}
for i, itemRaw := range raw["lists_test"].([]interface{}) {
t.ListsTests[i] = itemRaw.(string)
}
The idiomatic style for most Terraform providers is to write logic like this in separate functions for each complex-typed attribute, where each returns an object of the appropriate type in the target platform's SDK. There would typically also be a matching function for going in the opposite direction: given an object from the target platform's SDK, return a map[string]interface{}
that can be assigned to this attribute using d.Set
.
However, just because there isn't something built in to the SDK to handle this, that doesn't mean you can't use other libraries that are more general utilities for use in any Go programs.
One example library is github.com/mitchellh/mapstructure
, which is designed for exactly the goal you have in mind: to take a value of some interface type and try to use reflection to fit it onto a tagged structure type.
If you want to use that library then you would need to annotate your structure with mapstructure:
, instead of the json:
ones, and then pass your raw
value to the mapstructure.Decode
function:
raw := d.Get("template").([]interface{})[0].(map[string]interface{})
var t TestStruct
err := mapstructure.Decode(raw, &t)
Since the schema.ResourceData
abstraction in SDKv2 guarantees to return specific data types based on the schema you defined, you should not typically get errors from mapstructure.Decode
as long as your schema and your target type match, but still a good idea to check for errors anyway because otherwise your t
value may not be completely populated, causing confusing broken behavior downstream.
This is not a typical implementation style used in the official providers, but there's no real harm in writing your provider in this way if you find this style more convenient, or easier to maintain.
Alternatively, if you are not already deeply invested in SDKv2 then you may wish to consider using Terraform Plugin Framework instead. As well as being designed around the type system of modern Terraform (whereas SDKv2 was designed for Terraform v0.11 and earlier), it also supports a programming style more like what you are aiming for, with methods like tfsdk.Plan.Get
and tfsdk.Plan.GetAttribute
that can decode directly into an appropriately-shaped and appropriately tagged "normal" Go value.
I can't easily show an example of that because it would presume a provider written in quite a different way, but hopefully you can see from the signature of those two functions how they might be used. There's some more commentary and examples in Accessing State, Config, and Plan.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论