diff --git a/api/v1beta2/cloudscalecluster_types.go b/api/v1beta2/cloudscalecluster_types.go index 1f713e6..f55a010 100644 --- a/api/v1beta2/cloudscalecluster_types.go +++ b/api/v1beta2/cloudscalecluster_types.go @@ -233,6 +233,11 @@ type FloatingIPSpec struct { // CloudscaleClusterStatus defines the observed state of CloudscaleCluster. type CloudscaleClusterStatus struct { + // observedGeneration is the latest generation observed by the controller. + // +optional + // +kubebuilder:validation:Minimum=1 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // Initialization contains v1beta2 initialization tracking. // +optional Initialization *ClusterInitializationStatus `json:"initialization,omitempty"` diff --git a/api/v1beta2/cloudscalemachine_types.go b/api/v1beta2/cloudscalemachine_types.go index 056e36a..68c32ca 100644 --- a/api/v1beta2/cloudscalemachine_types.go +++ b/api/v1beta2/cloudscalemachine_types.go @@ -130,6 +130,11 @@ type MachineInitializationStatus struct { // CloudscaleMachineStatus defines the observed state of CloudscaleMachine. type CloudscaleMachineStatus struct { + // observedGeneration is the latest generation observed by the controller. + // +optional + // +kubebuilder:validation:Minimum=1 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // Initialization contains v1beta2 initialization tracking. // +optional Initialization *MachineInitializationStatus `json:"initialization,omitempty"` diff --git a/api/v1beta2/cloudscalemachinetemplate_types.go b/api/v1beta2/cloudscalemachinetemplate_types.go index 2b91553..37c607f 100644 --- a/api/v1beta2/cloudscalemachinetemplate_types.go +++ b/api/v1beta2/cloudscalemachinetemplate_types.go @@ -35,6 +35,11 @@ type CloudscaleMachineTemplateResource struct { // CloudscaleMachineTemplateStatus defines the observed state of CloudscaleMachineTemplate. type CloudscaleMachineTemplateStatus struct { + // observedGeneration is the latest generation observed by the controller. + // +optional + // +kubebuilder:validation:Minimum=1 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // Capacity defines the resource capacity for nodes created from this template. // This value is used for autoscaling from zero operations as defined in: // https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscaleclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscaleclusters.yaml index 323e83e..ea6d5f6 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscaleclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscaleclusters.yaml @@ -400,6 +400,12 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + observedGeneration: + description: observedGeneration is the latest generation observed + by the controller. + format: int64 + minimum: 1 + type: integer type: object required: - spec diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscalemachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscalemachines.yaml index 0ba75e4..fd1c4b1 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscalemachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscalemachines.yaml @@ -266,6 +266,12 @@ spec: True when the server is running and ready. type: boolean type: object + observedGeneration: + description: observedGeneration is the latest generation observed + by the controller. + format: int64 + minimum: 1 + type: integer serverGroupID: description: ServerGroupID is the cloudscale.ch server group UUID. type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscalemachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscalemachinetemplates.yaml index e52a208..17871d6 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscalemachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudscalemachinetemplates.yaml @@ -238,6 +238,12 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + observedGeneration: + description: observedGeneration is the latest generation observed + by the controller. + format: int64 + minimum: 1 + type: integer type: object required: - spec diff --git a/internal/controller/cloudscalecluster_controller.go b/internal/controller/cloudscalecluster_controller.go index bcbea49..e1ac6d8 100644 --- a/internal/controller/cloudscalecluster_controller.go +++ b/internal/controller/cloudscalecluster_controller.go @@ -34,6 +34,7 @@ import ( "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/annotations" "sigs.k8s.io/cluster-api/util/conditions" + "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/cluster-api/util/predicates" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -135,7 +136,14 @@ func (r *CloudscaleClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re // when the reconcile context has timed out. patchCtx, patchCancel := context.WithTimeout(context.Background(), 30*time.Second) defer patchCancel() - if err := clusterScope.Close(patchCtx); err != nil && reterr == nil { + + // Advance status.observedGeneration only on a successful reconcile so + // it lags metadata.generation while reconciliation is failing. + var opts []patch.Option + if reterr == nil { + opts = append(opts, patch.WithStatusObservedGeneration{}) + } + if err := clusterScope.Close(patchCtx, opts...); err != nil && reterr == nil { reterr = err } }() diff --git a/internal/controller/cloudscalemachine_controller.go b/internal/controller/cloudscalemachine_controller.go index acfbb6f..a698599 100644 --- a/internal/controller/cloudscalemachine_controller.go +++ b/internal/controller/cloudscalemachine_controller.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/annotations" "sigs.k8s.io/cluster-api/util/conditions" + "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/cluster-api/util/predicates" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -161,7 +162,14 @@ func (r *CloudscaleMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re // when the reconcile context has timed out. patchCtx, patchCancel := context.WithTimeout(context.Background(), 30*time.Second) defer patchCancel() - if err := machineScope.Close(patchCtx); err != nil && reterr == nil { + + // Advance status.observedGeneration only on a successful reconcile so + // it lags metadata.generation while reconciliation is failing. + var opts []patch.Option + if reterr == nil { + opts = append(opts, patch.WithStatusObservedGeneration{}) + } + if err := machineScope.Close(patchCtx, opts...); err != nil && reterr == nil { reterr = err } }() diff --git a/internal/controller/cloudscalemachinetemplate_controller.go b/internal/controller/cloudscalemachinetemplate_controller.go index c2dee31..bbe2c44 100644 --- a/internal/controller/cloudscalemachinetemplate_controller.go +++ b/internal/controller/cloudscalemachinetemplate_controller.go @@ -83,7 +83,9 @@ func (r *CloudscaleMachineTemplateReconciler) Reconcile(ctx context.Context, req template.Status.Capacity = capacity - if err := patchHelper.Patch(ctx, template); err != nil { + // This patch is only reached on the success path, so advance + // status.observedGeneration to the reconciled generation. + if err := patchHelper.Patch(ctx, template, patch.WithStatusObservedGeneration{}); err != nil { logger.Error(err, "Failed to patch CloudscaleMachineTemplate status") return ctrl.Result{}, err } diff --git a/internal/scope/cluster.go b/internal/scope/cluster.go index 1573e25..13c081b 100644 --- a/internal/scope/cluster.go +++ b/internal/scope/cluster.go @@ -79,8 +79,8 @@ func NewClusterScope(params ClusterScopeParams) (*ClusterScope, error) { } // Close persists the CloudscaleCluster status and spec changes. -func (s *ClusterScope) Close(ctx context.Context) error { - return s.patchHelper.Patch(ctx, s.CloudscaleCluster) +func (s *ClusterScope) Close(ctx context.Context, opts ...patch.Option) error { + return s.patchHelper.Patch(ctx, s.CloudscaleCluster, opts...) } // Name returns the cluster name. diff --git a/internal/scope/machine.go b/internal/scope/machine.go index 6eb9166..1c29916 100644 --- a/internal/scope/machine.go +++ b/internal/scope/machine.go @@ -92,8 +92,8 @@ func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { } // Close persists the CloudscaleMachine status and spec changes. -func (s *MachineScope) Close(ctx context.Context) error { - return s.patchHelper.Patch(ctx, s.CloudscaleMachine) +func (s *MachineScope) Close(ctx context.Context, opts ...patch.Option) error { + return s.patchHelper.Patch(ctx, s.CloudscaleMachine, opts...) } // Name returns the machine name.