Skip to content

Commit 2777fba

Browse files
committed
Fix: egress rules & tags check existing
* Currently this provider for isolated_network and tags do not properly check for existing egress rules or tags. This causes errors to happen in Cloudstack Events constantly saying the egress rule or tag already exists
1 parent 312a3c9 commit 2777fba

File tree

6 files changed

+158
-34
lines changed

6 files changed

+158
-34
lines changed

api/v1beta1/zz_generated.conversion.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta2/zz_generated.conversion.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta3/cloudstackisolatednetwork_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ type CloudStackIsolatedNetworkStatus struct {
7373
// Empty value means the network mode is NATTED, not ROUTED.
7474
RoutingMode string `json:"routingMode,omitempty"`
7575

76+
// Firewall rules open status.
77+
FirewallRulesOpened bool `json:"firewallRulesOpened,omitempty"`
78+
7679
// Ready indicates the readiness of this provider resource.
7780
Ready bool `json:"ready"`
7881
}

config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ spec:
251251
description: CloudStackIsolatedNetworkStatus defines the observed state
252252
of CloudStackIsolatedNetwork
253253
properties:
254+
firewallRulesOpened:
255+
description: Firewall rules open status.
256+
type: boolean
254257
loadBalancerRuleID:
255258
description: The ID of the lb rule used to assign VMs to the lb.
256259
type: string

pkg/cloud/isolated_network.go

