Skip to content

Commit 7d5bca2

Browse files
authored
Merge pull request #147 from richardcase/136_registration_method
feat: support different registration methods
2 parents 96b79e7 + 6c07719 commit 7d5bca2

12 files changed

Lines changed: 663 additions & 56 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ GOLANGCI_LINT_VER := v1.49.0
9999
GOLANGCI_LINT_BIN := golangci-lint
100100
GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/$(GOLANGCI_LINT_BIN))
101101

102-
GINKGO_VER := v2.9.1
102+
GINKGO_VER := v2.9.4
103103
GINKGO_BIN := ginkgo
104104
GINKGO := $(abspath $(TOOLS_BIN_DIR)/$(GINKGO_BIN)-$(GINKGO_VER))
105105
GINKGO_PKG := github.com/onsi/ginkgo/v2/ginkgo

bootstrap/api/v1alpha1/rke2config_webhook.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ import (
2323
apierrors "k8s.io/apimachinery/pkg/api/errors"
2424
"k8s.io/apimachinery/pkg/runtime"
2525
"k8s.io/apimachinery/pkg/util/validation/field"
26+
"k8s.io/klog/v2"
2627
ctrl "sigs.k8s.io/controller-runtime"
28+
logf "sigs.k8s.io/controller-runtime/pkg/log"
2729
"sigs.k8s.io/controller-runtime/pkg/webhook"
2830
)
2931

30-
var cannotUseWithIgnition = fmt.Sprintf("not supported when spec.format is set to %q", Ignition)
32+
var (
33+
cannotUseWithIgnition = fmt.Sprintf("not supported when spec.format is set to %q", Ignition)
34+
rke2configlog = logf.Log.WithName("rke2config-resource")
35+
)
3136

