diff --git a/api/v1alpha3/conversion.go b/api/v1alpha3/conversion.go index 4b3c5dd99e9f..0897f5bbdec2 100644 --- a/api/v1alpha3/conversion.go +++ b/api/v1alpha3/conversion.go @@ -99,6 +99,7 @@ func (src *Machine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.NodeDeletionTimeout = restored.Spec.NodeDeletionTimeout dst.Status.NodeInfo = restored.Status.NodeInfo + dst.Status.CertificatesExpiryDate = restored.Status.CertificatesExpiryDate return nil } diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index 9661e6cc3f18..57e91c8789de 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -1248,6 +1248,7 @@ func autoConvert_v1beta1_MachineStatus_To_v1alpha3_MachineStatus(in *v1beta1.Mac out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage)) out.Addresses = *(*MachineAddresses)(unsafe.Pointer(&in.Addresses)) out.Phase = in.Phase + // WARNING: in.CertificatesExpiryDate requires manual conversion: does not exist in peer-type out.BootstrapReady = in.BootstrapReady out.InfrastructureReady = in.InfrastructureReady out.ObservedGeneration = in.ObservedGeneration diff --git a/api/v1alpha4/conversion.go b/api/v1alpha4/conversion.go index ed788a78e183..72642d04f9e7 100644 --- a/api/v1alpha4/conversion.go +++ b/api/v1alpha4/conversion.go @@ -159,6 +159,7 @@ func (src *Machine) ConvertTo(dstRaw conversion.Hub) error { } dst.Spec.NodeDeletionTimeout = restored.Spec.NodeDeletionTimeout + dst.Status.CertificatesExpiryDate = restored.Status.CertificatesExpiryDate return nil } @@ -333,3 +334,8 @@ func Convert_v1beta1_ControlPlaneTopology_To_v1alpha4_ControlPlaneTopology(in *c // controlPlaneTopology.nodeDrainTimeout has been added with v1beta1. return autoConvert_v1beta1_ControlPlaneTopology_To_v1alpha4_ControlPlaneTopology(in, out, s) } + +func Convert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(in *clusterv1.MachineStatus, out *MachineStatus, s apiconversion.Scope) error { + // MachineStatus.CertificatesExpiryDate has been added in v1beta1. + return autoConvert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(in, out, s) +} diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index b065ce6bb082..10e1913232f5 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -370,11 +370,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.MachineStatus)(nil), (*MachineStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(a.(*v1beta1.MachineStatus), b.(*MachineStatus), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*MachineTemplateSpec)(nil), (*v1beta1.MachineTemplateSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_MachineTemplateSpec_To_v1beta1_MachineTemplateSpec(a.(*MachineTemplateSpec), b.(*v1beta1.MachineTemplateSpec), scope) }); err != nil { @@ -475,6 +470,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta1.MachineStatus)(nil), (*MachineStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(a.(*v1beta1.MachineStatus), b.(*MachineStatus), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta1.Topology)(nil), (*Topology)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_Topology_To_v1alpha4_Topology(a.(*v1beta1.Topology), b.(*Topology), scope) }); err != nil { @@ -1630,6 +1630,7 @@ func autoConvert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(in *v1beta1.Mac out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage)) out.Addresses = *(*MachineAddresses)(unsafe.Pointer(&in.Addresses)) out.Phase = in.Phase + // WARNING: in.CertificatesExpiryDate requires manual conversion: does not exist in peer-type out.BootstrapReady = in.BootstrapReady out.InfrastructureReady = in.InfrastructureReady out.ObservedGeneration = in.ObservedGeneration @@ -1637,11 +1638,6 @@ func autoConvert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(in *v1beta1.Mac return nil } -// Convert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus is an autogenerated conversion function. -func Convert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(in *v1beta1.MachineStatus, out *MachineStatus, s conversion.Scope) error { - return autoConvert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(in, out, s) -} - func autoConvert_v1alpha4_MachineTemplateSpec_To_v1beta1_MachineTemplateSpec(in *MachineTemplateSpec, out *v1beta1.MachineTemplateSpec, s conversion.Scope) error { if err := Convert_v1alpha4_ObjectMeta_To_v1beta1_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { return err diff --git a/api/v1beta1/machine_types.go b/api/v1beta1/machine_types.go index 073610fa03bf..1c7ab204c348 100644 --- a/api/v1beta1/machine_types.go +++ b/api/v1beta1/machine_types.go @@ -50,6 +50,12 @@ const ( // to pause reconciliation of deletion. These hooks will prevent removal of // an instance from an infrastructure provider until all are removed. PreTerminateDeleteHookAnnotationPrefix = "pre-terminate.delete.hook.machine.cluster.x-k8s.io" + + // MachineCertificatesExpiryDateAnnotation annotation specifies the expiry date of the machine certificates in RFC3339 format. + // This annotation can be used on control plane machines to trigger rollout before certificates expire. + // This annotation can be set on BootstrapConfig or Machine objects. The value set on the Machine object takes precedence. + // This annotation can only be used on Control Plane Machines. + MachineCertificatesExpiryDateAnnotation = "machine.cluster.x-k8s.io/certificates-expiry" ) // ANCHOR: MachineSpec @@ -171,6 +177,11 @@ type MachineStatus struct { // +optional Phase string `json:"phase,omitempty"` + // CertificatesExpiryDate is the expiry date of the machine certificates. + // This value is only set for control plane machines. + // +optional + CertificatesExpiryDate *metav1.Time `json:"certificatesExpiryDate,omitempty"` + // BootstrapReady is the state of the bootstrap provider. // +optional BootstrapReady bool `json:"bootstrapReady"` diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 2c7ce3814dbd..a754d7e349ec 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1427,6 +1427,10 @@ func (in *MachineStatus) DeepCopyInto(out *MachineStatus) { *out = make(MachineAddresses, len(*in)) copy(*out, *in) } + if in.CertificatesExpiryDate != nil { + in, out := &in.CertificatesExpiryDate, &out.CertificatesExpiryDate + *out = (*in).DeepCopy() + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make(Conditions, len(*in)) diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index f4fb3053952a..decd991b955d 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -2481,6 +2481,12 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_MachineStatus(ref common.Reference Format: "", }, }, + "certificatesExpiryDate": { + SchemaProps: spec.SchemaProps{ + Description: "CertificatesExpiryDate is the expiry date of the machine certificates. This value is only set for control plane machines.", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, "bootstrapReady": { SchemaProps: spec.SchemaProps{ Description: "BootstrapReady is the state of the bootstrap provider.", diff --git a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go index 608fd093ad90..a66a9776e35e 100644 --- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go +++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go @@ -66,8 +66,15 @@ const ( const ( // DefaultTokenTTL is the default TTL used for tokens. DefaultTokenTTL = 15 * time.Minute + + // This hard-coded duration matches the hard-coded value used by kubeadm certificate generation. + certificateExpiryDuration = 365 * 24 * time.Hour ) +// now returns the current time. +// This is defined as a variable so that it can be overridden in unit tests. +var now = time.Now + // InitLocker is a lock that is used around kubeadm init. type InitLocker interface { Lock(ctx context.Context, cluster *clusterv1.Cluster, machine *clusterv1.Machine) bool @@ -447,6 +454,7 @@ func (r *KubeadmConfigReconciler) handleClusterNotInitialized(ctx context.Contex conditions.MarkFalse(scope.Config, bootstrapv1.CertificatesAvailableCondition, bootstrapv1.CertificatesGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) return ctrl.Result{}, err } + conditions.MarkTrue(scope.Config, bootstrapv1.CertificatesAvailableCondition) verbosityFlag := "" @@ -503,6 +511,10 @@ func (r *KubeadmConfigReconciler) handleClusterNotInitialized(ctx context.Contex return ctrl.Result{}, err } + // Update the certificate expiration time in the config. + // This annotation will be used by KCP to trigger control plane machines rollout before the certificate generated on the machine are going to expire. + r.addCertificateExpiryAnnotation(scope.Config) + return ctrl.Result{}, nil } @@ -628,6 +640,7 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S conditions.MarkFalse(scope.Config, bootstrapv1.CertificatesAvailableCondition, bootstrapv1.CertificatesCorruptedReason, clusterv1.ConditionSeverityError, err.Error()) return ctrl.Result{}, err } + conditions.MarkTrue(scope.Config, bootstrapv1.CertificatesAvailableCondition) // Ensure that joinConfiguration.Discovery is properly set for joining node on the current cluster. @@ -703,6 +716,9 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S return ctrl.Result{}, err } + // Update the certificate expiration time in the config. + r.addCertificateExpiryAnnotation(scope.Config) + return ctrl.Result{}, nil } @@ -1020,3 +1036,19 @@ func (r *KubeadmConfigReconciler) storeBootstrapData(ctx context.Context, scope conditions.MarkTrue(scope.Config, bootstrapv1.DataSecretAvailableCondition) return nil } + +// addCertificateExpiryAnnotation sets the certificate expiration time as an +// annotation on KubeadmConfig, if it doesn't exist already. +// NOTE: the certificate expiry date stored in the annotation will be slightly different from the one +// actually used in the certificates - that depends on the exact time kubeadm runs on the machine-, +// but this approximation is acceptable given that it happens before the actual expiration date. +func (r *KubeadmConfigReconciler) addCertificateExpiryAnnotation(config *bootstrapv1.KubeadmConfig) { + annotations := config.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + if _, ok := annotations[clusterv1.MachineCertificatesExpiryDateAnnotation]; !ok { + annotations[clusterv1.MachineCertificatesExpiryDateAnnotation] = now().Add(certificateExpiryDuration).Format(time.RFC3339) + config.SetAnnotations(annotations) + } +} diff --git a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go index f9d08ed2f8db..b98c6b3cc67b 100644 --- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go +++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go @@ -2069,6 +2069,48 @@ func TestKubeadmConfigReconciler_ResolveUsers(t *testing.T) { } } +func TestKubeadmConfigReconciler_ReconcileCertificateExpiryTime(t *testing.T) { + fakeNow, _ := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z") + now = func() time.Time { + return fakeNow + } + oneYearFromNow := "2023-01-01T00:00:00Z" + time2 := "2023-10-01T00:00:00Z" + + tests := []struct { + name string + cfg *bootstrapv1.KubeadmConfig + wantTime string + }{ + { + name: "set the expiry time to one year from now if the expiry time is not set", + cfg: &bootstrapv1.KubeadmConfig{}, + wantTime: oneYearFromNow, + }, + { + name: "do not change the expiry time if it is already set", + cfg: &bootstrapv1.KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + clusterv1.MachineCertificatesExpiryDateAnnotation: time2, + }, + }, + }, + wantTime: time2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + k := &KubeadmConfigReconciler{} + k.addCertificateExpiryAnnotation(tt.cfg) + annotations := tt.cfg.GetAnnotations() + g.Expect(annotations[clusterv1.MachineCertificatesExpiryDateAnnotation]).To(Equal(tt.wantTime)) + }) + } +} + // test utils. // newWorkerMachineForCluster returns a Machine with the passed Cluster's information and a pre-configured name. diff --git a/config/crd/bases/cluster.x-k8s.io_machines.yaml b/config/crd/bases/cluster.x-k8s.io_machines.yaml index 122b6c69bf89..f5b4186d7649 100644 --- a/config/crd/bases/cluster.x-k8s.io_machines.yaml +++ b/config/crd/bases/cluster.x-k8s.io_machines.yaml @@ -956,6 +956,11 @@ spec: bootstrapReady: description: BootstrapReady is the state of the bootstrap provider. type: boolean + certificatesExpiryDate: + description: CertificatesExpiryDate is the expiry date of the machine + certificates. This value is only set for control plane machines. + format: date-time + type: string conditions: description: Conditions defines current service state of the Machine. items: diff --git a/controlplane/kubeadm/api/v1alpha3/conversion.go b/controlplane/kubeadm/api/v1alpha3/conversion.go index 1b8532b48cf8..96e2d65510b2 100644 --- a/controlplane/kubeadm/api/v1alpha3/conversion.go +++ b/controlplane/kubeadm/api/v1alpha3/conversion.go @@ -82,6 +82,8 @@ func (src *KubeadmControlPlane) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.KubeadmConfigSpec.JoinConfiguration.SkipPhases = restored.Spec.KubeadmConfigSpec.JoinConfiguration.SkipPhases } + dst.Spec.RolloutBefore = restored.Spec.RolloutBefore + return nil } diff --git a/controlplane/kubeadm/api/v1alpha3/zz_generated.conversion.go b/controlplane/kubeadm/api/v1alpha3/zz_generated.conversion.go index 165ca9c01d24..94b5206584e7 100644 --- a/controlplane/kubeadm/api/v1alpha3/zz_generated.conversion.go +++ b/controlplane/kubeadm/api/v1alpha3/zz_generated.conversion.go @@ -198,6 +198,7 @@ func autoConvert_v1beta1_KubeadmControlPlaneSpec_To_v1alpha3_KubeadmControlPlane if err := apiv1alpha3.Convert_v1beta1_KubeadmConfigSpec_To_v1alpha3_KubeadmConfigSpec(&in.KubeadmConfigSpec, &out.KubeadmConfigSpec, s); err != nil { return err } + // WARNING: in.RolloutBefore requires manual conversion: does not exist in peer-type // WARNING: in.RolloutAfter requires manual conversion: does not exist in peer-type out.RolloutStrategy = (*RolloutStrategy)(unsafe.Pointer(in.RolloutStrategy)) return nil diff --git a/controlplane/kubeadm/api/v1alpha4/conversion.go b/controlplane/kubeadm/api/v1alpha4/conversion.go index 8a77233ec578..a3d5a2fad9f4 100644 --- a/controlplane/kubeadm/api/v1alpha4/conversion.go +++ b/controlplane/kubeadm/api/v1alpha4/conversion.go @@ -67,6 +67,7 @@ func (src *KubeadmControlPlane) ConvertTo(dstRaw conversion.Hub) error { } dst.Spec.MachineTemplate.NodeDeletionTimeout = restored.Spec.MachineTemplate.NodeDeletionTimeout + dst.Spec.RolloutBefore = restored.Spec.RolloutBefore return nil } @@ -140,6 +141,8 @@ func (src *KubeadmControlPlaneTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.MachineTemplate.NodeDeletionTimeout = restored.Spec.Template.Spec.MachineTemplate.NodeDeletionTimeout } + dst.Spec.Template.Spec.RolloutBefore = restored.Spec.Template.Spec.RolloutBefore + return nil } @@ -226,3 +229,8 @@ func Convert_v1beta1_KubeadmControlPlaneMachineTemplate_To_v1alpha4_KubeadmContr // .NodeDrainTimeout was added in v1beta1. return autoConvert_v1beta1_KubeadmControlPlaneMachineTemplate_To_v1alpha4_KubeadmControlPlaneMachineTemplate(in, out, s) } + +func Convert_v1beta1_KubeadmControlPlaneSpec_To_v1alpha4_KubeadmControlPlaneSpec(in *controlplanev1.KubeadmControlPlaneSpec, out *KubeadmControlPlaneSpec, scope apiconversion.Scope) error { + // .RolloutBefore was added in v1beta1. + return autoConvert_v1beta1_KubeadmControlPlaneSpec_To_v1alpha4_KubeadmControlPlaneSpec(in, out, scope) +} diff --git a/controlplane/kubeadm/api/v1alpha4/zz_generated.conversion.go b/controlplane/kubeadm/api/v1alpha4/zz_generated.conversion.go index 8e8241b7e5b4..3aecdb801264 100644 --- a/controlplane/kubeadm/api/v1alpha4/zz_generated.conversion.go +++ b/controlplane/kubeadm/api/v1alpha4/zz_generated.conversion.go @@ -72,11 +72,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.KubeadmControlPlaneSpec)(nil), (*KubeadmControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_KubeadmControlPlaneSpec_To_v1alpha4_KubeadmControlPlaneSpec(a.(*v1beta1.KubeadmControlPlaneSpec), b.(*KubeadmControlPlaneSpec), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*KubeadmControlPlaneStatus)(nil), (*v1beta1.KubeadmControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_KubeadmControlPlaneStatus_To_v1beta1_KubeadmControlPlaneStatus(a.(*KubeadmControlPlaneStatus), b.(*v1beta1.KubeadmControlPlaneStatus), scope) }); err != nil { @@ -157,6 +152,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta1.KubeadmControlPlaneSpec)(nil), (*KubeadmControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_KubeadmControlPlaneSpec_To_v1alpha4_KubeadmControlPlaneSpec(a.(*v1beta1.KubeadmControlPlaneSpec), b.(*KubeadmControlPlaneSpec), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta1.KubeadmControlPlaneTemplateResourceSpec)(nil), (*KubeadmControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_KubeadmControlPlaneTemplateResourceSpec_To_v1alpha4_KubeadmControlPlaneSpec(a.(*v1beta1.KubeadmControlPlaneTemplateResourceSpec), b.(*KubeadmControlPlaneSpec), scope) }); err != nil { @@ -291,16 +291,12 @@ func autoConvert_v1beta1_KubeadmControlPlaneSpec_To_v1alpha4_KubeadmControlPlane if err := kubeadmapiv1alpha4.Convert_v1beta1_KubeadmConfigSpec_To_v1alpha4_KubeadmConfigSpec(&in.KubeadmConfigSpec, &out.KubeadmConfigSpec, s); err != nil { return err } + // WARNING: in.RolloutBefore requires manual conversion: does not exist in peer-type out.RolloutAfter = (*v1.Time)(unsafe.Pointer(in.RolloutAfter)) out.RolloutStrategy = (*RolloutStrategy)(unsafe.Pointer(in.RolloutStrategy)) return nil } -// Convert_v1beta1_KubeadmControlPlaneSpec_To_v1alpha4_KubeadmControlPlaneSpec is an autogenerated conversion function. -func Convert_v1beta1_KubeadmControlPlaneSpec_To_v1alpha4_KubeadmControlPlaneSpec(in *v1beta1.KubeadmControlPlaneSpec, out *KubeadmControlPlaneSpec, s conversion.Scope) error { - return autoConvert_v1beta1_KubeadmControlPlaneSpec_To_v1alpha4_KubeadmControlPlaneSpec(in, out, s) -} - func autoConvert_v1alpha4_KubeadmControlPlaneStatus_To_v1beta1_KubeadmControlPlaneStatus(in *KubeadmControlPlaneStatus, out *v1beta1.KubeadmControlPlaneStatus, s conversion.Scope) error { out.Selector = in.Selector out.Replicas = in.Replicas diff --git a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_types.go b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_types.go index 5508d71ecbd9..8b68d14e262f 100644 --- a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_types.go +++ b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_types.go @@ -70,10 +70,14 @@ type KubeadmControlPlaneSpec struct { // to use for initializing and joining machines to the control plane. KubeadmConfigSpec bootstrapv1.KubeadmConfigSpec `json:"kubeadmConfigSpec"` + // RolloutBefore is a field to indicate a rollout should be performed + // if the specified criteria is met. + // +optional + RolloutBefore *RolloutBefore `json:"rolloutBefore,omitempty"` + // RolloutAfter is a field to indicate a rollout should be performed // after the specified time even if no changes have been made to the // KubeadmControlPlane. - // // +optional RolloutAfter *metav1.Time `json:"rolloutAfter,omitempty"` @@ -109,6 +113,14 @@ type KubeadmControlPlaneMachineTemplate struct { NodeDeletionTimeout *metav1.Duration `json:"nodeDeletionTimeout,omitempty"` } +// RolloutBefore describes when a rollout should be performed on the KCP machines. +type RolloutBefore struct { + // CertificatesExpiryDays indicates a rollout needs to be performed if the + // certificates of the machine will expire within the specified days. + // +optional + CertificatesExpiryDays *int32 `json:"certificatesExpiryDays,omitempty"` +} + // RolloutStrategy describes how to replace existing machines // with new ones. type RolloutStrategy struct { diff --git a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook.go b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook.go index 87b327026620..26c40aaa0014 100644 --- a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook.go +++ b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook.go @@ -128,6 +128,8 @@ const ( ignition = "ignition" ) +const minimumCertificatesExpiryDays = 7 + // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. func (in *KubeadmControlPlane) ValidateUpdate(old runtime.Object) error { // add a * to indicate everything beneath is ok. @@ -313,11 +315,28 @@ func validateKubeadmControlPlaneSpec(s KubeadmControlPlaneSpec, namespace string allErrs = append(allErrs, field.Invalid(pathPrefix.Child("version"), s.Version, "must be a valid semantic version")) } + allErrs = append(allErrs, validateRolloutBefore(s.RolloutBefore, pathPrefix.Child("rolloutBefore"))...) allErrs = append(allErrs, validateRolloutStrategy(s.RolloutStrategy, s.Replicas, pathPrefix.Child("rolloutStrategy"))...) return allErrs } +func validateRolloutBefore(rolloutBefore *RolloutBefore, pathPrefix *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if rolloutBefore == nil { + return allErrs + } + + if rolloutBefore.CertificatesExpiryDays != nil { + if *rolloutBefore.CertificatesExpiryDays < minimumCertificatesExpiryDays { + allErrs = append(allErrs, field.Invalid(pathPrefix.Child("certificatesExpiryDays"), *rolloutBefore.CertificatesExpiryDays, fmt.Sprintf("must be greater than or equal to %v", minimumCertificatesExpiryDays))) + } + } + + return allErrs +} + func validateRolloutStrategy(rolloutStrategy *RolloutStrategy, replicas *int32, pathPrefix *field.Path) field.ErrorList { allErrs := field.ErrorList{} diff --git a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook_test.go b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook_test.go index 39282b970a24..d5da7be9b710 100644 --- a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook_test.go +++ b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook_test.go @@ -140,6 +140,11 @@ func TestKubeadmControlPlaneValidateCreate(t *testing.T) { invalidCoreDNSVersion := valid.DeepCopy() invalidCoreDNSVersion.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag = "v1.7" // not a valid semantic version + invalidRolloutBeforeCertificateExpiryDays := valid.DeepCopy() + invalidRolloutBeforeCertificateExpiryDays.Spec.RolloutBefore = &RolloutBefore{ + CertificatesExpiryDays: pointer.Int32(5), // less than minimum + } + invalidIgnitionConfiguration := valid.DeepCopy() invalidIgnitionConfiguration.Spec.KubeadmConfigSpec.Ignition = &bootstrapv1.IgnitionSpec{} @@ -213,6 +218,11 @@ func TestKubeadmControlPlaneValidateCreate(t *testing.T) { expectErr: false, kcp: stringMaxSurge, }, + { + name: "should return error when given an invalid rolloutBefore.certificatesExpiryDays value", + expectErr: true, + kcp: invalidRolloutBeforeCertificateExpiryDays, + }, { name: "should return error when Ignition configuration is invalid", @@ -609,6 +619,11 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) { disableNTPServers := before.DeepCopy() disableNTPServers.Spec.KubeadmConfigSpec.NTP.Enabled = pointer.BoolPtr(false) + invalidRolloutBeforeCertificateExpiryDays := before.DeepCopy() + invalidRolloutBeforeCertificateExpiryDays.Spec.RolloutBefore = &RolloutBefore{ + CertificatesExpiryDays: pointer.Int32(5), // less than minimum + } + invalidIgnitionConfiguration := before.DeepCopy() invalidIgnitionConfiguration.Spec.KubeadmConfigSpec.Ignition = &bootstrapv1.IgnitionSpec{} @@ -955,6 +970,12 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) { before: before, kcp: updateJoinConfigurationPatches, }, + { + name: "should return error when rolloutBefore.certificatesExpiryDays is invalid", + expectErr: true, + before: before, + kcp: invalidRolloutBeforeCertificateExpiryDays, + }, { name: "should return error when Ignition configuration is invalid", enableIgnitionFeature: true, diff --git a/controlplane/kubeadm/api/v1beta1/kubeadmcontrolplanetemplate_types.go b/controlplane/kubeadm/api/v1beta1/kubeadmcontrolplanetemplate_types.go index 575b6b8e0771..4a0d168e135e 100644 --- a/controlplane/kubeadm/api/v1beta1/kubeadmcontrolplanetemplate_types.go +++ b/controlplane/kubeadm/api/v1beta1/kubeadmcontrolplanetemplate_types.go @@ -73,6 +73,12 @@ type KubeadmControlPlaneTemplateResourceSpec struct { // to use for initializing and joining machines to the control plane. KubeadmConfigSpec bootstrapv1.KubeadmConfigSpec `json:"kubeadmConfigSpec"` + // RolloutBefore is a field to indicate a rollout should be performed + // if the specified criteria is met. + // + // +optional + RolloutBefore *RolloutBefore `json:"rolloutBefore,omitempty"` + // RolloutAfter is a field to indicate a rollout should be performed // after the specified time even if no changes have been made to the // KubeadmControlPlane. diff --git a/controlplane/kubeadm/api/v1beta1/kubeadmcontrolplanetemplate_webhook.go b/controlplane/kubeadm/api/v1beta1/kubeadmcontrolplanetemplate_webhook.go index babfcccd2657..f08e8e3a8134 100644 --- a/controlplane/kubeadm/api/v1beta1/kubeadmcontrolplanetemplate_webhook.go +++ b/controlplane/kubeadm/api/v1beta1/kubeadmcontrolplanetemplate_webhook.go @@ -102,5 +102,10 @@ func (r *KubeadmControlPlaneTemplate) ValidateDelete() error { // validateKubeadmControlPlaneTemplateResourceSpec is a copy of validateKubeadmControlPlaneSpec which // only validates the fields in KubeadmControlPlaneTemplateResourceSpec we care about. func validateKubeadmControlPlaneTemplateResourceSpec(s KubeadmControlPlaneTemplateResourceSpec, pathPrefix *field.Path) field.ErrorList { - return validateRolloutStrategy(s.RolloutStrategy, nil, pathPrefix.Child("rolloutStrategy")) + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateRolloutBefore(s.RolloutBefore, pathPrefix.Child("rolloutBefore"))...) + allErrs = append(allErrs, validateRolloutStrategy(s.RolloutStrategy, nil, pathPrefix.Child("rolloutStrategy"))...) + + return allErrs } diff --git a/controlplane/kubeadm/api/v1beta1/zz_generated.deepcopy.go b/controlplane/kubeadm/api/v1beta1/zz_generated.deepcopy.go index ff16517906e7..00a23a61ede5 100644 --- a/controlplane/kubeadm/api/v1beta1/zz_generated.deepcopy.go +++ b/controlplane/kubeadm/api/v1beta1/zz_generated.deepcopy.go @@ -124,6 +124,11 @@ func (in *KubeadmControlPlaneSpec) DeepCopyInto(out *KubeadmControlPlaneSpec) { } in.MachineTemplate.DeepCopyInto(&out.MachineTemplate) in.KubeadmConfigSpec.DeepCopyInto(&out.KubeadmConfigSpec) + if in.RolloutBefore != nil { + in, out := &in.RolloutBefore, &out.RolloutBefore + *out = new(RolloutBefore) + (*in).DeepCopyInto(*out) + } if in.RolloutAfter != nil { in, out := &in.RolloutAfter, &out.RolloutAfter *out = (*in).DeepCopy() @@ -285,6 +290,11 @@ func (in *KubeadmControlPlaneTemplateResourceSpec) DeepCopyInto(out *KubeadmCont (*in).DeepCopyInto(*out) } in.KubeadmConfigSpec.DeepCopyInto(&out.KubeadmConfigSpec) + if in.RolloutBefore != nil { + in, out := &in.RolloutBefore, &out.RolloutBefore + *out = new(RolloutBefore) + (*in).DeepCopyInto(*out) + } if in.RolloutAfter != nil { in, out := &in.RolloutAfter, &out.RolloutAfter *out = (*in).DeepCopy() @@ -342,6 +352,26 @@ func (in *RollingUpdate) DeepCopy() *RollingUpdate { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutBefore) DeepCopyInto(out *RolloutBefore) { + *out = *in + if in.CertificatesExpiryDays != nil { + in, out := &in.CertificatesExpiryDays, &out.CertificatesExpiryDays + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutBefore. +func (in *RolloutBefore) DeepCopy() *RolloutBefore { + if in == nil { + return nil + } + out := new(RolloutBefore) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RolloutStrategy) DeepCopyInto(out *RolloutStrategy) { *out = *in diff --git a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml index d0ab6855b1c5..6aff2c2c3392 100644 --- a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml +++ b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml @@ -3592,6 +3592,17 @@ spec: made to the KubeadmControlPlane. format: date-time type: string + rolloutBefore: + description: RolloutBefore is a field to indicate a rollout should + be performed if the specified criteria is met. + properties: + certificatesExpiryDays: + description: CertificatesExpiryDays indicates a rollout needs + to be performed if the certificates of the machine will expire + within the specified days. + format: int32 + type: integer + type: object rolloutStrategy: default: rollingUpdate: diff --git a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml index dd2dea45fb4d..f39b43f79bbd 100644 --- a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml +++ b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml @@ -2344,6 +2344,17 @@ spec: changes have been made to the KubeadmControlPlane. format: date-time type: string + rolloutBefore: + description: RolloutBefore is a field to indicate a rollout + should be performed if the specified criteria is met. + properties: + certificatesExpiryDays: + description: CertificatesExpiryDays indicates a rollout + needs to be performed if the certificates of the machine + will expire within the specified days. + format: int32 + type: integer + type: object rolloutStrategy: default: rollingUpdate: diff --git a/controlplane/kubeadm/internal/control_plane.go b/controlplane/kubeadm/internal/control_plane.go index 59d02759c443..16267afde301 100644 --- a/controlplane/kubeadm/internal/control_plane.go +++ b/controlplane/kubeadm/internal/control_plane.go @@ -251,6 +251,8 @@ func (c *ControlPlane) MachinesNeedingRollout() collections.Machines { // Return machines if they are scheduled for rollout or if with an outdated configuration. return machines.AnyFilter( + // Machines whose certificates are about to expire. + collections.ShouldRolloutBefore(&c.reconciliationTime, c.KCP.Spec.RolloutBefore), // Machines that are scheduled for rollout (KCP.Spec.RolloutAfter set, the RolloutAfter deadline is expired, and the machine was created before the deadline). collections.ShouldRolloutAfter(&c.reconciliationTime, c.KCP.Spec.RolloutAfter), // Machines that do not match with KCP config. @@ -262,6 +264,8 @@ func (c *ControlPlane) MachinesNeedingRollout() collections.Machines { // plane's configuration and therefore do not require rollout. func (c *ControlPlane) UpToDateMachines() collections.Machines { return c.Machines.Filter( + // Machines that shouldn't be rollout out if their certificates are not about to expire. + collections.Not(collections.ShouldRolloutBefore(&c.reconciliationTime, c.KCP.Spec.RolloutBefore)), // Machines that shouldn't be rolled out after the deadline has expired. collections.Not(collections.ShouldRolloutAfter(&c.reconciliationTime, c.KCP.Spec.RolloutAfter)), // Machines that match with KCP config. diff --git a/internal/controllers/machine/machine_controller.go b/internal/controllers/machine/machine_controller.go index 6553bd74f335..eabec0faaba8 100644 --- a/internal/controllers/machine/machine_controller.go +++ b/internal/controllers/machine/machine_controller.go @@ -277,6 +277,7 @@ func (r *Reconciler) reconcile(ctx context.Context, cluster *clusterv1.Cluster, r.reconcileInfrastructure, r.reconcileNode, r.reconcileInterruptibleNodeLabel, + r.reconcileCertificateExpiry, } res := ctrl.Result{} diff --git a/internal/controllers/machine/machine_controller_phases.go b/internal/controllers/machine/machine_controller_phases.go index 243eb79125e3..5a83a03b11db 100644 --- a/internal/controllers/machine/machine_controller_phases.go +++ b/internal/controllers/machine/machine_controller_phases.go @@ -318,3 +318,54 @@ func (r *Reconciler) reconcileInfrastructure(ctx context.Context, cluster *clust m.Spec.ProviderID = pointer.StringPtr(providerID) return ctrl.Result{}, nil } + +func (r *Reconciler) reconcileCertificateExpiry(ctx context.Context, _ *clusterv1.Cluster, m *clusterv1.Machine) (ctrl.Result, error) { + var annotations map[string]string + + if !util.IsControlPlaneMachine(m) { + // If the machine is not a control plane machine, return early. + return ctrl.Result{}, nil + } + + var expiryInfoFound bool + + // Check for certificate expiry information in the machine annotation. + // This should take precedence over other information. + annotations = m.GetAnnotations() + if expiry, ok := annotations[clusterv1.MachineCertificatesExpiryDateAnnotation]; ok { + expiryInfoFound = true + expiryTime, err := time.Parse(time.RFC3339, expiry) + if err != nil { + return ctrl.Result{}, errors.Wrapf(err, "failed to reconcile certificates expiry: failed to parse expiry date from annotation on %s", klog.KObj(m)) + } + expTime := metav1.NewTime(expiryTime) + m.Status.CertificatesExpiryDate = &expTime + } else if m.Spec.Bootstrap.ConfigRef != nil { + // If the expiry information is not available on the machine annotation + // look for it on the bootstrap config. + bootstrapConfig, err := external.Get(ctx, r.Client, m.Spec.Bootstrap.ConfigRef, m.Namespace) + if err != nil { + return ctrl.Result{}, errors.Wrap(err, "failed to reconcile certificates expiry") + } + + // Check for certificate expiry information in the bootstrap config. + annotations = bootstrapConfig.GetAnnotations() + if expiry, ok := annotations[clusterv1.MachineCertificatesExpiryDateAnnotation]; ok { + expiryInfoFound = true + expiryTime, err := time.Parse(time.RFC3339, expiry) + if err != nil { + return ctrl.Result{}, errors.Wrapf(err, "failed to reconcile certificates expiry: failed to parse expiry date from annotation on %s", klog.KObj(bootstrapConfig)) + } + expTime := metav1.NewTime(expiryTime) + m.Status.CertificatesExpiryDate = &expTime + } + } + + // If the certificates expiry information is not fond on the machine + // and on the bootstrap config then reset machine.status.certificatesExpiryDate. + if !expiryInfoFound { + m.Status.CertificatesExpiryDate = nil + } + + return ctrl.Result{}, nil +} diff --git a/internal/controllers/machine/machine_controller_phases_test.go b/internal/controllers/machine/machine_controller_phases_test.go index 22cb1a341b6b..81c9a1b2bc4e 100644 --- a/internal/controllers/machine/machine_controller_phases_test.go +++ b/internal/controllers/machine/machine_controller_phases_test.go @@ -1100,3 +1100,236 @@ func TestReconcileInfrastructure(t *testing.T) { }) } } + +func TestReconcileCertificateExpiry(t *testing.T) { + fakeTimeString := "2020-01-01T00:00:00Z" + fakeTime, _ := time.Parse(time.RFC3339, fakeTimeString) + fakeMetaTime := &metav1.Time{Time: fakeTime} + + fakeTimeString2 := "2020-02-02T00:00:00Z" + fakeTime2, _ := time.Parse(time.RFC3339, fakeTimeString2) + fakeMetaTime2 := &metav1.Time{Time: fakeTime2} + + bootstrapConfigWithExpiry := map[string]interface{}{ + "kind": "GenericBootstrapConfig", + "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", + "metadata": map[string]interface{}{ + "name": "bootstrap-config-with-expiry", + "namespace": metav1.NamespaceDefault, + "annotations": map[string]interface{}{ + clusterv1.MachineCertificatesExpiryDateAnnotation: fakeTimeString, + }, + }, + "spec": map[string]interface{}{}, + "status": map[string]interface{}{ + "ready": true, + "dataSecretName": "secret-data", + }, + } + + bootstrapConfigWithoutExpiry := map[string]interface{}{ + "kind": "GenericBootstrapConfig", + "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", + "metadata": map[string]interface{}{ + "name": "bootstrap-config-without-expiry", + "namespace": metav1.NamespaceDefault, + }, + "spec": map[string]interface{}{}, + "status": map[string]interface{}{ + "ready": true, + "dataSecretName": "secret-data", + }, + } + + tests := []struct { + name string + machine *clusterv1.Machine + expected func(g *WithT, m *clusterv1.Machine) + }{ + { + name: "worker machine with certificate expiry annotation should not update expiry date", + machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bootstrap-test-existing", + Namespace: metav1.NamespaceDefault, + Annotations: map[string]string{ + clusterv1.MachineCertificatesExpiryDateAnnotation: fakeTimeString, + }, + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{}, + }, + }, + expected: func(g *WithT, m *clusterv1.Machine) { + g.Expect(m.Status.CertificatesExpiryDate).To(BeNil()) + }, + }, + { + name: "control plane machine with no bootstrap config and no certificate expiry annotation should not set expiry date", + machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bootstrap-test-existing", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + clusterv1.MachineControlPlaneLabelName: "", + }, + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{}, + }, + }, + expected: func(g *WithT, m *clusterv1.Machine) { + g.Expect(m.Status.CertificatesExpiryDate).To(BeNil()) + }, + }, + { + name: "control plane machine with bootstrap config and no certificate expiry annotation should not set expiry date", + machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bootstrap-test-existing", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + clusterv1.MachineControlPlaneLabelName: "", + }, + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{ + ConfigRef: &corev1.ObjectReference{ + APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", + Kind: "GenericBootstrapConfig", + Name: "bootstrap-config-without-expiry", + }, + }, + }, + }, + expected: func(g *WithT, m *clusterv1.Machine) { + g.Expect(m.Status.CertificatesExpiryDate).To(BeNil()) + }, + }, + { + name: "control plane machine with certificate expiry annotation in bootstrap config should set expiry date", + machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bootstrap-test-existing", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + clusterv1.MachineControlPlaneLabelName: "", + }, + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{ + ConfigRef: &corev1.ObjectReference{ + APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", + Kind: "GenericBootstrapConfig", + Name: "bootstrap-config-with-expiry", + }, + }, + }, + }, + expected: func(g *WithT, m *clusterv1.Machine) { + g.Expect(m.Status.CertificatesExpiryDate).To(Equal(fakeMetaTime)) + }, + }, + { + name: "control plane machine with certificate expiry annotation and no certificate expiry annotation on bootstrap config should set expiry date", + machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bootstrap-test-existing", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + clusterv1.MachineControlPlaneLabelName: "", + }, + Annotations: map[string]string{ + clusterv1.MachineCertificatesExpiryDateAnnotation: fakeTimeString, + }, + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{ + ConfigRef: &corev1.ObjectReference{ + APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", + Kind: "GenericBootstrapConfig", + Name: "bootstrap-config-without-expiry", + }, + }, + }, + }, + expected: func(g *WithT, m *clusterv1.Machine) { + g.Expect(m.Status.CertificatesExpiryDate).To(Equal(fakeMetaTime)) + }, + }, + { + name: "control plane machine with certificate expiry annotation in machine should take precedence over bootstrap config and should set expiry date", + machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bootstrap-test-existing", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + clusterv1.MachineControlPlaneLabelName: "", + }, + Annotations: map[string]string{ + clusterv1.MachineCertificatesExpiryDateAnnotation: fakeTimeString2, + }, + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{ + ConfigRef: &corev1.ObjectReference{ + APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", + Kind: "GenericBootstrapConfig", + Name: "bootstrap-config-with-expiry", + }, + }, + }, + }, + expected: func(g *WithT, m *clusterv1.Machine) { + g.Expect(m.Status.CertificatesExpiryDate).To(Equal(fakeMetaTime2)) + }, + }, + { + name: "reset certificates expiry information in machine status if the information is not available on the machine and the bootstrap config", + machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bootstrap-test-existing", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{ + clusterv1.MachineControlPlaneLabelName: "", + }, + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{ + ConfigRef: &corev1.ObjectReference{ + APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", + Kind: "GenericBootstrapConfig", + Name: "bootstrap-config-without-expiry", + }, + }, + }, + Status: clusterv1.MachineStatus{ + CertificatesExpiryDate: fakeMetaTime, + }, + }, + expected: func(g *WithT, m *clusterv1.Machine) { + g.Expect(m.Status.CertificatesExpiryDate).To(BeNil()) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + r := &Reconciler{ + Client: fake.NewClientBuilder(). + WithObjects( + tc.machine, + &unstructured.Unstructured{Object: bootstrapConfigWithExpiry}, + &unstructured.Unstructured{Object: bootstrapConfigWithoutExpiry}, + ).Build(), + } + + _, _ = r.reconcileCertificateExpiry(ctx, nil, tc.machine) + if tc.expected != nil { + tc.expected(g, tc.machine) + } + }) + } +} diff --git a/test/e2e/data/infrastructure-docker/v1beta1/clusterclass-quick-start.yaml b/test/e2e/data/infrastructure-docker/v1beta1/clusterclass-quick-start.yaml index b5eac232b244..6a8a505a7e60 100644 --- a/test/e2e/data/infrastructure-docker/v1beta1/clusterclass-quick-start.yaml +++ b/test/e2e/data/infrastructure-docker/v1beta1/clusterclass-quick-start.yaml @@ -230,6 +230,8 @@ metadata: spec: template: spec: + rolloutBefore: + certificatesExpiryDays: 21 machineTemplate: nodeDrainTimeout: 1s kubeadmConfigSpec: diff --git a/util/collections/machine_filters.go b/util/collections/machine_filters.go index 9de182b22cb6..37a1d1925d42 100644 --- a/util/collections/machine_filters.go +++ b/util/collections/machine_filters.go @@ -17,6 +17,8 @@ limitations under the License. package collections import ( + "time" + "github.com/blang/semver" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -177,6 +179,21 @@ func ShouldRolloutAfter(reconciliationTime, rolloutAfter *metav1.Time) Func { } } +// ShouldRolloutBefore returns a filter to find all machine whose +// certificates will expire within the specified days. +func ShouldRolloutBefore(reconciliationTime *metav1.Time, rolloutBefore *controlplanev1.RolloutBefore) Func { + return func(machine *clusterv1.Machine) bool { + if rolloutBefore == nil || rolloutBefore.CertificatesExpiryDays == nil { + return false + } + if machine == nil || machine.Status.CertificatesExpiryDate == nil { + return false + } + certsExpiryTime := machine.Status.CertificatesExpiryDate.Time + return reconciliationTime.Add(time.Duration(*rolloutBefore.CertificatesExpiryDays) * 24 * time.Hour).After(certsExpiryTime) + } +} + // HasAnnotationKey returns a filter to find all machines that have the // specified Annotation key present. func HasAnnotationKey(key string) Func { diff --git a/util/collections/machine_filters_test.go b/util/collections/machine_filters_test.go index 9644e35ef7d1..37c02790a84b 100644 --- a/util/collections/machine_filters_test.go +++ b/util/collections/machine_filters_test.go @@ -165,6 +165,53 @@ func TestShouldRolloutAfter(t *testing.T) { }) } +func TestShouldRolloutBeforeCertificatesExpire(t *testing.T) { + reconciliationTime := &metav1.Time{Time: time.Now()} + t.Run("if rolloutBefore is nil it should return false", func(t *testing.T) { + g := NewWithT(t) + m := &clusterv1.Machine{} + g.Expect(collections.ShouldRolloutBefore(reconciliationTime, nil)(m)).To(BeFalse()) + }) + t.Run("if rolloutBefore.certificatesExpiryDays is nil it should return false", func(t *testing.T) { + g := NewWithT(t) + m := &clusterv1.Machine{} + g.Expect(collections.ShouldRolloutBefore(reconciliationTime, &controlplanev1.RolloutBefore{})(m)).To(BeFalse()) + }) + t.Run("if machine is nil it should return false", func(t *testing.T) { + g := NewWithT(t) + rb := &controlplanev1.RolloutBefore{CertificatesExpiryDays: pointer.Int32(10)} + g.Expect(collections.ShouldRolloutBefore(reconciliationTime, rb)(nil)).To(BeFalse()) + }) + t.Run("if the machine certificate expiry information is not available it should return false", func(t *testing.T) { + g := NewWithT(t) + m := &clusterv1.Machine{} + rb := &controlplanev1.RolloutBefore{CertificatesExpiryDays: pointer.Int32(10)} + g.Expect(collections.ShouldRolloutBefore(reconciliationTime, rb)(m)).To(BeFalse()) + }) + t.Run("if the machine certificates are not going to expire within the expiry time it should return false", func(t *testing.T) { + g := NewWithT(t) + certificateExpiryTime := reconciliationTime.Add(60 * 24 * time.Hour) // certificates will expire in 60 days from 'now'. + m := &clusterv1.Machine{ + Status: clusterv1.MachineStatus{ + CertificatesExpiryDate: &metav1.Time{Time: certificateExpiryTime}, + }, + } + rb := &controlplanev1.RolloutBefore{CertificatesExpiryDays: pointer.Int32(10)} + g.Expect(collections.ShouldRolloutBefore(reconciliationTime, rb)(m)).To(BeFalse()) + }) + t.Run("if machine certificates will expire within the expiry time then it should return true", func(t *testing.T) { + g := NewWithT(t) + certificateExpiryTime := reconciliationTime.Add(5 * 24 * time.Hour) // certificates will expire in 5 days from 'now'. + m := &clusterv1.Machine{ + Status: clusterv1.MachineStatus{ + CertificatesExpiryDate: &metav1.Time{Time: certificateExpiryTime}, + }, + } + rb := &controlplanev1.RolloutBefore{CertificatesExpiryDays: pointer.Int32(10)} + g.Expect(collections.ShouldRolloutBefore(reconciliationTime, rb)(m)).To(BeTrue()) + }) +} + func TestHashAnnotationKey(t *testing.T) { t.Run("machine with specified annotation returns true", func(t *testing.T) { g := NewWithT(t)