Longsight commented on issue #115:
URL: 
https://github.com/apache/cloudstack-terraform-provider/issues/115#issuecomment-2757522110

   @baltazorbest I'm not convinced that swapping the logic of that conditional 
is the fix we're looking for.
   
   I was hitting a similar issue with Network ACL rules, and after some hours 
of debugging found that it was occurring _there_ because I wasn't supplying a 
project ID to `cloudstack_network_acl_rule`, which meant that 
`resourceCloudStackNetworkACLRuleRead` was returning early:
   
   ```go
        _, count, err := cs.NetworkACL.GetNetworkACLListByID(
                d.Id(),
                cloudstack.WithProject(d.Get("project").(string)),
        )
        if err != nil {
                if count == 0 {
                        log.Printf(
                                "[DEBUG] Network ACL list %s does no longer 
exist", d.Id())
                        d.SetId("")
                        return nil
                }
   
                return err
        }
   ```
   ```go
   func (s *NetworkACLService) GetNetworkACLListByID(id string, opts 
...OptionFunc) (*NetworkACLList, int, error) {
        p := &ListNetworkACLListsParams{}
        p.p = make(map[string]interface{})
   
        p.p["id"] = id
   
        for _, fn := range append(s.cs.options, opts...) {
                if err := fn(s.cs, p); err != nil {
                        return nil, -1, err
                }
        }
   
        l, err := s.ListNetworkACLLists(p)
        if err != nil {
                if strings.Contains(err.Error(), fmt.Sprintf(
                        "Invalid parameter id value=%s due to incorrect long 
value format, "+
                                "or entity does not exist", id)) {
                        return nil, 0, fmt.Errorf("No match found for %s: %+v", 
id, l)
                }
                return nil, -1, err
        }
   
           // -
           // - This is where a zero-count set results in an error, which is 
cascaded to the calling function
           // -
   
        if l.Count == 0 {
                return nil, l.Count, fmt.Errorf("No match found for %s: %+v", 
id, l)
        }
   
        if l.Count == 1 {
                return l.NetworkACLLists[0], l.Count, nil
        }
        return nil, l.Count, fmt.Errorf("There is more then one result for 
NetworkACLList UUID: %s!", id)
   }
   ```
   
   `GetNetworkACLListByID` _always_ returned an empty set and an error _if_ the 
parent `NetworkACLList` belonged to a project, and the `project` parameter was 
omitted from the resource. This is by design - project-scoped resources are not 
returned by `List*` API calls _unless_ a `project` (called `projectid` in the 
API) parameter is supplied, even if `listall` is `true`.
   
   When I included the `project` parameter in the Terraform resource, the rules 
were matched and created correctly.
   
   ---
   
   In the case of `cloudstack_firewall`, the relevant read logic is:
   
   ```go
        // Get all the rules from the running environment
        p := cs.Firewall.NewListFirewallRulesParams()
        p.SetIpaddressid(d.Id())
        p.SetListall(true)
   
        l, err := cs.Firewall.ListFirewallRules(p)
        if err != nil {
                return err
        }
   ```
   ```go
   func (s *FirewallService) ListFirewallRules(p *ListFirewallRulesParams) 
(*ListFirewallRulesResponse, error) {
        resp, err := s.cs.newRequest("listFirewallRules", p.toURLValues())
        if err != nil {
                return nil, err
        }
   
        resp, err = convertFirewallServiceResponse(resp)
        if err != nil {
                return nil, err
        }
   
        var r ListFirewallRulesResponse
        if err := json.Unmarshal(resp, &r); err != nil {
                return nil, err
        }
   
        return &r, nil
   }
   ```
   
   In this case, if the firewall is scoped to a project, this code will _never_ 
find any rules, as the `project` is never included in the API call. However, 
unlike `GetNetworkACLListByID `, `ListFirewallRules` does _not_ throw an error 
in the case of an empty set, so `resourceCloudStackFirewallRead` continues:
   
   ```go
           // -
           // ruleMap is supposed to be a map of existing rules fetched from 
the API
           // BUT:
           // if the firewall belongs to a project, it will always be empty
           // This is incorrect behaviour
           // -
   
        ruleMap := make(map[string]*cloudstack.FirewallRule, l.Count)
        for _, r := range l.FirewallRules {
                ruleMap[r.Id] = r
        }
   
        // Create an empty schema.Set to hold all rules
        rules := 
resourceCloudStackFirewall().Schema["rule"].ZeroValue().(*schema.Set)
   
        // Read all rules that are configured
        if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 {
                for _, rule := range rs.List() {
   
                           // ...rule matching logic
   ```
   
   Under normal circumstances, `ruleMap` is a map of the rules fetched from the 
