Skip to content

Commit

Permalink
fix: validates NUTANIX_ENDPOINT does not fall in the Load Balancer IP…
Browse files Browse the repository at this point in the history
… Range
  • Loading branch information
manoj-nutanix committed Dec 11, 2024
1 parent 11bff42 commit bff8f92
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 1 deletion.
2 changes: 1 addition & 1 deletion api/v1alpha1/nutanix_clusterconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type NutanixPrismCentralEndpointCredentials struct {
//nolint:gocritic // No need for named return values
func (s NutanixPrismCentralEndpointSpec) ParseURL() (string, uint16, error) {
var prismCentralURL *url.URL
prismCentralURL, err := url.Parse(s.URL)
prismCentralURL, err := url.ParseRequestURI(s.URL)
if err != nil {
return "", 0, fmt.Errorf("error parsing Prism Central URL: %w", err)
}
Expand Down
43 changes: 43 additions & 0 deletions pkg/helpers/helpers.go
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)
}
107 changes: 107 additions & 0 deletions pkg/helpers/helpers_test.go
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)
}
})
}
}
127 changes: 127 additions & 0 deletions pkg/webhook/cluster/nutanix_validator.go
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
}
Loading

0 comments on commit bff8f92

Please sign in to comment.