-
Notifications
You must be signed in to change notification settings - Fork 7
fix: validates PC IP is outside Load Balancer IP Range #1001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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,26 @@ | ||
// Copyright 2024 Nutanix. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package helpers | ||
|
||
import ( | ||
"fmt" | ||
"net/netip" | ||
) | ||
|
||
// IsIPInRange checks if the target IP falls within the start and end IP range (inclusive). | ||
func IsIPInRange(startIP, endIP, targetIP string) (bool, error) { | ||
manoj-nutanix marked this conversation as resolved.
Show resolved
Hide resolved
|
||
start, err := netip.ParseAddr(startIP) | ||
if err != nil { | ||
return false, fmt.Errorf("invalid start IP: %w", err) | ||
} | ||
end, err := netip.ParseAddr(endIP) | ||
if err != nil { | ||
return false, fmt.Errorf("invalid end IP: %w", err) | ||
} | ||
target, err := netip.ParseAddr(targetIP) | ||
if err != nil { | ||
return false, fmt.Errorf("invalid target IP: %w", err) | ||
} | ||
|
||
return start.Compare(target) <= 0 && end.Compare(target) >= 0, nil | ||
} |
This file contains hidden or 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,116 @@ | ||
// 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: ParseAddr(%q): unable to parse IP", | ||
"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: ParseAddr(%q): unable to parse IP", | ||
"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: ParseAddr(%q): unable to parse IP", | ||
"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 hidden or 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( | ||
manoj-nutanix marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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", | ||
manoj-nutanix marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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", | ||
manoj-nutanix marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pcIP, pool.Start, pool.End) | ||
} | ||
} | ||
|
||
return nil | ||
} |
This file contains hidden or 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,114 @@ | ||
// Copyright 2024 Nutanix. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package cluster | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" | ||
) | ||
|
||
func TestCheckIfPrismCentralIPInLoadBalancerIPRange(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
pcEndpoint v1alpha1.NutanixPrismCentralEndpointSpec | ||
serviceLoadBalancerConfiguration *v1alpha1.ServiceLoadBalancer | ||
expectedErr error | ||
}{ | ||
{ | ||
name: "PC IP not in range", | ||
pcEndpoint: v1alpha1.NutanixPrismCentralEndpointSpec{ | ||
URL: "https://192.168.1.1:9440", | ||
}, | ||
serviceLoadBalancerConfiguration: &v1alpha1.ServiceLoadBalancer{ | ||
Provider: v1alpha1.ServiceLoadBalancerProviderMetalLB, | ||
Configuration: &v1alpha1.ServiceLoadBalancerConfiguration{ | ||
AddressRanges: []v1alpha1.AddressRange{ | ||
{Start: "192.168.1.10", End: "192.168.1.20"}, | ||
}, | ||
}, | ||
}, | ||
expectedErr: nil, | ||
}, | ||
{ | ||
name: "PC IP in range", | ||
pcEndpoint: v1alpha1.NutanixPrismCentralEndpointSpec{ | ||
URL: "https://192.168.1.15:9440", | ||
}, | ||
serviceLoadBalancerConfiguration: &v1alpha1.ServiceLoadBalancer{ | ||
Provider: v1alpha1.ServiceLoadBalancerProviderMetalLB, | ||
Configuration: &v1alpha1.ServiceLoadBalancerConfiguration{ | ||
AddressRanges: []v1alpha1.AddressRange{ | ||
{Start: "192.168.1.10", End: "192.168.1.20"}, | ||
}, | ||
}, | ||
}, | ||
expectedErr: fmt.Errorf( | ||
"prism central IP %q must not be part of MetalLB address range %q-%q", | ||
"192.168.1.15", | ||
"192.168.1.10", | ||
"192.168.1.20", | ||
), | ||
}, | ||
{ | ||
name: "Invalid Prism Central URL", | ||
pcEndpoint: v1alpha1.NutanixPrismCentralEndpointSpec{ | ||
URL: "invalid-url", | ||
}, | ||
serviceLoadBalancerConfiguration: &v1alpha1.ServiceLoadBalancer{ | ||
Provider: v1alpha1.ServiceLoadBalancerProviderMetalLB, | ||
Configuration: &v1alpha1.ServiceLoadBalancerConfiguration{ | ||
AddressRanges: []v1alpha1.AddressRange{ | ||
{Start: "192.168.1.10", End: "192.168.1.20"}, | ||
}, | ||
}, | ||
}, | ||
expectedErr: fmt.Errorf( | ||
"error parsing Prism Central URL: parse %q: invalid URI for request", | ||
"invalid-url", | ||
), | ||
}, | ||
{ | ||
name: "Service Load Balancer Configuration is nil", | ||
pcEndpoint: v1alpha1.NutanixPrismCentralEndpointSpec{ | ||
URL: "https://192.168.1.1:9440", | ||
}, | ||
serviceLoadBalancerConfiguration: nil, | ||
expectedErr: nil, | ||
}, | ||
{ | ||
name: "Provider is not MetalLB", | ||
pcEndpoint: v1alpha1.NutanixPrismCentralEndpointSpec{ | ||
URL: "https://192.168.1.1:9440", | ||
}, | ||
serviceLoadBalancerConfiguration: &v1alpha1.ServiceLoadBalancer{ | ||
Provider: "other-provider", | ||
Configuration: &v1alpha1.ServiceLoadBalancerConfiguration{ | ||
AddressRanges: []v1alpha1.AddressRange{ | ||
{Start: "192.168.1.10", End: "192.168.1.20"}, | ||
}, | ||
}, | ||
}, | ||
expectedErr: nil, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
err := checkIfPrismCentralIPInLoadBalancerIPRange( | ||
tt.pcEndpoint, | ||
tt.serviceLoadBalancerConfiguration, | ||
) | ||
|
||
if tt.expectedErr != nil { | ||
assert.Equal(t, tt.expectedErr.Error(), err.Error()) | ||
} else { | ||
assert.NoError(t, err) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.