Copilot commented on code in PR #109:
URL: 
https://github.com/apache/cloudstack-kubernetes-provider/pull/109#discussion_r4256381094


##########
cloudstack_loadbalancer.go:
##########
@@ -361,9 +362,12 @@ func (cs *CSCloud) EnsureLoadBalancerDeleted(ctx 
context.Context, clusterName st
                        if err != nil {
                                klog.Errorf("Error parsing port: %v", err)
                        } else {
-                               networkId, err := 
cs.getNetworkIDFromIPAddress(lb.ipAddrID)
-                               if err != nil {
-                                       return err
+                               networkId := lbRule.Networkid
+                               if networkId == "" {
+                                       networkId, err = 
cs.getNetworkIDFromIPAddress(lb.ipAddrID)

Review Comment:
   When `lbRule.Networkid` is empty, the fallback uses `lb.ipAddrID` rather 
than the IP associated with the specific rule being deleted. If the service 
ever has rules on different IPs (e.g., duplicates/orphans), this can look up 
the wrong network and delete/list ACLs in the wrong tier. Prefer deriving the 
network from `lbRule.Publicipid` (or another rule-specific identifier) in this 
fallback path.



##########
cloudstack_loadbalancer.go:
##########
@@ -1972,57 +1992,176 @@ func (lb *loadBalancer) updateNetworkACL(publicPort 
int, protocol LoadBalancerPr
                return true, err
        }
 
-       networkAclParams := lb.NetworkACL.NewListNetworkACLsParams()
-       networkAclParams.SetAclid(network.Aclid)
-       networkAclParams.SetNetworkid(networkId)
-       networkAclParams.SetListall(true)
+       ipProtocol := protocol.IPProtocol()
+       aclRules, err := lb.listNetworkACLRules(networkId, ipProtocol, 
publicPort)
+       if err != nil {
+               return false, fmt.Errorf("error fetching Network ACL with ID: 
%v for network with id: %v, due to: %s", network.Aclid, networkId, err)
+       }

Review Comment:
   This newly introduced error wraps the underlying error using `%s`, which 
loses unwrap-ability and makes it harder for callers/tests to use 
`errors.Is/As`. Use `%w` for the underlying error (and `%v` for values like 
IDs) so the original error is preserved in the chain.



##########
test/e2e/framework.go:
##########
@@ -559,6 +559,60 @@ func (f *Framework) ACLRules(aclListID string) 
([]*cloudstack.NetworkACL, error)
        return resp.NetworkACLs, nil
 }
 
+// aclRuleMarker is the reason the CCM gives the Network ACL rules it creates 
for a load balancer.
+func aclRuleMarker(lbName string) string {
+       return "Managed by the CloudStack Kubernetes Provider for " + lbName

Review Comment:
   The reason prefix string is duplicated here and in production 
(`networkACLReasonPrefix`). Since the production comment explicitly notes this 
text must not change, duplicating it in e2e increases drift risk (tests could 
start failing for the wrong reason, or miss a real regression). Consider 
exposing the prefix (or a helper) from the production package for tests to 
consume, or centralizing it in a shared constant location.



##########
docs/development.md:
##########
@@ -420,7 +425,8 @@ so a run is reproducible; avoid floating tags like `latest`.
 | Nodes keep the uninitialized taint; CCM logs `provided node ip for node 
"..." is not valid` | The CloudStack VM's NIC IP does not match the IP kubelet 
registered with. Recreate the VM with `ipaddress=` set to the kind node's 
docker IP. |
 | Services stay `<pending>`; CCM logs `none of the hosts matched the list of 
VMs retrieved from CS API` | No CloudStack VM has a name matching a Kubernetes 
node name. |
 | CCM logs `found hosts that belong to different networks` | VMs matching the 
node names exist on more than one network — typically leftovers from a previous 
scenario. |
-| No ACL rules are created on a VPC network | The tier uses `default_allow` or 
`default_deny`. The CCM only manages rules on custom ACL lists. |
+| No ACL rules are created on a VPC network | The tier uses `default_allow` or 
`default_deny`; the CCM only manages rules on custom ACL lists. Or the list 
already has an ingress rule for the port that the CCM did not create, which it 
leaves the port to. |

Review Comment:
   The phrase “leaves the port to” is grammatically incomplete/unclear. 
Consider rewording to explicitly name what the port is left to (e.g., “left to 
that rule / left to the existing operator rule”) to avoid confusion when 
troubleshooting.



##########
cloudstack_loadbalancer.go:
##########
@@ -1972,57 +1992,176 @@ func (lb *loadBalancer) updateNetworkACL(publicPort 
int, protocol LoadBalancerPr
                return true, err
        }
 
-       networkAclParams := lb.NetworkACL.NewListNetworkACLsParams()
-       networkAclParams.SetAclid(network.Aclid)
-       networkAclParams.SetNetworkid(networkId)
-       networkAclParams.SetListall(true)
+       ipProtocol := protocol.IPProtocol()
+       aclRules, err := lb.listNetworkACLRules(networkId, ipProtocol, 
publicPort)
+       if err != nil {
+               return false, fmt.Errorf("error fetching Network ACL with ID: 
%v for network with id: %v, due to: %s", network.Aclid, networkId, err)
+       }
+
+       // Only a root admin may change the rules of a global list, the kind 
that belongs to no VPC.
+       adoptable := networkAclList.Vpcid != ""
+       kind, aclRule := lb.decidingNetworkACLRule(aclRules, adoptable)
+       switch kind {
+       case aclRuleOwn:
+               klog.V(4).Infof("Network ACL rule %v already opens %v port %v 
for load balancer %v", aclRule.Id, protocol, publicPort, lb.name)
+               return true, nil
+       case aclRuleOperator:
+               klog.Infof("Network ACL rule %v already covers %v port %v on 
network %v; not adding one for load balancer %v", aclRule.Id, protocol, 
publicPort, networkId, lb.name)
+               return true, nil
+       case aclRuleLegacy:
+               if err := lb.adoptNetworkACLRule(aclRule, network.Aclid, 
networkId, ipProtocol, publicPort); err != nil {
+                       return false, err
+               }
+               return true, nil
+       }
+
+       if err := lb.createNetworkACLRule(network.Aclid, networkId, ipProtocol, 
publicPort); err != nil {
+               return false, err
+       }
+       return true, nil
+}
+
+// networkACLReasonPrefix starts the reason of every Network ACL rule the 
controller creates.
+// The rest of the reason names the owning load balancer, and only rules 
carrying a load
+// balancer's own reason are ever deleted for it, so this text must not change.
+const networkACLReasonPrefix = "Managed by the CloudStack Kubernetes Provider 
for "
+
+// networkACLReason is the reason that marks a Network ACL rule as this load 
balancer's.
+func (lb *loadBalancer) networkACLReason() string {
+       return networkACLReasonPrefix + lb.name
+}
+
+// networkACLRuleKind is what an existing Network ACL rule on a needed port 
means to a load
+// balancer. The kinds are ordered by precedence.
+type networkACLRuleKind int
+
+const (
+       aclRuleIgnored  networkACLRuleKind = iota // being deleted, or another 
load balancer's
+       aclRuleOperator                           // someone else's; the port 
is left to them
+       aclRuleLegacy                             // made by a release before 
rules had owners
+       aclRuleOwn                                // this load balancer's
+)
+
+// classifyNetworkACLRule tells what an ingress rule on the needed port is to 
the load balancer
+// whose reason is given. CloudStack shows a rule being revoked as Deleting, 
and updating such a
+// rule would bring it back, so it never counts. A rule from an earlier 
release has exactly the
+// shape those releases created; any other unmarked rule, such as one with a 
restricted CIDR, a
+// deny rule or "0.0.0.0/0,::/0", is someone else's.
+func classifyNetworkACLRule(aclRule *cloudstack.NetworkACL, reason string) 
networkACLRuleKind {
+       switch {
+       case aclRule.State == "Deleting":
+               return aclRuleIgnored
+       case aclRule.Reason == reason:
+               return aclRuleOwn
+       case strings.HasPrefix(aclRule.Reason, networkACLReasonPrefix):
+               return aclRuleIgnored
+       case aclRule.Reason == "" && aclRule.Action == "Allow" &&
+               compareStringSlice(splitCIDRList(aclRule.Cidrlist), 
[]string{defaultAllowedCIDR}):
+               return aclRuleLegacy
+       }
+       return aclRuleOperator
+}
+
+// decidingNetworkACLRule picks the rule that decides what this load balancer 
does about a port:
+// its own rule, else one from an earlier release to adopt, else someone 
else's. A rule from an
+// earlier release counts as someone else's when it cannot be adopted.
+func (lb *loadBalancer) decidingNetworkACLRule(aclRules 
[]*cloudstack.NetworkACL, adoptable bool) (networkACLRuleKind, 
*cloudstack.NetworkACL) {
+       kind, deciding := aclRuleIgnored, (*cloudstack.NetworkACL)(nil)
+       for _, aclRule := range aclRules {
+               k := classifyNetworkACLRule(aclRule, lb.networkACLReason())
+               if k == aclRuleLegacy && !adoptable {
+                       k = aclRuleOperator
+               }
+               if k > kind {
+                       kind, deciding = k, aclRule
+               }
+       }
+       return kind, deciding
+}
+
+// listNetworkACLRules lists the ingress rules of a tier for exactly one port 
and IP protocol.
+// Listing by network covers the ACL list the tier uses now. CloudStack stores 
the protocol as it
+// was sent, and the protocol is compared exactly, as earlier releases did, so 
rules stored in upper
+// case, such as the "TCP" rules CloudStack's Kubernetes service adds for its 
own ports, are left
+// alone.
+func (lb *loadBalancer) listNetworkACLRules(networkID, ipProtocol string, 
publicPort int) ([]*cloudstack.NetworkACL, error) {
+       p := lb.NetworkACL.NewListNetworkACLsParams()
+       p.SetNetworkid(networkID)
+       p.SetListall(true)
        if lb.projectID != "" {
-               networkAclParams.SetProjectid(lb.projectID)
+               p.SetProjectid(lb.projectID)
        }
 
-       networkAcls, err := listAll(networkAclParams, func() (int, 
[]*cloudstack.NetworkACL, error) {
-               networkAclResponse, err := 
lb.NetworkACL.ListNetworkACLs(networkAclParams)
+       aclRules, err := listAll(p, func() (int, []*cloudstack.NetworkACL, 
error) {
+               r, err := lb.NetworkACL.ListNetworkACLs(p)
                if err != nil {
                        return 0, nil, err
                }
 
-               return networkAclResponse.Count, 
networkAclResponse.NetworkACLs, nil
+               return r.Count, r.NetworkACLs, nil
        })
        if err != nil {
-               return false, fmt.Errorf("error fetching Network ACL with ID: 
%v for network with id: %v, due to: %s", network.Aclid, networkId, err)
+               return nil, err
        }
 
-       // find all network ACL rules that have a matching proto+port
-       // a map may or may not be faster, but is a bit easier to understand
-       filtered := make(map[*cloudstack.NetworkACL]bool)
-       for _, netAclRule := range networkAcls {
-               if netAclRule.Protocol == protocol.IPProtocol() && 
netAclRule.Startport == strconv.Itoa(publicPort) && netAclRule.Endport == 
strconv.Itoa(publicPort) {
-                       filtered[netAclRule] = true
+       port := strconv.Itoa(publicPort)
+       var matching []*cloudstack.NetworkACL
+       for _, aclRule := range dedupeByID(aclRules, func(r 
*cloudstack.NetworkACL) string { return r.Id }) {
+               if strings.EqualFold(aclRule.Traffictype, "Ingress") && 
aclRule.Protocol == ipProtocol &&
+                       aclRule.Startport == port && aclRule.Endport == port {
+                       matching = append(matching, aclRule)
                }
        }
 
-       if len(filtered) > 0 {
-               klog.V(4).Infof("Network ACL rule for port %v and protocol %v 
already exists. No need to added a duplicate rule", publicPort, protocol)
-               return true, err
+       return matching, nil
+}
+
+// adoptNetworkACLRule marks a rule an earlier release created as this load 
balancer's. Only the
+// reason is sent: an update is partial by default, so CloudStack keeps the 
other fields. An account
+// that may not update ACL rules creates a rule of its own instead, and one 
that may not create
+// them either keeps relying on the old rule, as earlier releases did.
+func (lb *loadBalancer) adoptNetworkACLRule(aclRule *cloudstack.NetworkACL, 
aclID, networkID, ipProtocol string, publicPort int) error {
+       p := lb.NetworkACL.NewUpdateNetworkACLItemParams(aclRule.Id)
+       p.SetReason(lb.networkACLReason())
+
+       _, err := lb.NetworkACL.UpdateNetworkACLItem(p)
+       if err == nil {
+               klog.Infof("Adopted Network ACL rule %v for load balancer %v", 
aclRule.Id, lb.name)
+               return nil
+       }
+       if !isNotAllowed(err) {
+               return fmt.Errorf("error adopting Network ACL rule %v: %w", 
aclRule.Id, err)
+       }
+
+       klog.Warningf("Cannot adopt Network ACL rule %v for load balancer %v, 
creating a new one; delete the old rule by hand: %v", aclRule.Id, lb.name, err)
+       err = lb.createNetworkACLRule(aclID, networkID, ipProtocol, publicPort)
+       if err != nil && isNotAllowed(err) {
+               klog.Warningf("Cannot create a Network ACL rule for load 
balancer %v either; port %v stays open through rule %v: %v", lb.name, 
publicPort, aclRule.Id, err)
+               return nil
        }
 
-       // create ACL rule
-       // ACL rules only know tcp/udp/icmp, so tcp-proxy maps to tcp. This 
also matches the
-       // filter above, which would otherwise never find the rule again.
-       acl := lb.NetworkACL.NewCreateNetworkACLParams(protocol.IPProtocol())
-       acl.SetAclid(network.Aclid)
+       return err
+}
+
+// createNetworkACLRule opens a port on a tier with a rule marked as this load 
balancer's. ACL
+// rules only know tcp, udp and icmp, so the rule takes the IP protocol and a 
tcp-proxy port
+// opens tcp.
+func (lb *loadBalancer) createNetworkACLRule(aclID, networkID, ipProtocol 
string, publicPort int) error {
+       acl := lb.NetworkACL.NewCreateNetworkACLParams(ipProtocol)
+       acl.SetAclid(aclID)
        acl.SetAction("Allow")
-       acl.SetCidrlist([]string{"0.0.0.0/0"})
+       acl.SetCidrlist([]string{defaultAllowedCIDR})
        acl.SetStartport(publicPort)
        acl.SetEndport(publicPort)
-       acl.SetNetworkid(networkId)
+       acl.SetNetworkid(networkID)
        acl.SetTraffictype("Ingress")
+       acl.SetReason(lb.networkACLReason())
 
-       _, err = lb.NetworkACL.CreateNetworkACL(acl)
-       if err != nil {
-               return false, fmt.Errorf("error creating Network ACL for port: 
%v, due to: %s", publicPort, err)
+       if _, err := lb.NetworkACL.CreateNetworkACL(acl); err != nil {
+               return fmt.Errorf("error creating Network ACL for port: %v, due 
to: %s", publicPort, err)

Review Comment:
   Same issue as above: `%s` prevents proper error unwrapping. Prefer `%w` for 
`err` so higher-level code can preserve and inspect the original CloudStack 
error.



-- 
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: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to