Lines changed: 126 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -154,54 +154,150 @@ func (c *client) CreateIsolatedNetwork(fd *infrav1.CloudStackFailureDomain, isoN
154154

155155
// OpenFirewallRules opens a CloudStack egress firewall for an isolated network.
156156
func (c *client) OpenFirewallRules(isoNet *infrav1.CloudStackIsolatedNetwork) (retErr error) {
157+
// Early return if VPC is present
157158
if isoNet.Spec.VPC != nil && isoNet.Spec.VPC.ID != "" {
158159
return nil
159160
}
160161

161-
// If network's egress policy is true, then we don't need to open the firewall rules for all protocols
162-
network, count, err := c.cs.Network.GetNetworkByID(isoNet.Spec.ID, cloudstack.WithProject(c.user.Project.ID))
162+
// Get network details
163+
network, err := c.getNetwork(isoNet.Spec.ID)
163164
if err != nil {
164-
return errors.Wrapf(err, "failed to get network by ID %s", isoNet.Spec.ID)
165-
}
166-
if count == 0 {
167-
return errors.Errorf("no network found with ID %s", isoNet.Spec.ID)
165+
return err
168166
}
167+
168+
// Early return if egress default policy is true
169169
if network.Egressdefaultpolicy {
170+
isoNet.Status.FirewallRulesOpened = true
170171
return nil
171172
}
172173

174+
// Check if all required firewall rules exist
175+
allRulesPresent, err := c.checkFirewallRules(isoNet)
176+
if err != nil {
177+
return err
178+
}
179+
if allRulesPresent {
180+
isoNet.Status.FirewallRulesOpened = true
181+
return nil
182+
}
183+
184+
// Reset status if rules are missing
185+
isoNet.Status.FirewallRulesOpened = false
186+
187+
// Create firewall rules for each protocol
173188
protocols := []string{NetworkProtocolTCP, NetworkProtocolUDP, NetworkProtocolICMP}
174189
for _, proto := range protocols {
175-
var err error
176-
if isoNet.Status.RoutingMode != "" {
177-
p := c.cs.Firewall.NewCreateRoutingFirewallRuleParams(isoNet.Spec.ID, proto)
178-
if proto == "icmp" {
179-
p.SetIcmptype(-1)
180-
p.SetIcmpcode(-1)
181-
}
182-
_, err = c.cs.Firewall.CreateRoutingFirewallRule(p)
183-
} else {
184-
p := c.cs.Firewall.NewCreateEgressFirewallRuleParams(isoNet.Spec.ID, proto)
190+
if err := c.createFirewallRule(isoNet, proto); err != nil {
191+
retErr = err
192+
}
193+
}
194+
195+
// Mark firewall rules as opened if no errors occurred
196+
if retErr == nil {
197+
isoNet.Status.FirewallRulesOpened = true
198+
}
199+
200+
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(retErr)
201+
return retErr
202+
}
203+
204+
// Helper function to check if all required firewall rules exist
205+
func (c *client) checkFirewallRules(isoNet *infrav1.CloudStackIsolatedNetwork) (bool, error) {
206+
protocols := []string{NetworkProtocolTCP, NetworkProtocolUDP, NetworkProtocolICMP}
207+
p := c.cs.Firewall.NewListEgressFirewallRulesParams()
208+
p.SetNetworkid(isoNet.Spec.ID)
209+
setIfNotEmpty(c.user.Project.ID, p.SetProjectid)
210+
211+
rules, err := c.cs.Firewall.ListEgressFirewallRules(p)
212+
if err != nil {
213+
return false, errors.Wrapf(err, "failed to list egress firewall rules for network ID %s", isoNet.Spec.ID)
214+
}
185215

186-
if proto == "icmp" {
187-
p.SetIcmptype(-1)
188-
p.SetIcmpcode(-1)
216+
// Create a map to track found protocols
217+
foundProtocols := make(map[string]bool)
218+
for _, proto := range protocols {
219+
foundProtocols[proto] = false
220+
}
221+
222+
// Check each rule for matching protocol and parameters
223+
for _, rule := range rules.EgressFirewallRules {
224+
if _, exists := foundProtocols[rule.Protocol]; exists {
225+
if rule.Protocol == "icmp" {
226+
// For ICMP, ensure icmptype and icmpcode are -1
227+
if rule.Icmptype == -1 && rule.Icmpcode == -1 {
228+
foundProtocols[rule.Protocol] = true
229+
}
230+
} else {
231+
// For TCP/UDP, ensure no specific ports are set (all ports)
232+
if rule.Startport == 0 && rule.Endport == 0 {
233+
foundProtocols[rule.Protocol] = true
234+
}
189235
}
236+
}
237+
}
190238

191-
_, err = c.cs.Firewall.CreateEgressFirewallRule(p)
239+
// Return true only if all required protocols are found
240+
for _, proto := range protocols {
241+
if !foundProtocols[proto] {
242+
return false, nil
192243
}
193-
if err != nil &&
194-
// Ignore errors regarding already existing fw rules for TCP/UDP for non-dynamic routing mode
195-
!strings.Contains(strings.ToLower(err.Error()), "there is already") &&
196-
!strings.Contains(strings.ToLower(err.Error()), "conflicts with rule") &&
197-
// Ignore errors regarding already existing fw rule for ICMP
198-
!strings.Contains(strings.ToLower(err.Error()), "new rule conflicts with existing rule") {
199-
retErr = errors.Wrapf(
200-
err, "failed creating egress firewall rule for network ID %s protocol %s", isoNet.Spec.ID, proto)
244+
}
245+
return true, nil
246+
}
247+
248+
// Helper function to get network details
249+
func (c *client) getNetwork(networkID string) (*cloudstack.Network, error) {
250+
network, count, err := c.cs.Network.GetNetworkByID(networkID, cloudstack.WithProject(c.user.Project.ID))
251+
if err != nil {
252+
return nil, errors.Wrapf(err, "failed to get network by ID %s", networkID)
253+
}
254+
if count == 0 {
255+
return nil, errors.Errorf("no network found with ID %s", networkID)
256+
}
257+
return network, nil
258+
}
259+
260+
// Helper function to create a firewall rule for a given protocol
261+
func (c *client) createFirewallRule(isoNet *infrav1.CloudStackIsolatedNetwork, proto string) error {
262+
var p interface{}
263+
if isoNet.Status.RoutingMode != "" {
264+
p = c.cs.Firewall.NewCreateRoutingFirewallRuleParams(isoNet.Spec.ID, proto)
265+
} else {
266+
p = c.cs.Firewall.NewCreateEgressFirewallRuleParams(isoNet.Spec.ID, proto)
267+
}
268+
269+
// Set ICMP-specific parameters
270+
if proto == "icmp" {
271+
if routingP, ok := p.(*cloudstack.CreateRoutingFirewallRuleParams); ok {
272+
routingP.SetIcmptype(-1)
273+
routingP.SetIcmpcode(-1)
274+
} else if egressP, ok := p.(*cloudstack.CreateEgressFirewallRuleParams); ok {
275+
egressP.SetIcmptype(-1)
276+
egressP.SetIcmpcode(-1)
201277
}
202278
}
203-
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(retErr)
204-
return retErr
279+
280+
// Create the firewall rule
281+
var err error
282+
if routingP, ok := p.(*cloudstack.CreateRoutingFirewallRuleParams); ok {
283+
_, err = c.cs.Firewall.CreateRoutingFirewallRule(routingP)
284+
} else if egressP, ok := p.(*cloudstack.CreateEgressFirewallRuleParams); ok {
285+
_, err = c.cs.Firewall.CreateEgressFirewallRule(egressP)
286+
}
287+
288+
// Ignore specific errors
289+
if err != nil && !c.isIgnorableError(err) {
290+
return errors.Wrapf(err, "failed creating firewall rule for network ID %s protocol %s", isoNet.Spec.ID, proto)
291+
}
292+
return nil
293+
}
294+
295+
// Helper function to check if an error is ignorable
296+
func (c *client) isIgnorableError(err error) bool {
297+
errorMsg := strings.ToLower(err.Error())
298+
return strings.Contains(errorMsg, "there is already") ||
299+
strings.Contains(errorMsg, "conflicts with rule") ||
300+
strings.Contains(errorMsg, "new rule conflicts with existing rule")
205301
}
206302

207303
// GetPublicIP gets a public IP with ID for cluster endpoint.

pkg/cloud/tags.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,30 @@ func (c *client) DoClusterTagsAllowDisposal(resourceType ResourceType, resourceI
117117

118118
// AddTags adds arbitrary tags to a resource.
119119
func (c *client) AddTags(resourceType ResourceType, resourceID string, tags map[string]string) error {
120-
p := c.cs.Resourcetags.NewCreateTagsParams([]string{resourceID}, string(resourceType), tags)
121-
_, err := c.cs.Resourcetags.CreateTags(p)
122-
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err)
123-
return ignoreAlreadyPresentErrors(err, resourceType, resourceID)
120+
// Retrieve existing tags
121+
existingTags, err := c.GetTags(resourceType, resourceID)
122+
if err != nil {
123+
return errors.Wrapf(err, "fetching tags for resource %s with ID %s", resourceType, resourceID)
124+
}
125+
126+
// Identify tags that need to be added or updated
127+
tagsToAdd := make(map[string]string)
128+
for key, value := range tags {
129+
if existingValue, exists := existingTags[key]; !exists || existingValue != value {
130+
tagsToAdd[key] = value
131+
}
132+
}
133+
134+
// If there are tags to add, call the API
135+
if len(tagsToAdd) > 0 {
136+
p := c.cs.Resourcetags.NewCreateTagsParams([]string{resourceID}, string(resourceType), tagsToAdd)
137+
if _, err := c.cs.Resourcetags.CreateTags(p); err != nil {
138+
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err)
139+
return errors.Wrapf(err, "creating tags for resource %s with ID %s", resourceType, resourceID)
140+
}
141+
}
142+
143+
return nil
124144
}
125145

126146
// GetTags gets all of a resource's tags.

0 commit comments

Comments
 (0)