From 4651c5b41a4c9aa3a71a06eec8dbddd415d511d5 Mon Sep 17 00:00:00 2001 From: Manoj Surudwad Date: Mon, 9 Dec 2024 17:05:26 +0530 Subject: [PATCH] fix: validates CONTROL_PLANE_ENDPOINT_IP and NUTANIX_ENDPOINT are distinct --- pkg/webhook/cluster/nutanix_validator.go | 123 ++++++++++++++++++ .../cluster/{vaiidator.go => validator.go} | 1 + 2 files changed, 124 insertions(+) create mode 100644 pkg/webhook/cluster/nutanix_validator.go rename pkg/webhook/cluster/{vaiidator.go => validator.go} (89%) diff --git a/pkg/webhook/cluster/nutanix_validator.go b/pkg/webhook/cluster/nutanix_validator.go new file mode 100644 index 000000000..7eee86997 --- /dev/null +++ b/pkg/webhook/cluster/nutanix_validator.go @@ -0,0 +1,123 @@ +// Copyright 2024 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package cluster + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + + 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" +) + +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 { + // Check if Prism Central and Control Plane IP are same. + if err := checkIfPrismCentralAndControlPlaneIPSame( + clusterConfig.Nutanix.PrismCentralEndpoint.URL, + clusterConfig.Nutanix.ControlPlaneEndpoint.Host, + ); err != nil { + return admission.Denied(err.Error()) + } + } + + return admission.Allowed("") +} + +// checkIfPrismCentralAndControlPlaneIPSame checks if Prism Central and Control Plane IP are same. +// It compares strictly IP addresses(no FQDN) and doesn't involve any network calls. +// This is a temporary check until we have a better way to handle this by reserving IPs +// using IPAM provider. +func checkIfPrismCentralAndControlPlaneIPSame( + pcRawURL string, + controlPlaneEndpointHost string, +) error { + controlPlaneEndpointIP := net.ParseIP(controlPlaneEndpointHost) + if controlPlaneEndpointIP == nil { + // controlPlaneEndpointIP is strictly accepted as an IP address from user so + // if it is not an IP address, it is invalid. + return fmt.Errorf("invalid Nutanix control plane endpoint IP %q", + controlPlaneEndpointHost) + } + + pcURL, err := url.ParseRequestURI(pcRawURL) + if err != nil { + return fmt.Errorf("failed to parse Prism Central URL %q: %w", + pcURL, + err) + } + + pcHost, _, err := net.SplitHostPort(pcURL.Host) + if err != nil { + return fmt.Errorf("failed to parse Prism Central host %q: %w", + pcURL.Host, + err) + } + + pcIP := net.ParseIP(pcHost) + // PC URL can contain IP/FQDN, so compare only if PC is an IP address. + if pcIP != nil && pcIP.Equal(controlPlaneEndpointIP) { + return fmt.Errorf("prism central and control plane endpoint cannot have the same IP %q", + pcIP) + } + + return nil +} diff --git a/pkg/webhook/cluster/vaiidator.go b/pkg/webhook/cluster/validator.go similarity index 89% rename from pkg/webhook/cluster/vaiidator.go rename to pkg/webhook/cluster/validator.go index 3eae0a7dc..adbddae27 100644 --- a/pkg/webhook/cluster/vaiidator.go +++ b/pkg/webhook/cluster/validator.go @@ -11,5 +11,6 @@ import ( func NewValidator(client ctrlclient.Client, decoder admission.Decoder) admission.Handler { return admission.MultiValidatingHandler( NewClusterUUIDLabeler(client, decoder).Validator(), + NewNutanixValidator(client, decoder).Validator(), ) }