Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions PR_225_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# PR #225 Summary: CI/Bump Simulator Wait

## Overview
This PR increases the CloudStack simulator readiness wait time from 10 minutes to 20 minutes to stabilize the acceptance test matrix.

## Problem
- Acceptance test matrix experiencing frequent timeouts around 20-27 minute mark
- Jobs failing out of the "Run acceptance test" step
- Issue is consistent with CloudStack simulator readiness rather than provider logic

## Solution
- Double the simulator readiness wait from 10 minutes (20 × 30s) to 20 minutes (40 × 30s)
- Change in `.github/actions/setup-cloudstack/action.yml` line 46:
- **Before:** `until [ $T -gt 20 ] || curl -sfL http://localhost:8080 --output /dev/null`
- **After:** `until [ $T -gt 40 ] || curl -sfL http://localhost:8080 --output /dev/null`

## Changes Made
- **File:** `.github/actions/setup-cloudstack/action.yml`
- **Line 46:** Increased timeout from 20 to 40 iterations (10m → 20m)
- **Scope:** CI only - no provider logic changes
- **Risk:** None (CI-only change)

## Repository Setup
- **Location:** `~/Downloads/cloudstack-terraform-provider/`
- **Branch:** `pr-225` (checked out from PR #225)
- **Build Status:** ✅ Successfully built
- **Binary:** `~/go/bin/terraform-provider-cloudstack`

## Dependencies Verified
- ✅ Go 1.25.1 (required: 1.20+)
- ✅ Terraform 1.10.5 (required: 1.0.x)
- ✅ Docker 28.4.0 (for CloudStack simulator)

## Testing
To test the changes:
1. Run acceptance tests: `make testacc`
2. Monitor CI logs for the increased wait time
3. Verify tests complete within the extended timeout

## Related Issues
- Relates to #218 (stabilizes acceptance matrix for that feature PR)
- Addresses frequent CI timeouts in acceptance test matrix
209 changes: 193 additions & 16 deletions cloudstack/resource_cloudstack_egress_firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package cloudstack

import (
"fmt"
"sort"
"strconv"
"strings"
"sync"
Expand All @@ -31,6 +32,81 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// isAllPortsTCPUDP determines if a rule represents all ports for TCP/UDP protocols across CloudStack versions that may return 0/0, -1/-1, or 1/65535
func isAllPortsTCPUDP(protocol string, start, end int) bool {
p := strings.ToLower(protocol)
if p != "tcp" && p != "udp" {
return false
}
// handle various representations of all ports across CloudStack versions
if (start == 0 && end == 0) ||
(start == -1 && end == -1) ||
(start == 1 && end == 65535) {
return true
}
return false
}

// normalizeRemoteCIDRs normalizes a comma-separated CIDR string from CloudStack API
func normalizeRemoteCIDRs(cidrList string) []string {
if cidrList == "" {
return []string{}
}

cidrs := strings.Split(cidrList, ",")
normalized := make([]string, 0, len(cidrs))

for _, cidr := range cidrs {
trimmed := strings.TrimSpace(cidr)
if trimmed != "" {
normalized = append(normalized, trimmed)
}
}

sort.Strings(normalized)
return normalized
}

// normalizeLocalCIDRs normalizes a Terraform schema.Set of CIDRs
func normalizeLocalCIDRs(cidrSet *schema.Set) []string {
if cidrSet == nil {
return []string{}
}

normalized := make([]string, 0, cidrSet.Len())
for _, cidr := range cidrSet.List() {
if cidrStr, ok := cidr.(string); ok {
trimmed := strings.TrimSpace(cidrStr)
if trimmed != "" {
normalized = append(normalized, trimmed)
}
}
}

sort.Strings(normalized)
return normalized
}

// cidrSetsEqual compares normalized CIDR sets for equality (order/whitespace agnostic)
func cidrSetsEqual(remoteCidrList string, localCidrSet *schema.Set) bool {
remoteCidrs := normalizeRemoteCIDRs(remoteCidrList)
localCidrs := normalizeLocalCIDRs(localCidrSet)

// Compare lengths first
if len(remoteCidrs) != len(localCidrs) {
return false
}

// Compare each element (both are already sorted)
for i, remoteCidr := range remoteCidrs {
if remoteCidr != localCidrs[i] {
return false
}
}

return true
}

func resourceCloudStackEgressFirewall() *schema.Resource {
return &schema.Resource{
Create: resourceCloudStackEgressFirewallCreate,
Expand Down Expand Up @@ -250,6 +326,17 @@ func createEgressFirewallRule(d *schema.ResourceData, meta interface{}, rule map
uuids[port.(string)] = r.Id
rule["uuids"] = uuids
}
} else {
// No ports specified - create a rule that encompasses all ports
// by not setting startport and endport parameters
r, err := cs.Firewall.CreateEgressFirewallRule(p)
if err != nil {
return fmt.Errorf("failed to create all-ports egress firewall rule: %w", err)
}
uuids["all"] = r.Id
rule["uuids"] = uuids
// Remove the ports field since we're creating an all-ports rule
delete(rule, "ports")
}
}

Expand Down Expand Up @@ -315,8 +402,13 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface

// Create a set with all CIDR's
cidrs := &schema.Set{F: schema.HashString}
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidrs.Add(cidr)
if r.Cidrlist != "" {
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidr = strings.TrimSpace(cidr)
if cidr != "" {
cidrs.Add(cidr)
}
}
}

// Update the values
Expand Down Expand Up @@ -353,8 +445,13 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface

// Create a set with all CIDR's
cidrs := &schema.Set{F: schema.HashString}
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidrs.Add(cidr)
if r.Cidrlist != "" {
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidr = strings.TrimSpace(cidr)
if cidr != "" {
cidrs.Add(cidr)
}
}
}