3237
// SetupWebhookWithManager sets up and registers the webhook with the manager.
3338
func (r *RKE2Config) SetupWebhookWithManager(mgr ctrl.Manager) error {
@@ -58,12 +63,32 @@ var _ webhook.Validator = &RKE2Config{}
5863

5964
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
6065
func (r *RKE2Config) ValidateCreate() error {
61-
return ValidateRKE2ConfigSpec(r.Name, &r.Spec)
66+
rke2configlog.Info("RKE2Config validate create", "rke2config", klog.KObj(r))
67+
68+
var allErrs field.ErrorList
69+
70+
allErrs = append(allErrs, ValidateRKE2ConfigSpec(r.Name, &r.Spec)...)
71+
72+
if len(allErrs) == 0 {
73+
return nil
74+
}
75+
76+
return apierrors.NewInvalid(GroupVersion.WithKind("RKE2Config").GroupKind(), r.Name, allErrs)
6277
}
6378

6479
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
6580
func (r *RKE2Config) ValidateUpdate(_ runtime.Object) error {
66-
return ValidateRKE2ConfigSpec(r.Name, &r.Spec)
81+
rke2configlog.Info("RKE2Config validate update", "rke2config", klog.KObj(r))
82+
83+
var allErrs field.ErrorList
84+
85+
allErrs = append(allErrs, ValidateRKE2ConfigSpec(r.Name, &r.Spec)...)
86+
87+
if len(allErrs) == 0 {
88+
return nil
89+
}
90+
91+
return apierrors.NewInvalid(GroupVersion.WithKind("RKE2Config").GroupKind(), r.Name, allErrs)
6792
}
6893

6994
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
@@ -72,14 +97,14 @@ func (r *RKE2Config) ValidateDelete() error {
7297
}
7398

7499
// ValidateRKE2ConfigSpec validates the RKE2ConfigSpec.
75-
func ValidateRKE2ConfigSpec(name string, spec *RKE2ConfigSpec) error {
100+
func ValidateRKE2ConfigSpec(name string, spec *RKE2ConfigSpec) field.ErrorList {
76101
allErrs := spec.validate(field.NewPath("spec"))
77102

78103
if len(allErrs) == 0 {
79104
return nil
80105
}
81106

82-
return apierrors.NewInvalid(GroupVersion.WithKind("RKE2Config").GroupKind(), name, allErrs)
107+
return allErrs
83108
}
84109

85110
func (s *RKE2ConfigSpec) validate(pathPrefix *field.Path) field.ErrorList {

controlplane/api/v1alpha1/rke2controlplane_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ type RKE2ControlPlaneSpec struct {
6060
// NOTE: NodeDrainTimeout is different from `kubectl drain --timeout`
6161
// +optional
6262
NodeDrainTimeout *metav1.Duration `json:"nodeDrainTimeout,omitempty"`
63+
64+
// RegistrationMethod is the method to use for registering nodes into the RKE2 cluster.
65+
// +kubebuilder:validation:Enum=internal-first;internal-only-ips;external-only-ips;address
66+
// +kubebuilder:default=internal-first
67+
// +optional
68+
RegistrationMethod RegistrationMethod `json:"registrationMethod"`
69+
70+
// RegistrationAddress is an explicit address to use when registering a node. This is required if
71+
// the registration type is "address". Its for scenarios where a load-balancer or VIP is used.
72+
// +optional
73+
RegistrationAddress string `json:"registrationAddress,omitempty"`
6374
}
6475

6576
// RKE2ServerConfig specifies configuration for the agent nodes.

controlplane/api/v1alpha1/rke2controlplane_webhook.go

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ limitations under the License.
1717
package v1alpha1
1818

1919
import (
20+
"errors"
21+
2022
apierrors "k8s.io/apimachinery/pkg/api/errors"
2123
"k8s.io/apimachinery/pkg/runtime"
2224
"k8s.io/apimachinery/pkg/util/validation/field"
25+
"k8s.io/klog/v2"
2326
ctrl "sigs.k8s.io/controller-runtime"
2427
logf "sigs.k8s.io/controller-runtime/pkg/log"
2528
"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -44,6 +47,16 @@ var _ webhook.Defaulter = &RKE2ControlPlane{}
4447
// Default implements webhook.Defaulter so a webhook will be registered for the type.
4548
func (r *RKE2ControlPlane) Default() {
4649
bootstrapv1.DefaultRKE2ConfigSpec(&r.Spec.RKE2ConfigSpec)
50+
51+
if r.Spec.RegistrationMethod == RegistrationMethodAddress {
52+
if r.Spec.ServerConfig.AdvertiseAddress == "" {
53+
rke2controlplanelog.Info("setting advertise address from registration address",
54+
"rke2-control-plane", klog.KObj(r),
55+
"address", r.Spec.RegistrationAddress)
56+
57+
r.Spec.ServerConfig.AdvertiseAddress = r.Spec.RegistrationAddress
58+
}
59+
}
4760
}
4861

4962
//+kubebuilder:webhook:path=/validate-controlplane-cluster-x-k8s-io-v1alpha1-rke2controlplane,mutating=false,failurePolicy=fail,sideEffects=None,groups=controlplane.cluster.x-k8s.io,resources=rke2controlplanes,verbs=create;update,versions=v1alpha1,name=vrke2controlplane.kb.io,admissionReviewVersions=v1
@@ -52,20 +65,46 @@ var _ webhook.Validator = &RKE2ControlPlane{}
5265

5366
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
5467
func (r *RKE2ControlPlane) ValidateCreate() error {
55-
if bootstrapv1.ValidateRKE2ConfigSpec(r.Name, &r.Spec.RKE2ConfigSpec) != nil {
56-
return bootstrapv1.ValidateRKE2ConfigSpec(r.Name, &r.Spec.RKE2ConfigSpec)
68+
rke2controlplanelog.Info("RKE2ControlPlane validate create", "control-plane", klog.KObj(r))
69+
70+
var allErrs field.ErrorList
71+
72+
allErrs = append(allErrs, bootstrapv1.ValidateRKE2ConfigSpec(r.Name, &r.Spec.RKE2ConfigSpec)...)
73+
allErrs = append(allErrs, r.validateCNI()...)
74+
allErrs = append(allErrs, r.validateRegistrationMethod()...)
75+
76+
if len(allErrs) == 0 {
77+
return nil
5778
}
5879

59-
return ValidateRKE2ControlPlaneSpec(r.Name, &r.Spec)
80+
return apierrors.NewInvalid(GroupVersion.WithKind("RKE2ControlPlane").GroupKind(), r.Name, allErrs)
6081
}
6182

6283
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
6384
func (r *RKE2ControlPlane) ValidateUpdate(old runtime.Object) error {
64-
if bootstrapv1.ValidateRKE2ConfigSpec(r.Name, &r.Spec.RKE2ConfigSpec) != nil {
65-
return bootstrapv1.ValidateRKE2ConfigSpec(r.Name, &r.Spec.RKE2ConfigSpec)
85+
oldControlplane, ok := old.(*RKE2ControlPlane)
86+
if !ok {
87+
return apierrors.NewInvalid(GroupVersion.WithKind("RKE2ControlPlane").GroupKind(), r.Name, field.ErrorList{
88+
field.InternalError(nil, errors.New("failed to convert old RKE2ControlPlane to object")),
89+
})
6690
}
6791

68-
return ValidateRKE2ControlPlaneSpec(r.Name, &r.Spec)
92+
var allErrs field.ErrorList
93+
94+
allErrs = append(allErrs, bootstrapv1.ValidateRKE2ConfigSpec(r.Name, &r.Spec.RKE2ConfigSpec)...)
95+
allErrs = append(allErrs, r.validateCNI()...)
96+
97+
if r.Spec.RegistrationMethod != oldControlplane.Spec.RegistrationMethod {
98+
allErrs = append(allErrs,
99+
field.Invalid(field.NewPath("spec", "registrationMethod"), r.Spec.RegistrationMethod, "field is immutable"),
100+
)
101+
}
102+
103+
if len(allErrs) == 0 {
104+
return nil
105+
}
106+
107+
return apierrors.NewInvalid(GroupVersion.WithKind("RKE2ControlPlane").GroupKind(), r.Name, allErrs)
69108
}
70109

71110
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
@@ -75,24 +114,27 @@ func (r *RKE2ControlPlane) ValidateDelete() error {
75114
return nil
76115
}
77116

78-
// ValidateRKE2ControlPlaneSpec validates the RKE2ControlPlaneSpec Object.
79-
func ValidateRKE2ControlPlaneSpec(name string, spec *RKE2ControlPlaneSpec) error {
80-
allErrs := spec.validate()
81-
if len(allErrs) == 0 {
82-
return nil
117+
func (r *RKE2ControlPlane) validateCNI() field.ErrorList {
118+
var allErrs field.ErrorList
119+
120+
if r.Spec.ServerConfig.CNIMultusEnable && r.Spec.ServerConfig.CNI == "" {
121+
allErrs = append(allErrs,
122+
field.Invalid(field.NewPath("spec", "serverConfig", "cni"),
123+
r.Spec.ServerConfig.CNI, "must be specified when cniMultusEnable is true"))
83124
}
84125

85-
return apierrors.NewInvalid(GroupVersion.WithKind("RKE2ControlPlane").GroupKind(), name, allErrs)
126+
return allErrs
86127
}
87128

88-
// validate validates the RKE2ControlPlaneSpec Object.
89-
func (s *RKE2ControlPlaneSpec) validate() field.ErrorList {
129+
func (r *RKE2ControlPlane) validateRegistrationMethod() field.ErrorList {
90130
var allErrs field.ErrorList
91131

92-
if s.ServerConfig.CNIMultusEnable && s.ServerConfig.CNI == "" {
93-
allErrs = append(allErrs,
94-
field.Invalid(field.NewPath("spec", "serverConfig", "cni"),
95-
s.ServerConfig.CNI, "must be specified when cniMultusEnable is true"))
132+
if r.Spec.RegistrationMethod == RegistrationMethodAddress {
133+
if r.Spec.RegistrationAddress == "" {
134+
allErrs = append(allErrs,
135+
field.Invalid(field.NewPath("spec.registrationAddress"),
136+
r.Spec.RegistrationAddress, "registrationAddress must be supplied when using registration method 'address'"))
137+
}
96138
}
97139

98140
return allErrs

controlplane/api/v1alpha1/types.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2023 SUSE.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
// RegistrationMethod defines the methods to use for registering a new node in a cluster.
20+
type RegistrationMethod string
21+
22+
var (
23+
// RegistrationMethodFavourInternalIPs is a registration method where the IP address of the control plane
24+
// machines are used for registration. For each machine it will check if there is an internal IP address
25+
// and will use that. If there is no internal IP address it will use the external IP address if there is one.
26+
RegistrationMethodFavourInternalIPs = RegistrationMethod("internal-first")
27+
// RegistrationMethodInternalIPs is a registration method where the internal IP address of the control plane
28+
// machines are used for registration.
29+
RegistrationMethodInternalIPs = RegistrationMethod("internal-only-ips")
30+
// RegistrationMethodExternalIPs is a registration method where the external IP address of the control plane
31+
// machines are used for registration.
32+
RegistrationMethodExternalIPs = RegistrationMethod("external-only-ips")
33+
// RegistrationMethodAddress is a registration method where an explicit address supplied at cluster creation
34+
// time is used for registration. This is for use in LB or VIP scenarios.
35+
RegistrationMethodAddress = RegistrationMethod("address")
36+
)

controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,21 @@ spec:
567567
description: Mirrors are namespace to mirror mapping for all namespaces.
568568
type: object
569569
type: object
570+
registrationAddress:
571+
description: RegistrationAddress is an explicit address to use when
572+
registering a node. This is required if the registration type is
573+
"address". Its for scenarios where a load-balancer or VIP is used.
574+
type: string
575+
registrationMethod:
576+
default: internal-first
577+
description: RegistrationMethod is the method to use for registering
578+
nodes into the RKE2 cluster.
579+
enum:
580+
- internal-first
581+
- internal-only-ips
582+
- external-only-ips
583+
- address
584+
type: string
570585
replicas:
571586
description: Replicas is the number of replicas for the Control Plane.
572587
format: int32

controlplane/internal/controllers/rke2controlplane_controller.go

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747

4848
controlplanev1 "github.com/rancher-sandbox/cluster-api-provider-rke2/controlplane/api/v1alpha1"
4949
"github.com/rancher-sandbox/cluster-api-provider-rke2/pkg/kubeconfig"
50+
"github.com/rancher-sandbox/cluster-api-provider-rke2/pkg/registration"
5051
"github.com/rancher-sandbox/cluster-api-provider-rke2/pkg/rke2"
5152
"github.com/rancher-sandbox/cluster-api-provider-rke2/pkg/secret"
5253
)
@@ -352,15 +353,14 @@ func (r *RKE2ControlPlaneReconciler) updateStatus(ctx context.Context, rcp *cont
352353

353354
availableCPMachines := readyMachines
354355

355-
validIPAddresses := []string{}
356-
357-
for _, machine := range availableCPMachines {
358-
ipAddress, err := getIPAddress(*machine)
359-
if err != nil {
360-
break
361-
}
356+
registrationmethod, err := registration.NewRegistrationMethod(string(rcp.Spec.RegistrationMethod))
357+
if err != nil {
358+
return fmt.Errorf("getting node registration method: %w", err)
359+
}
362360

363-
validIPAddresses = append(validIPAddresses, ipAddress)
361+
validIPAddresses, err := registrationmethod(rcp, availableCPMachines)
362+
if err != nil {
363+
return fmt.Errorf("getting registration addresses: %w", err)
364364
}
365365

366366
rcp.Status.AvailableServerIPs = validIPAddresses
@@ -751,24 +751,3 @@ func (r *RKE2ControlPlaneReconciler) ClusterToRKE2ControlPlane(o client.Object)
751751

752752
return nil
753753
}
754-
755-
func getIPAddress(machine clusterv1.Machine) (ip string, err error) {
756-
for _, address := range machine.Status.Addresses {
757-
switch address.Type {
758-
case clusterv1.MachineInternalIP:
759-
if address.Address != "" {
760-
return address.Address, nil
761-
}
762-
case clusterv1.MachineExternalIP:
763-
if address.Address != "" {
764-
ip = address.Address
765-
}
766-
}
767-
}
768-
769-
if ip == "" {
770-
err = fmt.Errorf("no IP Address found for machine: %s", machine.Name)
771-
}
772-
773-
return
774-
}

docs/registration-methods.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Node Registration Methods
2+
3+
The provider supports multiple methods for registering a new node into the cluster.
4+
5+
## Usage
6+
7+
The method to use is specified on the **RKEControlPlane** within the **spec**. If no method is supplied then the default method of **internal-first** will be used.
8+
9+
> You cannot change the registration method after creation.
10+
11+
An example of using a different method:
12+
13+
```yaml
14+
apiVersion: controlplane.cluster.x-k8s.io/v1alpha1
15+
kind: RKE2ControlPlane
16+
metadata:
17+
name: test1-control-plane
18+
namespace: default
19+
spec:
20+
agentConfig:
21+
version: v1.26.4+rke2r1
22+
infrastructureRef:
23+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
24+
kind: DockerMachineTemplate
25+
name: controlplane
26+
nodeDrainTimeout: 2m
27+
replicas: 3
28+
serverConfig:
29+
cni: calico
30+
registrationMethod: "address"
31+
registrationAddress: "172.19.0.3"
32+
```
33+
34+
## Registration Methods
35+
36+
### internal-first
37+
38+
For each CAPI `Machine` that is used for the control plane, we take the **internal** ip address from `Machine.status.addresses` if it exists. If there is no **internal** ip for a machine then we will use an **external** address instead. For the ip address found for a machine then we add it to `RKEControlPlane.status.availableServerIPs`.
39+
40+
The first IP address listed in `RKEControlPlane.status.availableServerIPs` is then used for the join.
41+
42+
### internal-only-ips
43+
44+
For each CAPI `Machine` that is used for the control plane, we take the **internal** ip address from `Machine.status.addresses` if it exists and then we add it to `RKEControlPlane.status.availableServerIPs`.
45+
46+
The first IP address listed in `RKEControlPlane.status.availableServerIPs` is then used for the join.
47+
48+
### external-only-ips
49+
50+
For each CAPI `Machine` that is used for the control plane, we take the **external** ip address from `Machine.status.addresses` if it exists and then we add it to `RKEControlPlane.status.availableServerIPs`.
51+
52+
The first IP address listed in `RKEControlPlane.status.availableServerIPs` is then used for the join.
53+
54+
### address
55+
56+
For this method you must supply an address in the control plane spec (i.e. `RKE2ControlPlane.spec.registrationAddress`). This address is then used for the join.
57+
58+
With this method its expected that you have a load balancer / VIP solution sitting in front of all the control plane machines and all the join requests will be routed via this.

0 commit comments

Comments
 (0)