英文:
Changed attributes in Terraform Plugin Framework
问题
我正在创建一个使用新的Terraform插件框架的自定义Terraform提供程序。
我想将以下对象保存到Terraform状态中:
"attributes": {
"name_servers": {
"ns1": {
"host": "host1",
"id": 100,
"ip": "",
"is_used": true
},
"ns2": {
"host": "host2",
"id": 101,
"ip": "",
"is_used": true,
}
},
"domain": "example.com"
},
并且它与以下Terraform资源一起工作:
...
type FooResourceModel struct {
Domain types.String `tfsdk:"domain"`
NameServers map[string]NameServersModel `tfsdk:"name_servers"`
}
type NameServersModel struct {
ID types.Int64 `tfsdk:"id"`
Host types.String `tfsdk:"host"`
IP types.String `tfsdk:"ip"`
IsUsed types.Bool `tfsdk:"is_used"`
}
func (r *FooNSResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"domain": schema.StringAttribute{
Required: true,
},
"name_servers": schema.MapNestedAttribute{
Required: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Computed: true,
},
"host": schema.StringAttribute{
Optional: true,
Computed: true,
},
"ip": schema.StringAttribute{
Optional: true,
Computed: true,
},
"is_used": schema.BoolAttribute{
Computed: true,
},
},
},
},
},
}
}
func (r *FooNSResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *FooNSResourceModel
diags := req.State.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
serviceName := data.ServiceName.ValueString()
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
nameServers, err := getNs(r, serviceName)
data.NameServers = nameServers
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
func getNs(r *FooNSResource, domain string) (map[string]NameServersModel, error) {
nameServers := make(map[string]NameServersModel)
var ids []uint64
err := r.client.Get(domain, &ids)
if err != nil {
return nil, err
}
for key, id := range ids {
nsResponse := NameServerResponse{}
err := r.client.Get(id, &nsResponse)
if err != nil {
return nil, err
}
nameServers["ns"+strconv.Itoa(key+1)] = NameServersModel{
ID: types.Int64Value(int64(nsResponse.Id)),
Host: types.StringValue(nsResponse.GetHost()),
IP: types.StringValue(nsResponse.GetIP()),
IsUsed: types.BoolValue(nsResponse.IsUsed),
}
}
return nameServers, nil
}
func (r *FooNSResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
serviceName := req.ID
nameServers, err := getNs(r, serviceName)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain"), serviceName)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name_servers"), nameServers)...)
}
...
所以当我从API导入数据时,它会将其保存到Terraform状态中。
我在我的Terraform文件中有以下内容:
...
resource "my_custom_provider" "my-server" {
service_name = "example.com"
name_servers = {
"ns1" = {
host = "host1"
},
"ns2" = {
host = "host2"
},
}
}
...
当我运行terraform plan时,它显示一切正常:
No changes. Your infrastructure matches the configuration.
但是当我添加ip
值时:
...
resource "my_custom_provider" "my-server" {
service_name = "example.com"
name_servers = {
"ns1" = {
host = "host1"
},
"ns2" = {
host = "host2"
ip = "127.0.0.1"
},
}
}
...
然后,Terraform希望更新所有内容:
Terraform will perform the following actions:
# my_custom_provider.my-server will be updated in-place
~ resource "my_custom_provider" "my-server" {
~ name_servers = {
~ "ns1" = {
~ id = 100 -> (known after apply)
~ ip = "" -> (known after apply)
~ is_used = true -> (known after apply)
# (1 unchanged attribute hidden)
},
~ "ns2" = {
~ id = 101 -> (known after apply)
~ ip = "" -> "127.0.0.1"
~ is_used = true -> (known after apply)
# (1 unchanged attribute hidden)
},
}
# (1 unchanged attribute hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
name_servers
中未更改的属性是我的主机。
问题是:
为什么Terraform认为将更改所有内容?如何防止它,只更新ns1或ns2的host/ip值?
英文:
I'm creating a custom terraform provider with the new Terraform Plugin Framework.
I want to save objects like this to terraform state:
"attributes": {
"name_servers": {
"ns1": {
"host": "host1",
"id": 100,
"ip": "",
"is_used": true
},
"ns2": {
"host": "host2",
"id": 101,
"ip": "",
"is_used": true,
}
},
"domain": "example.com"
},
And it's working with following terraform resource:
...
type FooResourceModel struct {
Domain types.String `tfsdk:"domain"`
NameServers map[string]NameServersModel `tfsdk:"name_servers"`
}
type NameServersModel struct {
ID types.Int64 `tfsdk:"id"`
Host types.String `tfsdk:"host"`
IP types.String `tfsdk:"ip"`
IsUsed types.Bool `tfsdk:"is_used"`
}
func (r *FooNSResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"domain": schema.StringAttribute{
Required: true,
},
"name_servers": schema.MapNestedAttribute{
Required: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Computed: true,
},
"host": schema.StringAttribute{
Optional: true,
Computed: true,
},
"ip": schema.StringAttribute{
Optional: true,
Computed: true,
},
"is_used": schema.BoolAttribute{
Computed: true,
},
},
},
},
},
}
}
func (r *FooNSResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *FooNSResourceModel
diags := req.State.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
serviceName := data.ServiceName.ValueString()
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
nameServers, err := getNs(r, serviceName)
data.NameServers = nameServers
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
func getNs(r *FooNSResource, domain string) (map[string]NameServersModel, error) {
nameServers := make(map[string]NameServersModel)
var ids []uint64
err := r.client.Get(domain, &ids)
if err != nil {
return nil, err
}
for key, id := range ids {
nsResponse := NameServerResponse{}
err := r.client.Get(id, &nsResponse)
if err != nil {
return nil, err
}
nameServers["ns"+strconv.Itoa(key+1)] = NameServersModel{
ID: types.Int64Value(int64(nsResponse.Id)),
Host: types.StringValue(nsResponse.GetHost()),
IP: types.StringValue(nsResponse.GetIP()),
IsUsed: types.BoolValue(nsResponse.IsUsed),
}
}
return nameServers, nil
}
func (r *FooNSResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
serviceName := req.ID
nameServers, err := getNs(r, serviceName)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain"), serviceName)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name_servers"), nameServers)...)
}
...
So when I'm importing data from API, it saves it into terraform state.
I have this in my Terraform file:
...
resource "my_custom_provider" "my-server" {
service_name = "example.com"
name_servers = {
"ns1" = {
host = "host1"
},
"ns2" = {
host = "host2"
},
}
}
...
And when I run terraform plan, it says everything is fine:
No changes. Your infrastructure matches the configuration.
But when I add ip
value:
...
resource "my_custom_provider" "my-server" {
service_name = "example.com"
name_servers = {
"ns1" = {
host = "host1"
},
"ns2" = {
host = "host2"
ip = "127.0.0.1"
},
}
}
...
then Terraform wants to update everything:
Terraform will perform the following actions:
# my_custom_provider.my-server will be updated in-place
~ resource "my_custom_provider" "my-server" {
~ name_servers = {
~ "ns1" = {
~ id = 100 -> (known after apply)
~ ip = "" -> (known after apply)
~ is_used = true -> (known after apply)
# (1 unchanged attribute hidden)
},
~ "ns2" = {
~ id = 101 -> (known after apply)
~ ip = "" -> "127.0.0.1"
~ is_used = true -> (known after apply)
# (1 unchanged attribute hidden)
},
}
# (1 unchanged attribute hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
The unchanged attribute in name_servers
is my host.
The question is:
Why Terraform thinks that everything will be changed? How can I prevent it, so only ns1 or ns2 values host/ip are going to be updated?
答案1
得分: 2
我找到了一个解决方案-PlanModifier: https://developer.hashicorp.com/terraform/plugin/framework/resources/plan-modification#common-use-case-attribute-plan-modifiers
使用PlanModifier
和UseStateForUnknown()
函数:
UseStateForUnknown(): 如果先前的状态值不为空,则复制该值。这对于减少计算属性的(在应用后已知)计划输出很有用,这些属性在时间上已知不会更改。
现在的模式看起来是这样的:
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"host": schema.StringAttribute{
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"ip": schema.StringAttribute{
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"is_used": schema.BoolAttribute{
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
},
},
对于每个属性,我都添加了计划修改器,现在terraform plan的输出看起来正常。
我不知道这是否是最佳解决方案,但它有效 在Terraform 1.4.2上进行了测试。
英文:
I've found a solution - PlanModifier: https://developer.hashicorp.com/terraform/plugin/framework/resources/plan-modification#common-use-case-attribute-plan-modifiers
Use PlanModifier
with UseStateForUnknown()
function:
>UseStateForUnknown(): Copies the prior state value, if not null. This is useful for reducing (known after apply) plan outputs for computed attributes which are known to not change over time.
The Schema looks like this right now:
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"host": schema.StringAttribute{
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"ip": schema.StringAttribute{
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"is_used": schema.BoolAttribute{
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
},
},
For every attribute I've added the plan modifier, and now output in terraform plan looks as it should.
I don't know if its a best solution, but it works Tested on Terraform 1.4.2.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论