// Update the values
Expand All @@ -368,6 +465,80 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface
rule["ports"] = ports
rules.Add(rule)
}
} else {
// No ports specified - check if we have an "all" rule
id, ok := uuids["all"]
if !ok {
continue
}

// Get the rule
r, ok := ruleMap[id.(string)]
if !ok {
delete(uuids, "all")
continue
}

// Verify this is actually an all-ports rule using our helper
if !isAllPortsTCPUDP(r.Protocol, r.Startport, r.Endport) {
// This rule has specific ports, but we expected all-ports
// This might happen if CloudStack behavior changed
continue
}

// Delete the known rule so only unknown rules remain in the ruleMap
delete(ruleMap, id.(string))

// Create a set with all CIDR's
cidrs := &schema.Set{F: schema.HashString}
if r.Cidrlist != "" {
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidr = strings.TrimSpace(cidr)
if cidr != "" {
cidrs.Add(cidr)
}
}
}

// Update the values
rule["protocol"] = r.Protocol
rule["cidr_list"] = cidrs
// Remove ports field for all-ports rules
delete(rule, "ports")
rules.Add(rule)
}
}

// Fallback: Check if any remaining rules in ruleMap match our expected all-ports pattern
// This handles cases where CloudStack might return all-ports rules in unexpected formats
if rule["protocol"].(string) != "icmp" && strings.ToLower(rule["protocol"].(string)) != "all" {
// Look for any remaining rules that might be our all-ports rule
for ruleID, r := range ruleMap {
// Get local CIDR set for comparison
localCidrSet, ok := rule["cidr_list"].(*schema.Set)
if !ok {
continue
}

if isAllPortsTCPUDP(r.Protocol, r.Startport, r.Endport) &&
strings.EqualFold(r.Protocol, rule["protocol"].(string)) &&
cidrSetsEqual(r.Cidrlist, localCidrSet) {
// This looks like our all-ports rule, add it to state
cidrs := &schema.Set{F: schema.HashString}
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidrs.Add(cidr)
}

rule["protocol"] = r.Protocol
rule["cidr_list"] = cidrs
// Remove ports field for all-ports rules
delete(rule, "ports")
rules.Add(rule)

// Remove from ruleMap so it's not processed again
delete(ruleMap, ruleID)
break
}
}
}
if strings.ToLower(rule["protocol"].(string)) == "all" {
Expand All @@ -389,8 +560,13 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface
// Create a set with all CIDR's
if _, ok := rule["cidr_list"]; ok {
cidrs := &schema.Set{F: schema.HashString}
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidrs.Add(cidr)
if r.Cidrlist != "" {
for _, cidr := range strings.Split(r.Cidrlist, ",") {
cidr = strings.TrimSpace(cidr)
if cidr != "" {
cidrs.Add(cidr)
}
}
}
rule["cidr_list"] = cidrs
}
Expand Down Expand Up @@ -575,12 +751,14 @@ func verifyEgressFirewallParams(d *schema.ResourceData) error {

if !rules && !managed {
return fmt.Errorf(
"You must supply at least one 'rule' when not using the 'managed' firewall feature")
"you must supply at least one 'rule' when not using the 'managed' firewall feature")
}

return nil
}

// verifyEgressFirewallRuleParams validates egress firewall rule parameters.
// Note: ports parameter is optional for TCP/UDP protocols - when omitted, the rule will encompass all ports
func verifyEgressFirewallRuleParams(d *schema.ResourceData, rule map[string]interface{}) error {
protocol := rule["protocol"].(string)
if strings.ToLower(protocol) != "all" && protocol != "tcp" && protocol != "udp" && protocol != "icmp" {
Expand All @@ -590,12 +768,12 @@ func verifyEgressFirewallRuleParams(d *schema.ResourceData, rule map[string]inte

if protocol == "icmp" {
if _, ok := rule["icmp_type"]; !ok {
return fmt.Errorf(
"Parameter icmp_type is a required parameter when using protocol 'icmp'")
return fmt.Errorf(
"parameter icmp_type is a required parameter when using protocol 'icmp'")
}
if _, ok := rule["icmp_code"]; !ok {
return fmt.Errorf(
"Parameter icmp_code is a required parameter when using protocol 'icmp'")
return fmt.Errorf(
"parameter icmp_code is a required parameter when using protocol 'icmp'")
}
} else if strings.ToLower(protocol) != "all" {
if ports, ok := rule["ports"].(*schema.Set); ok {
Expand All @@ -606,16 +784,15 @@ func verifyEgressFirewallRuleParams(d *schema.ResourceData, rule map[string]inte
"%q is not a valid port value. Valid options are '80' or '80-90'", port.(string))
}
}
} else {
return fmt.Errorf(
"Parameter ports is a required parameter when *not* using protocol 'icmp'")
}
} else if strings.ToLower(protocol) == "all" {
if ports, _ := rule["ports"].(*schema.Set); ports.Len() > 0 {
return fmt.Errorf(
"Parameter ports is not required when using protocol 'ALL'")
return fmt.Errorf(
"parameter ports is not required when using protocol 'ALL'")
}
}

return nil
}

// Formatting fix
Loading
Loading