@@ -23,6 +23,8 @@ import (
2323 "fmt"
2424 "slices"
2525
26+ corev1 "k8s.io/api/core/v1"
27+ "k8s.io/apimachinery/pkg/api/equality"
2628 "k8s.io/apimachinery/pkg/api/meta"
2729 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2830 "k8s.io/apimachinery/pkg/runtime"
@@ -57,56 +59,111 @@ func (ac *AggregatesController) Reconcile(ctx context.Context, req ctrl.Request)
5759 return ctrl.Result {}, k8sclient .IgnoreNotFound (err )
5860 }
5961
60- /// On- and off-boarding need to mess with the aggregates, so let's get out of their way
61- if ! meta . IsStatusConditionFalse ( hv . Status . Conditions , kvmv1 . ConditionTypeOnboarding ) ||
62- meta . IsStatusConditionTrue ( hv .Status .Conditions , kvmv1 . ConditionTypeTerminating ) {
62+ // Wait for onboarding controller to populate HypervisorID and ServiceID
63+ // before attempting to modify aggregates
64+ if hv .Status .HypervisorID == "" || hv . Status . ServiceID == "" {
6365 return ctrl.Result {}, nil
6466 }
6567
66- if slices .Equal (hv .Spec .Aggregates , hv .Status .Aggregates ) {
67- // Nothing to be done
68- return ctrl.Result {}, nil
68+ base := hv .DeepCopy ()
69+ desiredAggregates , desiredCondition := ac .determineDesiredState (hv )
70+
71+ if ! slices .Equal (desiredAggregates , hv .Status .Aggregates ) {
72+ // Apply aggregates to OpenStack and update status
73+ uuids , err := openstack .ApplyAggregates (ctx , ac .computeClient , hv .Name , desiredAggregates )
74+ if err != nil {
75+ // Set error condition
76+ condition := metav1.Condition {
77+ Type : kvmv1 .ConditionTypeAggregatesUpdated ,
78+ Status : metav1 .ConditionFalse ,
79+ Reason : kvmv1 .ConditionReasonFailed ,
80+ Message : fmt .Errorf ("failed to apply aggregates: %w" , err ).Error (),
81+ }
82+
83+ if meta .SetStatusCondition (& hv .Status .Conditions , condition ) {
84+ if err2 := ac .Status ().Patch (ctx , hv , k8sclient .MergeFromWithOptions (base ,
85+ k8sclient.MergeFromWithOptimisticLock {}), k8sclient .FieldOwner (AggregatesControllerName )); err2 != nil {
86+ return ctrl.Result {}, errors .Join (err , err2 )
87+ }
88+ }
89+ return ctrl.Result {}, err
90+ }
91+
92+ hv .Status .Aggregates = desiredAggregates
93+ hv .Status .AggregateUUIDs = uuids
6994 }
7095
71- err := openstack .ApplyAggregates (ctx , ac .computeClient , hv .Name , hv .Spec .Aggregates )
72- if err != nil {
73- err = fmt .Errorf ("failed to apply aggregates: %w" , err )
74- if err2 := ac .setErrorCondition (ctx , hv , err .Error ()); err2 != nil {
75- return ctrl.Result {}, errors .Join (err , err2 )
76- }
77- return ctrl.Result {}, err
96+ // Set the condition based on the determined desired state
97+ meta .SetStatusCondition (& hv .Status .Conditions , desiredCondition )
98+
99+ if equality .Semantic .DeepEqual (base , hv ) {
100+ return ctrl.Result {}, nil
78101 }
79102
80- base := hv .DeepCopy ()
81- hv .Status .Aggregates = hv .Spec .Aggregates
82- meta .SetStatusCondition (& hv .Status .Conditions , metav1.Condition {
83- Type : kvmv1 .ConditionTypeAggregatesUpdated ,
84- Status : metav1 .ConditionTrue ,
85- Reason : kvmv1 .ConditionReasonSucceeded ,
86- Message : "Aggregates updated successfully" ,
87- })
88103 return ctrl.Result {}, ac .Status ().Patch (ctx , hv , k8sclient .MergeFromWithOptions (base ,
89104 k8sclient.MergeFromWithOptimisticLock {}), k8sclient .FieldOwner (AggregatesControllerName ))
90105}
91106
92- // setErrorCondition sets the error condition on the Hypervisor status, returns error if update fails
93- func (ac * AggregatesController ) setErrorCondition (ctx context.Context , hv * kvmv1.Hypervisor , msg string ) error {
94- condition := metav1.Condition {
95- Type : kvmv1 .ConditionTypeAggregatesUpdated ,
96- Status : metav1 .ConditionFalse ,
97- Reason : kvmv1 .ConditionReasonFailed ,
98- Message : msg ,
107+ // determineDesiredState returns the desired aggregates and the corresponding condition
108+ // based on the hypervisor's current state. The condition status is True only when
109+ // spec aggregates are being applied. Otherwise, it's False with a reason explaining
110+ // why different aggregates are applied.
111+ func (ac * AggregatesController ) determineDesiredState (hv * kvmv1.Hypervisor ) ([]string , metav1.Condition ) {
112+ // If terminating AND evicted, remove from all aggregates
113+ // We must wait for eviction to complete before removing aggregates
114+ if meta .IsStatusConditionTrue (hv .Status .Conditions , kvmv1 .ConditionTypeTerminating ) {
115+ evictingCondition := meta .FindStatusCondition (hv .Status .Conditions , kvmv1 .ConditionTypeEvicting )
116+ // Only remove aggregates if eviction is complete (Evicting=False)
117+ // If Evicting condition is not set or still True, keep current aggregates
118+ if evictingCondition != nil && evictingCondition .Status == metav1 .ConditionFalse {
119+ return []string {}, metav1.Condition {
120+ Type : kvmv1 .ConditionTypeAggregatesUpdated ,
121+ Status : metav1 .ConditionFalse ,
122+ Reason : kvmv1 .ConditionReasonTerminating ,
123+ Message : "Aggregates cleared due to termination after eviction" ,
124+ }
125+ }
126+ // Still evicting or eviction not started - keep current aggregates
127+ return hv .Status .Aggregates , metav1.Condition {
128+ Type : kvmv1 .ConditionTypeAggregatesUpdated ,
129+ Status : metav1 .ConditionFalse ,
130+ Reason : kvmv1 .ConditionReasonEvictionInProgress ,
131+ Message : "Aggregates unchanged while terminating and eviction in progress" ,
132+ }
99133 }
100134
101- base := hv .DeepCopy ()
102- if meta .SetStatusCondition (& hv .Status .Conditions , condition ) {
103- if err := ac .Status ().Patch (ctx , hv , k8sclient .MergeFromWithOptions (base ,
104- k8sclient.MergeFromWithOptimisticLock {}), k8sclient .FieldOwner (AggregatesControllerName )); err != nil {
105- return err
135+ // If onboarding is in progress (Initial or Testing), add test aggregate
136+ onboardingCondition := meta .FindStatusCondition (hv .Status .Conditions , kvmv1 .ConditionTypeOnboarding )
137+ if onboardingCondition != nil && onboardingCondition .Status == metav1 .ConditionTrue {
138+ if onboardingCondition .Reason == kvmv1 .ConditionReasonInitial ||
139+ onboardingCondition .Reason == kvmv1 .ConditionReasonTesting {
140+ zone := hv .Labels [corev1 .LabelTopologyZone ]
141+ return []string {zone , testAggregateName }, metav1.Condition {
142+ Type : kvmv1 .ConditionTypeAggregatesUpdated ,
143+ Status : metav1 .ConditionFalse ,
144+ Reason : kvmv1 .ConditionReasonTestAggregates ,
145+ Message : "Test aggregate applied during onboarding instead of spec aggregates" ,
146+ }
147+ }
148+
149+ // If removing test aggregate, use Spec.Aggregates (no test aggregate)
150+ if onboardingCondition .Reason == kvmv1 .ConditionReasonRemovingTestAggregate {
151+ return hv .Spec .Aggregates , metav1.Condition {
152+ Type : kvmv1 .ConditionTypeAggregatesUpdated ,
153+ Status : metav1 .ConditionTrue ,
154+ Reason : kvmv1 .ConditionReasonSucceeded ,
155+ Message : "Aggregates from spec applied successfully" ,
156+ }
106157 }
107158 }
108159
109- return nil
160+ // Normal operations or onboarding complete: use Spec.Aggregates
161+ return hv .Spec .Aggregates , metav1.Condition {
162+ Type : kvmv1 .ConditionTypeAggregatesUpdated ,
163+ Status : metav1 .ConditionTrue ,
164+ Reason : kvmv1 .ConditionReasonSucceeded ,
165+ Message : "Aggregates from spec applied successfully" ,
166+ }
110167}
111168
112169// SetupWithManager sets up the controller with the Manager.
@@ -118,7 +175,6 @@ func (ac *AggregatesController) SetupWithManager(mgr ctrl.Manager) error {
118175 if ac .computeClient , err = openstack .GetServiceClient (ctx , "compute" , nil ); err != nil {
119176 return err
120177 }
121- ac .computeClient .Microversion = "2.40" // gophercloud only supports numeric ids
122178
123179 return ctrl .NewControllerManagedBy (mgr ).
124180 Named (AggregatesControllerName ).
0 commit comments