Repository: rancher-sandbox/cluster-api-provider-rke2
Pull request #266 (2024-02-14 16:57:23)

feat: added control plane endpoint registration method
**What this PR does / why we need it**:

This adds a new registration method that will use the **host** from the control plane endpoint set on the **Cluster**. This is aimed at users that will use an external LB in front of the control-plane machines.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #

**Special notes for your reviewer**:

**Checklist**:
<!-- Put an "X" character inside the brackets of each completed task. Some may be optional depending on the PR in which case these can be deleted -->

- [ ] squashed commits into logical changes
- [ ] includes documentation
- [x] adds unit tests
- [ ] adds or updates e2e tests



Patch:
diff --git a/controlplane/api/v1beta1/rke2controlplane_types.go b/controlplane/api/v1beta1/rke2controlplane_types.go
index d2bfc16..4635d68 100644
--- a/controlplane/api/v1beta1/rke2controlplane_types.go
+++ b/controlplane/api/v1beta1/rke2controlplane_types.go
@@ -78,7 +78,7 @@ type RKE2ControlPlaneSpec struct {
 	NodeDrainTimeout *metav1.Duration `json:"nodeDrainTimeout,omitempty"`
 
 	// RegistrationMethod is the method to use for registering nodes into the RKE2 cluster.
-	// +kubebuilder:validation:Enum=internal-first;internal-only-ips;external-only-ips;address
+	// +kubebuilder:validation:Enum=internal-first;internal-only-ips;external-only-ips;address;control-plane-endpoint
 	// +kubebuilder:default=internal-first
 	// +optional
 	RegistrationMethod RegistrationMethod `json:"registrationMethod"`
diff --git a/controlplane/api/v1beta1/types.go b/controlplane/api/v1beta1/types.go
index 50e289e..5860590 100644
--- a/controlplane/api/v1beta1/types.go
+++ b/controlplane/api/v1beta1/types.go
@@ -33,4 +33,7 @@ var (
 	// RegistrationMethodAddress is a registration method where an explicit address supplied at cluster creation
 	// time is used for registration. This is for use in LB or VIP scenarios.
 	RegistrationMethodAddress = RegistrationMethod("address")
+	// RegistrationMethodControlPlaneEndpoint is a registration method where the control plane endpoint from the
+	// Cluster is used for registration.
+	RegistrationMethodControlPlaneEndpoint = RegistrationMethod("control-plane-endpoint")
 )
diff --git a/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml b/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml
index 9d9aa0e..bef2450 100644
--- a/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml
+++ b/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml
@@ -1854,6 +1854,7 @@ spec:
                 - internal-only-ips
                 - external-only-ips
                 - address
+                - control-plane-endpoint
                 type: string
               replicas:
                 description: Replicas is the number of replicas for the Control Plane.
diff --git a/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanetemplates.yaml b/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanetemplates.yaml
index 40c0983..0e86b81 100644
--- a/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanetemplates.yaml
+++ b/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanetemplates.yaml
@@ -763,6 +763,7 @@ spec:
                         - internal-only-ips
                         - external-only-ips
                         - address
+                        - control-plane-endpoint
                         type: string
                       replicas:
                         description: Replicas is the number of replicas for the Control
diff --git a/controlplane/internal/controllers/rke2controlplane_controller.go b/controlplane/internal/controllers/rke2controlplane_controller.go
index c00626e..f351f75 100644
--- a/controlplane/internal/controllers/rke2controlplane_controller.go
+++ b/controlplane/internal/controllers/rke2controlplane_controller.go
@@ -359,7 +359,7 @@ func (r *RKE2ControlPlaneReconciler) updateStatus(ctx context.Context, rcp *cont
 		return fmt.Errorf("getting node registration method: %w", err)
 	}
 
-	validIPAddresses, err := registrationmethod(rcp, availableCPMachines)
+	validIPAddresses, err := registrationmethod(cluster, rcp, availableCPMachines)
 	if err != nil {
 		return fmt.Errorf("getting registration addresses: %w", err)
 	}
diff --git a/pkg/registration/registration.go b/pkg/registration/registration.go
index 852c7f8..6bbfc5e 100644
--- a/pkg/registration/registration.go
+++ b/pkg/registration/registration.go
@@ -28,7 +28,9 @@ import (
 
 // GetRegistrationAddresses is a function type that is used to provide different implementations of
 // getting the addresses just when registering a new node into a cluster.
-type GetRegistrationAddresses func(rcp *controlplanev1.RKE2ControlPlane, cpMachines collections.Machines) ([]string, error)
+type GetRegistrationAddresses func(cluster *clusterv1.Cluster,
+	rcp *controlplanev1.RKE2ControlPlane,
+	cpMachines collections.Machines) ([]string, error)
 
 // NewRegistrationMethod returns the function for the registration addresses based on the passed method name.
 func NewRegistrationMethod(method string) (GetRegistrationAddresses, error) {
@@ -41,13 +43,15 @@ func NewRegistrationMethod(method string) (GetRegistrationAddresses, error) {
 		return registrationMethodWithFilter(filterExternalOnly), nil
 	case "address":
 		return registrationMethodAddress, nil
+	case "control-plane-endpoint":
+		return registrationMethodControlPlaneEndpoint, nil
 	default:
 		return nil, fmt.Errorf("unsupported registration method: %s", method)
 	}
 }
 
 func registrationMethodWithFilter(filter addressFilter) GetRegistrationAddresses {
-	return func(rcp *controlplanev1.RKE2ControlPlane, availableMachines collections.Machines) ([]string, error) {
+	return func(_ *clusterv1.Cluster, rcp *controlplanev1.RKE2ControlPlane, availableMachines collections.Machines) ([]string, error) {
 		validIPAddresses := []string{}
 
 		for _, availableMachine := range availableMachines {
@@ -61,7 +65,7 @@ func registrationMethodWithFilter(filter addressFilter) GetRegistrationAddresses
 	}
 }
 
-func registrationMethodAddress(rcp *controlplanev1.RKE2ControlPlane, _ collections.Machines) ([]string, error) {
+func registrationMethodAddress(_ *clusterv1.Cluster, rcp *controlplanev1.RKE2ControlPlane, _ collections.Machines) ([]string, error) {
 	validIPAddresses := []string{}
 
 	validIPAddresses = append(validIPAddresses, rcp.Spec.RegistrationAddress)
@@ -73,6 +77,25 @@ func registrationMethodAddress(rcp *controlplanev1.RKE2ControlPlane, _ collectio
 	return validIPAddresses, nil
 }
 
+func registrationMethodControlPlaneEndpoint(cluster *clusterv1.Cluster,
+	_ *controlplanev1.RKE2ControlPlane,
+	_ collections.Machines,
+) ([]string, error) {
+	validAddresses := []string{}
+
+	if cluster.Spec.ControlPlaneEndpoint.IsZero() {
+		return nil, errors.New("no control plane endpoint set")
+	}
+
+	if cluster.Spec.ControlPlaneEndpoint.Host == "" {
+		return nil, errors.New("no control plane host set")
+	}
+
+	validAddresses = append(validAddresses, cluster.Spec.ControlPlaneEndpoint.Host)
+
+	return validAddresses, nil
+}
+
 type addressFilter func(machine *clusterv1.Machine) string
 
 func filterInternalFirst(machine *clusterv1.Machine) string {


Test patch:
diff --git a/pkg/registration/registration_test.go b/pkg/registration/registration_test.go
index 84fce3e..62dc7ed 100644
--- a/pkg/registration/registration_test.go
+++ b/pkg/registration/registration_test.go
@@ -49,6 +49,10 @@ func TestNewRegistrationMethod(t *testing.T) {
 			name:        "address",
 			expectError: false,
 		},
+		{
+			name:        "control-plane-endpoint",
+			expectError: false,
+		},
 		{
 			name:        "unknownmethod",
 			expectError: true,
@@ -113,7 +117,7 @@ func TestInternalFirstMethod(t *testing.T) {
 
 			col := collections.FromMachines(tc.machines...)
 
-			actualAddresses, err := regMethod(tc.rcp, col)
+			actualAddresses, err := regMethod(nil, tc.rcp, col)
 			g.Expect(err).NotTo(HaveOccurred())
 
 			g.Expect(actualAddresses).To(HaveExactElements(tc.expectedAddresses))
@@ -165,7 +169,7 @@ func TestInternalOnlyMethod(t *testing.T) {
 
 			col := collections.FromMachines(tc.machines...)
 
-			actualAddresses, err := regMethod(tc.rcp, col)
+			actualAddresses, err := regMethod(nil, tc.rcp, col)
 			g.Expect(err).NotTo(HaveOccurred())
 
 			g.Expect(actualAddresses).To(HaveExactElements(tc.expectedAddresses))
@@ -218,7 +222,7 @@ func TestExternalOnlyMethod(t *testing.T) {
 
 			col := collections.FromMachines(tc.machines...)
 
-			actualAddresses, err := regMethod(tc.rcp, col)
+			actualAddresses, err := regMethod(nil, tc.rcp, col)
 			g.Expect(err).NotTo(HaveOccurred())
 
 			g.Expect(actualAddresses).To(HaveExactElements(tc.expectedAddresses))
@@ -267,7 +271,7 @@ func TestAddressMethod(t *testing.T) {
 
 			col := collections.FromMachines(tc.machines...)
 
-			actualAddresses, err := regMethod(tc.rcp, col)
+			actualAddresses, err := regMethod(nil, tc.rcp, col)
 			g.Expect(err).NotTo(HaveOccurred())
 
 			expectedAddresses := []string{"100.100.100.100"}
@@ -278,6 +282,66 @@ func TestAddressMethod(t *testing.T) {
 	}
 }
 
+func TestControlPlaneMethod(t *testing.T) {
+	testCases := []struct {
+		name            string
+		cluster         *clusterv1.Cluster
+		expectErr       bool
+		expectedAddress string
+	}{
+		{
+			name:            "with host and port",
+			cluster:         createCluster("test1", "server1", 9445),
+			expectErr:       false,
+			expectedAddress: "server1",
+		},
+		{
+			name:            "with host and no port",
+			cluster:         createCluster("test1", "server1", 0),
+			expectErr:       false,
+			expectedAddress: "server1",
+		},
+		{
+			name:            "with port and no host",
+			cluster:         createCluster("test1", "", 9445),
+			expectErr:       true,
+			expectedAddress: "",
+		},
+		{
+			name:            "with no port and no host",
+			cluster:         createCluster("test1", "", 0),
+			expectErr:       true,
+			expectedAddress: "",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			regMethod, err := registration.NewRegistrationMethod(string(controlplanev1.RegistrationMethodControlPlaneEndpoint))
+			g.Expect(err).NotTo(HaveOccurred())
+			g.Expect(regMethod).NotTo(BeNil())
+
+			machines := []*clusterv1.Machine{}
+			rcp := createControlPlane(string(controlplanev1.RegistrationMethodControlPlaneEndpoint), "")
+			col := collections.FromMachines(machines...)
+
+			actualAddresses, err := regMethod(tc.cluster, rcp, col)
+			if tc.expectErr {
+				g.Expect(err).To(HaveOccurred())
+				return
+			}
+
+			g.Expect(err).NotTo(HaveOccurred())
+
+			expectedAddresses := []string{tc.expectedAddress}
+			g.Expect(actualAddresses).To(HaveExactElements(expectedAddresses))
+
+		})
+	}
+}
+
 func createControlPlane(registrationMethod, registrationAddress string) *controlplanev1.RKE2ControlPlane {
 	return &controlplanev1.RKE2ControlPlane{
 		ObjectMeta: v1.ObjectMeta{
@@ -292,6 +356,29 @@ func createControlPlane(registrationMethod, registrationAddress string) *control
 	}
 }
 
+func createCluster(name string, cpHost string, cpPort int) *clusterv1.Cluster {
+	cluster := &clusterv1.Cluster{
+		ObjectMeta: v1.ObjectMeta{
+			Name:      name,
+			Namespace: "default",
+		},
+		Spec:   clusterv1.ClusterSpec{},
+		Status: clusterv1.ClusterStatus{},
+	}
+
+	if cpHost == "" && cpPort == 0 {
+		return cluster
+	}
+
+	cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{
+		Host: cpHost,
+		Port: int32(cpPort),
+	}
+
+	return cluster
+
+}
+
 func createMachine(name string, internalIPs []string, externalIPs []string) *clusterv1.Machine {
 	machine := &clusterv1.Machine{
 		ObjectMeta: v1.ObjectMeta{