Skip to content
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

fix: validates PC IP is outside Load Balancer IP Range #1001

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
manoj-nutanix marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", 0, fmt.Errorf("error parsing Prism Central URL: %w", err)
}
Expand Down
26 changes: 26 additions & 0 deletions pkg/helpers/helpers.go
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
}
116 changes: 116 additions & 0 deletions pkg/helpers/helpers_test.go
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)
}
})
}
}
133 changes: 133 additions & 0 deletions pkg/webhook/cluster/nutanix_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2024 Nutanix. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package cluster

import (
"context"
"errors"
"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 := validatePrismCentralIPNotInLoadBalancerIPRange(
clusterConfig.Nutanix.PrismCentralEndpoint,
clusterConfig.Addons.ServiceLoadBalancer,
); err != nil {
return admission.Denied(err.Error())
}
}

return admission.Allowed("")
}

// validatePrismCentralIPNotInLoadBalancerIPRange checks if the Prism Central IP is not
// in the MetalLB Load Balancer IP range and error out if it is.
func validatePrismCentralIPNotInLoadBalancerIPRange(
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(
"failed to check if Prism Central IP %q is part of MetalLB address range %q-%q: %w",
pcIP,
pool.Start,
pool.End,
err,
)
}
if isIPInRange {
errMsg := fmt.Sprintf(
"Prism Central IP %q must not be part of MetalLB address range %q-%q",
pcIP,
pool.Start,
pool.End,
)
return errors.New(errMsg)
}
}

return nil
}
Loading
Loading