Skip to content

Commit bff8f92

Browse files
committed
fix: validates NUTANIX_ENDPOINT does not fall in the Load Balancer IP Range
1 parent 11bff42 commit bff8f92

File tree

6 files changed

+393
-1
lines changed

6 files changed

+393
-1
lines changed

api/v1alpha1/nutanix_clusterconfig_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ type NutanixPrismCentralEndpointCredentials struct {
5757
//nolint:gocritic // No need for named return values
5858
func (s NutanixPrismCentralEndpointSpec) ParseURL() (string, uint16, error) {
5959
var prismCentralURL *url.URL
60-
prismCentralURL, err := url.Parse(s.URL)
60+
prismCentralURL, err := url.ParseRequestURI(s.URL)
6161
if err != nil {
6262
return "", 0, fmt.Errorf("error parsing Prism Central URL: %w", err)
6363
}

pkg/helpers/helpers.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2024 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package helpers
4+
5+
import (
6+
"fmt"
7+
"math/big"
8+
"net"
9+
)
10+
11+
// IsIPInRange checks if the target IP falls within the start and end IP range (inclusive).
12+
func IsIPInRange(startIP, endIP, targetIP string) (bool, error) {
13+
// Parse the IPs
14+
start := net.ParseIP(startIP)
15+
end := net.ParseIP(endIP)
16+
target := net.ParseIP(targetIP)
17+
18+
// Ensure all IPs are valid
19+
if start == nil {
20+
return false, fmt.Errorf("invalid start IP: %q", startIP)
21+
}
22+
if end == nil {
23+
return false, fmt.Errorf("invalid end IP: %q", endIP)
24+
}
25+
if target == nil {
26+
return false, fmt.Errorf("invalid target IP: %q", targetIP)
27+
}
28+
29+
// Convert IPs to big integers
30+
startInt := ipToBigInt(start)
31+
endInt := ipToBigInt(end)
32+
targetInt := ipToBigInt(target)
33+
34+
// Check if target IP is within the range
35+
return targetInt.Cmp(startInt) >= 0 && targetInt.Cmp(endInt) <= 0, nil
36+
}
37+
38+
// ipToBigInt converts a net.IP to a big.Int for comparison.
39+
func ipToBigInt(ip net.IP) *big.Int {
40+
// Normalize to 16-byte representation for both IPv4 and IPv6
41+
ip = ip.To16()
42+
return big.NewInt(0).SetBytes(ip)
43+
}

pkg/helpers/helpers_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2024 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package helpers
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestIsIPInRange(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
startIP string
17+
endIP string
18+
targetIP string
19+
expectedInRange bool
20+
expectedErr error
21+
}{
22+
{
23+
name: "Valid range - target within range",
24+
startIP: "192.168.1.1",
25+
endIP: "192.168.1.10",
26+
targetIP: "192.168.1.5",
27+
expectedInRange: true,
28+
expectedErr: nil,
29+
},
30+
{
31+
name: "Valid range - target same as start IP",
32+
startIP: "192.168.1.1",
33+
endIP: "192.168.1.10",
34+
targetIP: "192.168.1.1",
35+
expectedInRange: true,
36+
expectedErr: nil,
37+
},
38+
{
39+
name: "Valid range - target same as end IP",
40+
startIP: "192.168.1.1",
41+
endIP: "192.168.1.10",
42+
targetIP: "192.168.1.10",
43+
expectedInRange: true,
44+
expectedErr: nil,
45+
},
46+
{
47+
name: "Valid range - target outside range",
48+
startIP: "192.168.1.1",
49+
endIP: "192.168.1.10",
50+
targetIP: "192.168.1.15",
51+
expectedInRange: false,
52+
expectedErr: nil,
53+
},
54+
{
55+
name: "Invalid start IP",
56+
startIP: "invalid-ip",
57+
endIP: "192.168.1.10",
58+
targetIP: "192.168.1.5",
59+
expectedInRange: false,
60+
expectedErr: fmt.Errorf("invalid start IP: %q", "invalid-ip"),
61+
},
62+
{
63+
name: "Invalid end IP",
64+
startIP: "192.168.1.1",
65+
endIP: "invalid-ip",
66+
targetIP: "192.168.1.5",
67+
expectedInRange: false,
68+
expectedErr: fmt.Errorf("invalid end IP: %q", "invalid-ip"),
69+
},
70+
{
71+
name: "Invalid target IP",
72+
startIP: "192.168.1.1",
73+
endIP: "192.168.1.10",
74+
targetIP: "invalid-ip",
75+
expectedInRange: false,
76+
expectedErr: fmt.Errorf("invalid target IP: %q", "invalid-ip"),
77+
},
78+
{
79+
name: "IPv6 range - target within range",
80+
startIP: "2001:db8::1",
81+
endIP: "2001:db8::10",
82+
targetIP: "2001:db8::5",
83+
expectedInRange: true,
84+
expectedErr: nil,
85+
},
86+
{
87+
name: "IPv6 range - target outside range",
88+
startIP: "2001:db8::1",
89+
endIP: "2001:db8::10",
90+
targetIP: "2001:db8::11",
91+
expectedInRange: false,
92+
expectedErr: nil,
93+
},
94+
}
95+
96+
for _, tt := range tests {
97+
t.Run(tt.name, func(t *testing.T) {
98+
got, err := IsIPInRange(tt.startIP, tt.endIP, tt.targetIP)
99+
assert.Equal(t, tt.expectedInRange, got)
100+
if tt.expectedErr != nil {
101+
assert.EqualError(t, err, tt.expectedErr.Error())
102+
} else {
103+
assert.NoError(t, err)
104+
}
105+
})
106+
}
107+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2024 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cluster
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"net"
10+
"net/http"
11+
12+
v1 "k8s.io/api/admission/v1"
13+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
14+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
15+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
16+
17+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
18+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/variables"
19+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/utils"
20+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/helpers"
21+
)
22+
23+
type nutanixValidator struct {
24+
client ctrlclient.Client
25+
decoder admission.Decoder
26+
}
27+
28+
func NewNutanixValidator(
29+
client ctrlclient.Client, decoder admission.Decoder,
30+
) *nutanixValidator {
31+
return &nutanixValidator{
32+
client: client,
33+
decoder: decoder,
34+
}
35+
}
36+
37+
func (a *nutanixValidator) Validator() admission.HandlerFunc {
38+
return a.validate
39+
}
40+
41+
func (a *nutanixValidator) validate(
42+
ctx context.Context,
43+
req admission.Request,
44+
) admission.Response {
45+
if req.Operation == v1.Delete {
46+
return admission.Allowed("")
47+
}
48+
49+
cluster := &clusterv1.Cluster{}
50+
err := a.decoder.Decode(req, cluster)
51+
if err != nil {
52+
return admission.Errored(http.StatusBadRequest, err)
53+
}
54+
55+
if cluster.Spec.Topology == nil {
56+
return admission.Allowed("")
57+
}
58+
59+
if utils.GetProvider(cluster) != "nutanix" {
60+
return admission.Allowed("")
61+
}
62+
63+
clusterConfig, err := variables.UnmarshalClusterConfigVariable(cluster.Spec.Topology.Variables)
64+
if err != nil {
65+
return admission.Denied(
66+
fmt.Errorf("failed to unmarshal cluster topology variable %q: %w",
67+
v1alpha1.ClusterConfigVariableName,
68+
err).Error(),
69+
)
70+
}
71+
72+
if clusterConfig.Nutanix != nil &&
73+
clusterConfig.Addons != nil {
74+
// Check if Prism Central IP is in MetalLB Load Balancer IP range.
75+
if err := checkIfPrismCentralIPInLoadBalancerIPRange(
76+
clusterConfig.Nutanix.PrismCentralEndpoint,
77+
clusterConfig.Addons.ServiceLoadBalancer,
78+
); err != nil {
79+
return admission.Denied(err.Error())
80+
}
81+
}
82+
83+
return admission.Allowed("")
84+
}
85+
86+
// checkIfPrismCentralIPInLoadBalancerIPRange checks if the Prism Central IP is in the MetalLB Load Balancer IP range.
87+
// Errors out if Prism Central IP is in the Load Balancer IP range.
88+
func checkIfPrismCentralIPInLoadBalancerIPRange(
89+
pcEndpoint v1alpha1.NutanixPrismCentralEndpointSpec,
90+
serviceLoadBalancerConfiguration *v1alpha1.ServiceLoadBalancer,
91+
) error {
92+
if serviceLoadBalancerConfiguration == nil ||
93+
serviceLoadBalancerConfiguration.Provider != v1alpha1.ServiceLoadBalancerProviderMetalLB ||
94+
serviceLoadBalancerConfiguration.Configuration == nil {
95+
return nil
96+
}
97+
98+
pcHostname, _, err := pcEndpoint.ParseURL()
99+
if err != nil {
100+
return err
101+
}
102+
103+
pcIP := net.ParseIP(pcHostname)
104+
// PC URL can contain IP/FQDN, so compare only if PC is an IP address.
105+
if pcIP == nil {
106+
return nil
107+
}
108+
109+
for _, pool := range serviceLoadBalancerConfiguration.Configuration.AddressRanges {
110+
isIPInRange, err := helpers.IsIPInRange(pool.Start, pool.End, pcIP.String())
111+
if err != nil {
112+
return fmt.Errorf(
113+
"error while checking if Prism Central IP %q is part of MetalLB address range %q-%q: %w",
114+
pcIP,
115+
pool.Start,
116+
pool.End,
117+
err,
118+
)
119+
}
120+
if isIPInRange {
121+
return fmt.Errorf("prism central IP %q must not be part of MetalLB address range %q-%q",
122+
pcIP, pool.Start, pool.End)
123+
}
124+
}
125+
126+
return nil
127+
}

0 commit comments

Comments
 (0)