hashicorp / terraform-plugin-sdk

Terraform Plugin SDK enables building plugins (providers) to manage any service providers or custom in-house solutions
https://developer.hashicorp.com/terraform/plugin
Mozilla Public License 2.0
425 stars 230 forks source link

TypeSet is not modified during Update #792

Open FlorianJDF opened 2 years ago

FlorianJDF commented 2 years ago

SDK Version

2.7.0

Relevant provider source code

Resource:

func resourceTestComponent() *schema.Resource {
    return &schema.Resource{
        Schema: map[string]*schema.Schema{
            "name": {
                Type:     schema.TypeString,
                Required: true,
            },
            "the_test": {
                Type:     schema.TypeSet,
                Computed: true,
                Elem: &schema.Resource{
                    Schema: map[string]*schema.Schema{
                        "id": {
                            Type:     schema.TypeString,
                            Computed: true,
                        },
                        "to_change": {
                            Type:     schema.TypeString,
                            Computed: true,
                        },
                    },
                },
            },
        },
        CreateContext: tCreate,
        CustomizeDiff: tDiff,
        ReadContext:   tRead,
        UpdateContext: tUpdate,
        DeleteContext: tDelete,
    }
}

func tRead(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
    log.Println("================> In Read <================")
    var diags diag.Diagnostics
    err := rd.Set("the_test", generateMap([]map[string]string{
        {
            "id":        "1",
            "to_change": "noRead",
        },
        {
            "id":        "2",
            "to_change": "NoRead",
        },
    }))
    if err != nil {
        return diag.FromErr(err)
    }

    log.Println("================> End of Read <================")
    return diags
}

func generateMap(mapper []map[string]string) []interface{} {
    var result []interface{}
    for _, elt := range mapper {
        result = append(result, elt)
    }

    return result
}

func tCreate(c context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
    log.Println("================> In create <================")
    var diags diag.Diagnostics
    data.SetId("un")

    err := data.Set("the_test", generateMap([]map[string]string{
        {
            "id":        "1",
            "to_change": "no",
        },
        {
            "id":        "2",
            "to_change": "no",
        },
    }))
    if err != nil {
        return diag.FromErr(err)
    }

    log.Println("================> End of create <================")
    log.Println(err)
    return diags
}

func tDiff(c context.Context, rd *schema.ResourceDiff, i interface{}) error {
    log.Println("================> In Custom diff <================")

    resourceName, _ := rd.GetChange("name")
    if resourceName == "" {
        return nil
    }

    err := rd.SetNew("the_test", generateMap([]map[string]string{
        {
            "id":        "1",
            "to_change": "yes",
        },
        {
            "id":        "2",
            "to_change": "yes",
        },
    }))
    if err != nil {
        return err
    }

    log.Println("================> End of custom diff <================")
    return nil
}

func tUpdate(c context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
    log.Println("================> In Update <================")
    var diags diag.Diagnostics
    setter := data.Get("the_test").(*schema.Set)

    log.Println("-> Before sata set")
    log.Println(setter.List())

    for _, elt := range setter.List() {
        m := elt.(map[string]interface{})
        m["to_change"] = "no2"
    }

    log.Println("-> After manual loop")
    log.Println(setter.List())

    err := data.Set("the_test", setter)
    if err != nil {
        return diag.FromErr(err)
    }

    log.Println("-> After sata set")
    setter2 := data.Get("the_test").(*schema.Set)
    log.Println(setter2.List())

    log.Println("================> End of Update <================")
    return diags
}

State First plan/apply

resource "test_comp" "base-config" {
    id       = "un"
    name     = "new"
    the_test = [
        {
            id        = "1"
            to_change = "no"
        },
        {
            id        = "2"
            to_change = "no"
        },
    ]
}

State after the second plan/apply

resource "test_comp" "base-config" {
    id       = "un"
    name     = "new"
    the_test = [
        {
            id        = "1"
            to_change = "yes"
        },
        {
            id        = "2"
            to_change = "yes"
        },
    ]
}

The value of the customDiff is persisted, not the Update.

Issue

I am developping a TF provider. I have a resource with a Computed TypeSet.

When calling the Create function at first apply, the TypeSet is created with the rights values. Then the second plan shows the diff from the values in the CustomizeDiff.

But once applied, it is the values of the CustomeDiff that are stored in the state and not the value of the the Update function.

I pasted a representation of the code. In the logs I can see that in the update function the change happened, but is not persisted at the end. Here are the logs of the provider

2021/08/12 09:46:46 ================> In Update <================: timestamp=2021-08-12T09:46:46.659Z
2021/08/12 09:46:46 -> Before sata set: timestamp=2021-08-12T09:46:46.659Z
2021/08/12 09:46:46 [map[id:1 to_change:yes] map[id:2 to_change:yes]]: timestamp=2021-08-12T09:46:46.659Z
2021/08/12 09:46:46 -> After manual loop: timestamp=2021-08-12T09:46:46.660Z
2021/08/12 09:46:46 [map[id:1 to_change:no2] map[id:2 to_change:no2]]: timestamp=2021-08-12T09:46:46.660Z
2021/08/12 09:46:46 -> After sata set: timestamp=2021-08-12T09:46:46.660Z
 2021/08/12 09:46:46 [map[id:2 to_change:no2] map[id:1 to_change:no2]]: timestamp=2021-08-12T09:46:46.660Z
2021/08/12 09:46:46 ================> End of Update <================: timestamp=2021-08-12T09:46:46.660Z

Expected Behavior

The value stored in the state file shoud be the one set in the function Update.

Am I missing something about how TypeSet works ?

The same behavior is working perfectly with TypeList.

eduardovra commented 2 years ago

I am experiencing the same problem

vbauzys commented 2 years ago

Observed the same behavior.

benben commented 1 year ago

took me now a full day to end up here, so an annoying 👍 as a comment instead of just a reaction :)

lgarber-akamai commented 6 months ago

Looks like we're running into a similar issue when indirectly updating the linode_instance_config.device field in the Linode Terraform Provider: https://github.com/linode/terraform-provider-linode/blob/dev/linode/instanceconfig/resource.go#L115

zliang-akamai commented 6 months ago

Also took a full day to debug this, lol May we have a fix in a future version of SDKv2? @bflad @bendbennett