hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io
Other
42.77k stars 9.56k forks source link

Question about computed nested values #10532

Closed jasmingacic closed 7 years ago

jasmingacic commented 7 years ago
terraform version
Terraform v0.8.0-dev (ac2f78f463b54b7e8d9f2c433c951e11b0cacd69)

I'm building a third party plugin for Terraform

And I'm running into a following problem:

I've got this resource

Schema: map[string]*schema.Schema{
            "name": {
                Type:     schema.TypeString,
                Required: true,
            },
            "description": {
                Type:     schema.TypeString,
                Optional: true,
            },
            "rules": {
                Type: schema.TypeList,
                Elem: &schema.Resource{
                    Schema: map[string]*schema.Schema{
                        "protocol" :{
                            Type : schema.TypeString,
                            Required: true,
                        },
                        "port_from": {
                            Type:     schema.TypeInt,
                            Optional: true,
                        },
                        "port_to": {
                            Type:     schema.TypeInt,
                            Optional: true,
                        },
                        "source_ip": {
                            Type:     schema.TypeString,
                            Optional: true,
                        },
                        "rule_id" : {
                            Type:schema.TypeString,
                            Computed: true,
                        },
                    },
                },
                Required: true,
            },
            "ips": {
                Type: schema.TypeList,
                Elem: &schema.Resource{
                    Schema: map[string]*schema.Schema{
                        "id": {
                            Type:     schema.TypeString,
                            Required: true,
                        },
                        "ip": {
                            Type:     schema.TypeString,
                            Computed: true,
                        },
                    },
                },
                Required: true,
            },
        },

In case of the resource rules nothing gets persisted into tfstate except for the values I've provided.

I have this in my code when i'm setting rules

func resourceOneandOneFirewallRead(d *schema.ResourceData, meta interface{}) error {
    config := meta.(*Config)

    fw, err := config.API.GetFirewallPolicy(d.Id())
    if err != nil {
        return err
    }

    d.Set("rules", readRules(fw.Rules))
    d.Set("description", fw.Description)
    d.Set("ips", readServerIps(fw.ServerIps))

    return nil
}
func readRules(rules []oneandone.FirewallPolicyRule) []map[string]interface{} {
    raw := make([]map[string]interface{}, 0, len(rules))
    for _, r := range rules {
        toAdd := map[string]interface{}{
            "protocol": r.Protocol,
            "port_from": r.PortFrom,
            "port_to": r.PortTo,
            "source_ip" : r.SourceIp,
            "rule_id" : r.Id,
        }
        raw = append(raw, toAdd)
    }
    log.Println("[DEBUG] returned", raw)

    return raw
}

Output from looks like this:

2016/12/05 15:24:47 [DEBUG] plugin: terraform-provider-oneandone: 
2016/12/05 15:24:47 [DEBUG] returned [map[] map[port_from:0xc4204650b0 port_to:0xc4204650e0 source_ip:0.0.0.0 rule_id:7ED6B306C96F7607089B0F1B073BA88E protocol:TCP]]

As you can see ips are stored correctly but rules are not.

"id": "45DAC6823E9471FF56FFFF02CB1E406E",
"ips.#": "1",
"ips.0.id": "1CC247D2755CF77C3B5910AB67FA5E3E",
"ips.0.ip": "77.68.10.110",
"name": "test_fw",
"rules.#": "2",
"rules.0.port_from": "80",
"rules.0.port_to": "80",
"rules.0.protocol": "TCP",
"rules.0.rule_id": "",
"rules.0.source_ip": "",
"rules.1.port_from": "22",
"rules.1.port_to": "22",
"rules.1.protocol": "TCP",
"rules.1.rule_id": "",
"rules.1.source_ip": ""

What am I doing wrong?

jasmingacic commented 7 years ago

I was able to find what the issue is. Since my TypeSet rules has one computed field and the other fields are required and optional. If I set only computed values in map[string]interface{} that gets recorded in .tfstate. So I've come up with following solution:

func readRules(d *schema.ResourceData, rules []oneandone.FirewallPolicyRule) interface{} {
    rawRules := d.Get("rules").([]interface{})
    counter := 0
    for _, rR := range rawRules {
        rawMap := rR.(map[string]interface{})
        rawMap["rule_id"] = rules[counter].Id
        rawMap["source_ip"] = rules[counter].SourceIp
        counter++
    }

    return rawRules
}

Is there a better solution for this?

apparentlymart commented 7 years ago

Hi @jasminSPC! Sorry for letting this question sit here unanswered for so long.

It looks like you found a reasonable solution, so I'm sure my answer here is pretty academic at this point but I'm going to leave one here for posterity in case anyone else finds this issue in future.

Things get quite complicated for nested structures, as you've seen. We will hopefully address some of these oddities in future versions of Terraform, but for now we usually suggest keeping Computed and non-Computed attributes separate, rather than mixing the two in nested structures, since Terraform's current support for reading values from nested sets and lists is rather fraught due to limitations of the interpolation language.

So practically speaking that could mean having a separate top-level attribute rule_ids that is a Computed list of strings, whose list element indices correspond with those in the rules list. This way the list of ids is easier to use with the primitives in the interpolation language: the user of your resource can either refer to a specific element in the list using an expression like oneandone_firewall_policy.example.rule_ids[0] or to the whole list of ids as oneandone_firewall_policy.example.rule_ids. The latter would not be possible with the ids nested inside the rule structure.

It's likely that we'll do some work to improve the collection-wrangling capabilities of the interpolation language in future. For now I'm going to close this.

ghost commented 4 years ago

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.