-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: validates NUTANIX_ENDPOINT does not fall in the Load Balancer IP…
… Range
- Loading branch information
1 parent
11bff42
commit bff8f92
Showing
6 changed files
with
393 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// Copyright 2024 Nutanix. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package helpers | ||
|
||
import ( | ||
"fmt" | ||
"math/big" | ||
"net" | ||
) | ||
|
||
// IsIPInRange checks if the target IP falls within the start and end IP range (inclusive). | ||
func IsIPInRange(startIP, endIP, targetIP string) (bool, error) { | ||
// Parse the IPs | ||
start := net.ParseIP(startIP) | ||
end := net.ParseIP(endIP) | ||
target := net.ParseIP(targetIP) | ||
|
||
// Ensure all IPs are valid | ||
if start == nil { | ||
return false, fmt.Errorf("invalid start IP: %q", startIP) | ||
} | ||
if end == nil { | ||
return false, fmt.Errorf("invalid end IP: %q", endIP) | ||
} | ||
if target == nil { | ||
return false, fmt.Errorf("invalid target IP: %q", targetIP) | ||
} | ||
|
||
// Convert IPs to big integers | ||
startInt := ipToBigInt(start) | ||
endInt := ipToBigInt(end) | ||
targetInt := ipToBigInt(target) | ||
|
||
// Check if target IP is within the range | ||
return targetInt.Cmp(startInt) >= 0 && targetInt.Cmp(endInt) <= 0, nil | ||
} | ||
|
||
// ipToBigInt converts a net.IP to a big.Int for comparison. | ||
func ipToBigInt(ip net.IP) *big.Int { | ||
// Normalize to 16-byte representation for both IPv4 and IPv6 | ||
ip = ip.To16() | ||
return big.NewInt(0).SetBytes(ip) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
// Copyright 2024 Nutanix. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package helpers | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestIsIPInRange(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
startIP string | ||
endIP string | ||
targetIP string | ||
expectedInRange bool | ||
expectedErr error | ||
}{ | ||
{ | ||
name: "Valid range - target within range", | ||
startIP: "192.168.1.1", | ||
endIP: "192.168.1.10", | ||
targetIP: "192.168.1.5", | ||
expectedInRange: true, | ||
expectedErr: nil, | ||
}, | ||
{ | ||
name: "Valid range - target same as start IP", | ||
startIP: "192.168.1.1", | ||
endIP: "192.168.1.10", | ||
targetIP: "192.168.1.1", | ||
expectedInRange: true, | ||
expectedErr: nil, | ||
}, | ||
{ | ||
name: "Valid range - target same as end IP", | ||
startIP: "192.168.1.1", | ||
endIP: "192.168.1.10", | ||
targetIP: "192.168.1.10", | ||
expectedInRange: true, | ||
expectedErr: nil, | ||
}, | ||
{ | ||
name: "Valid range - target outside range", | ||
startIP: "192.168.1.1", | ||
endIP: "192.168.1.10", | ||
targetIP: "192.168.1.15", | ||
expectedInRange: false, | ||
expectedErr: nil, | ||
}, | ||
{ | ||
name: "Invalid start IP", | ||
startIP: "invalid-ip", | ||
endIP: "192.168.1.10", | ||
targetIP: "192.168.1.5", | ||
expectedInRange: false, | ||
expectedErr: fmt.Errorf("invalid start IP: %q", "invalid-ip"), | ||
}, | ||
{ | ||
name: "Invalid end IP", | ||
startIP: "192.168.1.1", | ||
endIP: "invalid-ip", | ||
targetIP: "192.168.1.5", | ||
expectedInRange: false, | ||
expectedErr: fmt.Errorf("invalid end IP: %q", "invalid-ip"), | ||
}, | ||
{ | ||
name: "Invalid target IP", | ||
startIP: "192.168.1.1", | ||
endIP: "192.168.1.10", | ||
targetIP: "invalid-ip", | ||
expectedInRange: false, | ||
expectedErr: fmt.Errorf("invalid target IP: %q", "invalid-ip"), | ||
}, | ||
{ | ||
name: "IPv6 range - target within range", | ||
startIP: "2001:db8::1", | ||
endIP: "2001:db8::10", | ||
targetIP: "2001:db8::5", | ||
expectedInRange: true, | ||
expectedErr: nil, | ||
}, | ||
{ | ||
name: "IPv6 range - target outside range", | ||
startIP: "2001:db8::1", | ||
endIP: "2001:db8::10", | ||
targetIP: "2001:db8::11", | ||
expectedInRange: false, | ||
expectedErr: nil, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := IsIPInRange(tt.startIP, tt.endIP, tt.targetIP) | ||
assert.Equal(t, tt.expectedInRange, got) | ||
if tt.expectedErr != nil { | ||
assert.EqualError(t, err, tt.expectedErr.Error()) | ||
} else { | ||
assert.NoError(t, err) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// Copyright 2024 Nutanix. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package cluster | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net" | ||
"net/http" | ||
|
||
v1 "k8s.io/api/admission/v1" | ||
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" | ||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||
|
||
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" | ||
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/variables" | ||
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/utils" | ||
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/helpers" | ||
) | ||
|
||
type nutanixValidator struct { | ||
client ctrlclient.Client | ||
decoder admission.Decoder | ||
} | ||
|
||
func NewNutanixValidator( | ||
client ctrlclient.Client, decoder admission.Decoder, | ||
) *nutanixValidator { | ||
return &nutanixValidator{ | ||
client: client, | ||
decoder: decoder, | ||
} | ||
} | ||
|
||
func (a *nutanixValidator) Validator() admission.HandlerFunc { | ||
return a.validate | ||
} | ||
|
||
func (a *nutanixValidator) validate( | ||
ctx context.Context, | ||
req admission.Request, | ||
) admission.Response { | ||
if req.Operation == v1.Delete { | ||
return admission.Allowed("") | ||
} | ||
|
||
cluster := &clusterv1.Cluster{} | ||
err := a.decoder.Decode(req, cluster) | ||
if err != nil { | ||
return admission.Errored(http.StatusBadRequest, err) | ||
} | ||
|
||
if cluster.Spec.Topology == nil { | ||
return admission.Allowed("") | ||
} | ||
|
||
if utils.GetProvider(cluster) != "nutanix" { | ||
return admission.Allowed("") | ||
} | ||
|
||
clusterConfig, err := variables.UnmarshalClusterConfigVariable(cluster.Spec.Topology.Variables) | ||
if err != nil { | ||
return admission.Denied( | ||
fmt.Errorf("failed to unmarshal cluster topology variable %q: %w", | ||
v1alpha1.ClusterConfigVariableName, | ||
err).Error(), | ||
) | ||
} | ||
|
||
if clusterConfig.Nutanix != nil && | ||
clusterConfig.Addons != nil { | ||
// Check if Prism Central IP is in MetalLB Load Balancer IP range. | ||
if err := checkIfPrismCentralIPInLoadBalancerIPRange( | ||
clusterConfig.Nutanix.PrismCentralEndpoint, | ||
clusterConfig.Addons.ServiceLoadBalancer, | ||
); err != nil { | ||
return admission.Denied(err.Error()) | ||
} | ||
} | ||
|
||
return admission.Allowed("") | ||
} | ||
|
||
// checkIfPrismCentralIPInLoadBalancerIPRange checks if the Prism Central IP is in the MetalLB Load Balancer IP range. | ||
// Errors out if Prism Central IP is in the Load Balancer IP range. | ||
func checkIfPrismCentralIPInLoadBalancerIPRange( | ||
pcEndpoint v1alpha1.NutanixPrismCentralEndpointSpec, | ||
serviceLoadBalancerConfiguration *v1alpha1.ServiceLoadBalancer, | ||
) error { | ||
if serviceLoadBalancerConfiguration == nil || | ||
serviceLoadBalancerConfiguration.Provider != v1alpha1.ServiceLoadBalancerProviderMetalLB || | ||
serviceLoadBalancerConfiguration.Configuration == nil { | ||
return nil | ||
} | ||
|
||
pcHostname, _, err := pcEndpoint.ParseURL() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
pcIP := net.ParseIP(pcHostname) | ||
// PC URL can contain IP/FQDN, so compare only if PC is an IP address. | ||
if pcIP == nil { | ||
return nil | ||
} | ||
|
||
for _, pool := range serviceLoadBalancerConfiguration.Configuration.AddressRanges { | ||
isIPInRange, err := helpers.IsIPInRange(pool.Start, pool.End, pcIP.String()) | ||
if err != nil { | ||
return fmt.Errorf( | ||
"error while checking if Prism Central IP %q is part of MetalLB address range %q-%q: %w", | ||
pcIP, | ||
pool.Start, | ||
pool.End, | ||
err, | ||
) | ||
} | ||
if isIPInRange { | ||
return fmt.Errorf("prism central IP %q must not be part of MetalLB address range %q-%q", | ||
pcIP, pool.Start, pool.End) | ||
} | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.