API, `rs` is a set of the rules passed by the Terraform resource, and `rules` 
is the resulting intersection of the two, representing rules that exist in 
Cloudstack _and_ are known to Terraform. At this point in the code, when 
reading an existing rule, there are two valid scenarios:
   
   - `managed` is `false`, and `rules` has 1 or more elements
   - `managed` is `true`, and `rules` has 0 or more elements
   
   This logic exists because a _managed_ `cloudstack_firewall` with zero rules 
is a valid means of ensuring that no rules are added to the firewall by other 
means until you choose to add them with Terraform.
   
   If the firewall is scoped to a project, and `rulesMap` is empty, then the 
rule matching logic will ensure that `rules` is also empty. This _should not 
happen_, and the block you've identified is the code that handles this:
   
   ```go
           // If we've matched any of the existing Cloudstack firewall rules to 
the rules in the resource,
           // include them in the resulting ResourceData:
        if rules.Len() > 0 {
                d.Set("rule", rules)
           // If we haven't, AND this resource is NOT marked as `managed`, 
unset the resource ID
           // of the newly-created `cloudstack_firewall` resource, as this 
should only happen IF
           // the resource does not actually exist during Refresh, and should 
be removed from Terraform state.
        } else if !managed {
                d.SetId("")
        }
   ```
   
   The logic here is correct - during a normal resource read, if `rules.Len() 
== 0` and `!managed` then this suggests that a firewall resource that exists in 
Terraform state has been destroyed in infrastructure, and should be marked as 
destroyed in state.
   
   However, since `resourceCloudStackFirewallRead` is _also_ called from 
`resourceCloudStackFirewallCreate` after initial resource creation (so that 
unknown values like UUID can be fetched from Cloudstack), this logic causes the 
`apply` operation to fail if the firewall rule belongs to a project, as 
`Create` operations should never return a resource with an empty `Id`:
   
   ```go
        // ...
        // The SetId method must be called with a non-empty value for the 
managed
        // resource instance to be properly saved into the Terraform state and
        // avoid a "inconsistent result after apply" error.
        // ...
        Create CreateFunc
   ```
   
   This is why your fix appears to work: if you change `else if !managed` to 
`else if managed`, then the resource ID is no longer unset on apply even though 
the Read operation found an empty ruleset, so the actual error is masked by 
what looks like a successful Create that just happened to create zero rules. 
What you will probably find, however, is that duplicate rules are created on 
every apply, as existing rules will never be seen by the provider, so it will 
think that they're always missing. The more correct fix is to include the 
`projectid` parameter in the `ListFirewallRules ` API call in the first place, 
so that the newly-created firewall resource is returned by Cloudstack and 
recognised by the provider.
   
   ---
   
   Since `cloudstack_firewall` is mapped to a given `ip_address_id` rather than 
the `id` of the actual firewall, we cannot duplicate the `project` parameter 
logic used in other resources, as we can't call 
`cs.Firewall.GetFirewallRuleByID` without the firewall ID. What we can do, 
however, is ensure that the `project` is included in the `ListFirewallRules` 
call, by making use of `cloudstack.WithProject` directly:
   
   ```diff
   --- a/cloudstack/resource_cloudstack_firewall.go
   +++ b/cloudstack/resource_cloudstack_firewall.go
   @@ -95,6 +95,12 @@ func resourceCloudStackFirewall() *schema.Resource {
                                   },
                           },
    
   +                       "project": {
   +                               Type:     schema.TypeString,
   +                               Optional: true,
   +                               ForceNew: true,
   +                       },
   +
                           "parallelism": {
                                   Type:     schema.TypeInt,
                                   Optional: true,
   @@ -256,6 +262,12 @@ func resourceCloudStackFirewallRead(d 
*schema.ResourceData, meta interface{}) er
           p.SetIpaddressid(d.Id())
           p.SetListall(true)
    
   +       // Use WithProject(d.Get("project").(string)) to set the `projectid` 
parameter on the API call
   +       // if we've passed a `project` parameter to the resource
   +       if err := cloudstack.WithProject(d.Get("project").(string))(cs, p); 
err != nil {
   +               return err
   +       }
   +
           l, err := cs.Firewall.ListFirewallRules(p)
           if err != nil {
                   return err
   ```
   
   I'm not in a position to test this fix myself right now as I'm not currently 
using firewall rules in my deployment, but I'm fairly confident it should work.
   
   Side note: we have to supply the `project` resource parameter ourselves 
rather than look it up from the `PublicIpAddress` referenced in  
`ip_address_id` because, even though `cs.Address.GetPublicIpAddressByID` 
exists, we hit the same problem of scoping - unless we already know the 
`projectid` of the resource in Cloudstack, the underlying `List*` call will 
fail to return the resource we're looking for.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscr...@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org

Reply via email to