Skip to content

Commit c5829a2

Browse files
authored
Merge branch 'main' into dependabot/github_actions/github-actions-167a898cab
2 parents 68efae8 + 3b244f1 commit c5829a2

File tree

2 files changed

+210
-9
lines changed

2 files changed

+210
-9
lines changed

cloudconnexa/cloudconnexa.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ func (e ErrClientResponse) Error() string {
7171
return fmt.Sprintf("status code: %d, response body: %s", e.status, e.body)
7272
}
7373

74+
// StatusCode returns the HTTP status code of the API error response.
75+
func (e ErrClientResponse) StatusCode() int { return e.status }
76+
77+
// Body returns the raw response body of the API error response.
78+
func (e ErrClientResponse) Body() string { return e.body }
79+
7480
// NewClient creates a new CloudConnexa API client with the given credentials.
7581
// It authenticates using OAuth2 client credentials flow and returns a configured client.
7682
func NewClient(baseURL, clientID, clientSecret string) (*Client, error) {

e2e/client_test.go

Lines changed: 204 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package client
22

33
import (
4+
"crypto/rand"
5+
"encoding/binary"
6+
"errors"
47
"fmt"
8+
mrand "math/rand"
9+
"net"
510
"os"
611
"testing"
712
"time"
@@ -49,6 +54,169 @@ func setUpClient(t *testing.T) *cloudconnexa.Client {
4954
return client
5055
}
5156

57+
// cidrOverlaps returns true if two IPv4 networks overlap (IPv4 only, safe)
58+
func cidrOverlaps(a *net.IPNet, b *net.IPNet) bool {
59+
if a == nil || b == nil {
60+
return false
61+
}
62+
if a.IP.To4() == nil || b.IP.To4() == nil {
63+
return false
64+
}
65+
return a.Contains(b.IP) || b.Contains(a.IP)
66+
}
67+
68+
// parseCIDROrNil parses CIDR string and returns *net.IPNet or nil on error
69+
func parseCIDROrNil(cidr string) *net.IPNet {
70+
_, ipnet, err := net.ParseCIDR(cidr)
71+
if err != nil {
72+
return nil
73+
}
74+
return ipnet
75+
}
76+
77+
func shuffledRange(start, end int, rnd *mrand.Rand) []int {
78+
n := end - start + 1
79+
arr := make([]int, n)
80+
for i := 0; i < n; i++ {
81+
arr[i] = start + i
82+
}
83+
rnd.Shuffle(n, func(i, j int) { arr[i], arr[j] = arr[j], arr[i] })
84+
return arr
85+
}
86+
87+
// findAvailableInRange scans used subnets and returns a free 10.a.b.0/24 within [startA, endA]
88+
func findAvailableInRange(used []*net.IPNet, startA, endA int, rnd *mrand.Rand) (string, bool) {
89+
// Skip the commonly reserved 10.200.0.0/16 range first
90+
reserved := parseCIDROrNil("10.200.0.0/16")
91+
for _, a := range shuffledRange(startA, endA, rnd) {
92+
for _, b := range shuffledRange(0, 255, rnd) {
93+
candidate := fmt.Sprintf("10.%d.%d.0/24", a, b)
94+
_, ipn, err := net.ParseCIDR(candidate)
95+
if err != nil {
96+
continue
97+
}
98+
if reserved != nil && cidrOverlaps(ipn, reserved) {
99+
continue
100+
}
101+
overlap := false
102+
for _, u := range used {
103+
if cidrOverlaps(ipn, u) {
104+
overlap = true
105+
break
106+
}
107+
}
108+
if !overlap {
109+
return candidate, true
110+
}
111+
}
112+
}
113+
return "", false
114+
}
115+
116+
// findAvailableInRange172 scans used subnets for a free 172.16-31.b.0/24
117+
func findAvailableInRange172(used []*net.IPNet, rnd *mrand.Rand) (string, bool) {
118+
for _, a := range shuffledRange(16, 31, rnd) {
119+
for _, b := range shuffledRange(0, 255, rnd) {
120+
candidate := fmt.Sprintf("172.%d.%d.0/24", a, b)
121+
_, ipn, err := net.ParseCIDR(candidate)
122+
if err != nil {
123+
continue
124+
}
125+
overlap := false
126+
for _, u := range used {
127+
if cidrOverlaps(ipn, u) {
128+
overlap = true
129+
break
130+
}
131+
}
132+
if !overlap {
133+
return candidate, true
134+
}
135+
}
136+
}
137+
return "", false
138+
}
139+
140+
// findAvailableInRange192168 scans used subnets for a free 192.168.b.0/24
141+
func findAvailableInRange192168(used []*net.IPNet, rnd *mrand.Rand) (string, bool) {
142+
for _, b := range shuffledRange(0, 255, rnd) {
143+
candidate := fmt.Sprintf("192.168.%d.0/24", b)
144+
_, ipn, err := net.ParseCIDR(candidate)
145+
if err != nil {
146+
continue
147+
}
148+
overlap := false
149+
for _, u := range used {
150+
if cidrOverlaps(ipn, u) {
151+
overlap = true
152+
break
153+
}
154+
}
155+
if !overlap {
156+
return candidate, true
157+
}
158+
}
159+
return "", false
160+
}
161+
162+
// findAvailableIPv4Subnet scans existing networks' routes and system subnets
163+
// and returns an available RFC1918 /24 subnet that does not overlap
164+
func findAvailableIPv4Subnet(c *cloudconnexa.Client) (string, error) {
165+
var seedBytes [8]byte
166+
_, _ = rand.Read(seedBytes[:])
167+
rnd := mrand.New(mrand.NewSource(int64(binary.LittleEndian.Uint64(seedBytes[:]))))
168+
169+
networks, err := c.Networks.List()
170+
if err != nil {
171+
return "", err
172+
}
173+
174+
var used []*net.IPNet
175+
for _, n := range networks {
176+
// Collect existing routes via API to ensure we see them
177+
routes, err := c.Routes.List(n.ID)
178+
if err == nil {
179+
for _, r := range routes {
180+
if r.Subnet == "" {
181+
continue
182+
}
183+
if ipn := parseCIDROrNil(r.Subnet); ipn != nil {
184+
used = append(used, ipn)
185+
}
186+
}
187+
}
188+
// Collect system subnets from GET network (may not be present in List)
189+
if nn, err := c.Networks.Get(n.ID); err == nil && nn != nil {
190+
for _, s := range nn.SystemSubnets {
191+
if ipn := parseCIDROrNil(s); ipn != nil {
192+
used = append(used, ipn)
193+
}
194+
}
195+
}
196+
}
197+
198+
// Try 10.0.0.0/8 excluding known reserved 10.200.0.0/16, prefer higher ranges
199+
if candidate, ok := findAvailableInRange(used, 201, 254, rnd); ok {
200+
return candidate, nil
201+
}
202+
if candidate, ok := findAvailableInRange(used, 0, 199, rnd); ok {
203+
return candidate, nil
204+
}
205+
if candidate, ok := findAvailableInRange(used, 200, 200, rnd); ok {
206+
return candidate, nil
207+
}
208+
// Try 172.16.0.0/12
209+
if candidate, ok := findAvailableInRange172(used, rnd); ok {
210+
return candidate, nil
211+
}
212+
// Try 192.168.0.0/16
213+
if candidate, ok := findAvailableInRange192168(used, rnd); ok {
214+
return candidate, nil
215+
}
216+
217+
return "", fmt.Errorf("no available /24 subnet found in RFC1918 ranges")
218+
}
219+
52220
// TestListNetworks tests the retrieval of networks using pagination
53221
// It verifies that networks can be retrieved successfully
54222
func TestListNetworks(t *testing.T) {
@@ -115,11 +283,7 @@ func TestCreateNetwork(t *testing.T) {
115283
Name: testName,
116284
VpnRegionID: "it-mxp",
117285
}
118-
route := cloudconnexa.Route{
119-
Description: "test",
120-
Type: "IP_V4",
121-
Subnet: fmt.Sprintf("10.%d.%d.0/24", timestamp%256, (timestamp/256)%256),
122-
}
286+
123287
network := cloudconnexa.Network{
124288
Description: "test",
125289
Egress: false,
@@ -131,15 +295,46 @@ func TestCreateNetwork(t *testing.T) {
131295
response, err := c.Networks.Create(network)
132296
require.NoError(t, err)
133297
fmt.Printf("created %s network\n", response.ID)
134-
test, err := c.Routes.Create(response.ID, route)
135-
require.NoError(t, err)
136-
fmt.Printf("created %s route\n", test.ID)
298+
// Ensure cleanup even if subsequent steps fail
299+
defer func() { _ = c.Networks.Delete(response.ID) }()
300+
301+
// Attempt to create a non-overlapping route with retries to avoid CI matrix collisions
302+
var testRoute *cloudconnexa.Route
303+
var lastErr error
304+
for attempts := 0; attempts < 20; attempts++ {
305+
subnet, serr := findAvailableIPv4Subnet(c)
306+
require.NoError(t, serr)
307+
route := cloudconnexa.Route{
308+
Description: "test",
309+
Type: "IP_V4",
310+
Subnet: subnet,
311+
}
312+
testRoute, err = c.Routes.Create(response.ID, route)
313+
if err == nil {
314+
fmt.Printf("created %s route\n", testRoute.ID)
315+
break
316+
}
317+
lastErr = err
318+
var apiErr *cloudconnexa.ErrClientResponse
319+
if errors.As(err, &apiErr) {
320+
if apiErr.StatusCode() == 400 {
321+
// Overlap or validation error, refresh and retry
322+
time.Sleep(500 * time.Millisecond)
323+
continue
324+
}
325+
}
326+
// Unexpected error
327+
require.NoError(t, err)
328+
}
329+
require.NoError(t, lastErr)
330+
require.NotNil(t, testRoute)
331+
137332
serviceConfig := cloudconnexa.IPServiceConfig{
138333
ServiceTypes: []string{"ANY"},
139334
}
140335
ipServiceRoute := cloudconnexa.IPServiceRoute{
141336
Description: "test",
142-
Value: fmt.Sprintf("10.%d.%d.0/24", timestamp%256, (timestamp/256)%256),
337+
Value: testRoute.Subnet,
143338
}
144339
service := cloudconnexa.IPService{
145340
Name: testName,

0 commit comments

Comments
 (0)