Skip to content

Commit 9a7fee7

Browse files
committed
HypervisorMaintenance: Trigger eviction based on maintenance flag
When maintenance is set, it will trigger an eviction via the respective CRD and reflect the result back to the hypervisor status.
1 parent 06d108d commit 9a7fee7

File tree

3 files changed

+277
-8
lines changed

3 files changed

+277
-8
lines changed

api/v1/hypervisor_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const (
3535
ConditionReasonReadyReady = "ready"
3636
// or not
3737
ConditionReasonReadyMaintenance = "maintenance"
38+
ConditionReasonReadyEvicted = "evicted"
3839
)
3940

4041
// HypervisorSpec defines the desired state of Hypervisor

internal/controller/hypervisor_maintenance_controller.go

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ import (
2424
"context"
2525
"fmt"
2626

27+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2728
"k8s.io/apimachinery/pkg/api/meta"
2829
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2930
"k8s.io/apimachinery/pkg/runtime"
3031
ctrl "sigs.k8s.io/controller-runtime"
3132
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
33+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3234
logger "sigs.k8s.io/controller-runtime/pkg/log"
3335

3436
"github.com/gophercloud/gophercloud/v2"
@@ -39,7 +41,7 @@ import (
3941
)
4042

4143
const (
42-
HypervisorMaintenanceControllerName = "HypervisorMaintenanceController"
44+
HypervisorMaintenanceControllerName = "HypervisorMaintenance"
4345
)
4446

4547
type HypervisorMaintenanceController struct {
@@ -50,6 +52,7 @@ type HypervisorMaintenanceController struct {
5052

5153
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors,verbs=get;list;watch
5254
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors/status,verbs=get;list;watch;create;update;patch;delete
55+
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=evictions,verbs=get;list;watch;create;update;patch;delete
5356

5457
func (hec *HypervisorMaintenanceController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
5558
hv := &kvmv1.Hypervisor{}
@@ -69,15 +72,20 @@ func (hec *HypervisorMaintenanceController) Reconcile(ctx context.Context, req c
6972
}
7073

7174
log := logger.FromContext(ctx).
72-
WithName("HypervisorService")
75+
WithName(HypervisorMaintenanceControllerName)
7376
ctx = logger.IntoContext(ctx, log)
7477

7578
changed, err := hec.reconcileComputeService(ctx, hv)
7679
if err != nil {
7780
return ctrl.Result{}, err
7881
}
7982

80-
if changed {
83+
changed1, err := hec.reconcileEviction(ctx, hv)
84+
if err != nil {
85+
return ctrl.Result{}, err
86+
}
87+
88+
if changed || changed1 {
8189
return ctrl.Result{}, hec.Status().Update(ctx, hv)
8290
} else {
8391
return ctrl.Result{}, nil
@@ -115,6 +123,9 @@ func (hec *HypervisorMaintenanceController) reconcileComputeService(ctx context.
115123
return false, fmt.Errorf("failed to enable hypervisor due to %w", err)
116124
}
117125
case "manual", "auto", "ha": // Disable the compute service
126+
// Also in case of HA, as it doesn't hurt to disable it twice, and this
127+
// allows us to enable the service again, when the maintenance field is
128+
// cleared in the case above.
118129
if !meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{
119130
Type: kvmv1.ConditionTypeHypervisorDisabled,
120131
Status: metav1.ConditionTrue,
@@ -147,6 +158,106 @@ func (hec *HypervisorMaintenanceController) reconcileComputeService(ctx context.
147158
return true, nil
148159
}
149160

161+
func (hec *HypervisorMaintenanceController) reconcileEviction(ctx context.Context, hv *kvmv1.Hypervisor) (bool, error) {
162+
eviction := &kvmv1.Eviction{
163+
ObjectMeta: metav1.ObjectMeta{
164+
Name: hv.Name,
165+
},
166+
}
167+
168+
switch hv.Spec.Maintenance {
169+
case "":
170+
// Avoid deleting the eviction over and over.
171+
if hv.Status.Evicted || meta.RemoveStatusCondition(&hv.Status.Conditions, kvmv1.ConditionTypeEvicting) {
172+
err := k8sclient.IgnoreNotFound(hec.Delete(ctx, eviction))
173+
hv.Status.Evicted = false
174+
return true, err
175+
}
176+
return false, nil
177+
case "manual", "auto": // In case of "ha", the host gets emptied from the HA service
178+
if cond := meta.FindStatusCondition(hv.Status.Conditions, kvmv1.ConditionTypeEvicting); cond != nil {
179+
if cond.Reason == kvmv1.ConditionReasonSucceeded {
180+
// We are done here, no need to look at the eviction any more
181+
return false, nil
182+
}
183+
}
184+
status, err := hec.ensureEviction(ctx, eviction, hv)
185+
if err != nil {
186+
return false, err
187+
}
188+
var reason, message string
189+
changed := false
190+
if status == metav1.ConditionFalse {
191+
message = "Evicted"
192+
reason = kvmv1.ConditionReasonSucceeded
193+
if !hv.Status.Evicted {
194+
changed = true
195+
hv.Status.Evicted = true
196+
}
197+
} else {
198+
message = "Evicting"
199+
reason = kvmv1.ConditionReasonRunning
200+
if hv.Status.Evicted {
201+
changed = true
202+
hv.Status.Evicted = false
203+
}
204+
}
205+
206+
if meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{
207+
Type: kvmv1.ConditionTypeEvicting,
208+
Status: status,
209+
Reason: reason,
210+
Message: message,
211+
}) {
212+
changed = true
213+
}
214+
215+
if meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{
216+
Type: kvmv1.ConditionTypeReady,
217+
Status: metav1.ConditionFalse,
218+
Reason: kvmv1.ConditionReasonReadyEvicted,
219+
Message: "Hypervisor is disabled and evicted",
220+
}) {
221+
changed = true
222+
}
223+
logger.FromContext(ctx).Info("reconcile", "changed", changed, "conditions", hv.Status.Conditions)
224+
return changed, nil
225+
}
226+
227+
return false, nil
228+
}
229+
230+
func (hec *HypervisorMaintenanceController) ensureEviction(ctx context.Context, eviction *kvmv1.Eviction, hypervisor *kvmv1.Hypervisor) (metav1.ConditionStatus, error) {
231+
log := logger.FromContext(ctx)
232+
if err := hec.Get(ctx, k8sclient.ObjectKeyFromObject(eviction), eviction); err != nil {
233+
if !k8serrors.IsNotFound(err) {
234+
return metav1.ConditionUnknown, fmt.Errorf("failed to get eviction due to %w", err)
235+
}
236+
if err := controllerutil.SetControllerReference(hypervisor, eviction, hec.Scheme); err != nil {
237+
return metav1.ConditionUnknown, err
238+
}
239+
log.Info("Creating new eviction", "name", eviction.Name)
240+
eviction.Spec = kvmv1.EvictionSpec{
241+
Hypervisor: hypervisor.Name,
242+
Reason: "openstack-hypervisor-operator maintenance",
243+
}
244+
245+
// This also transports the label-selector, if set
246+
transportLabels(&eviction.ObjectMeta, hypervisor)
247+
248+
if err = hec.Create(ctx, eviction); err != nil {
249+
return metav1.ConditionUnknown, fmt.Errorf("failed to create eviction due to %w", err)
250+
}
251+
}
252+
253+
// check if we are still evicting (defaulting to yes)
254+
if meta.IsStatusConditionFalse(eviction.Status.Conditions, kvmv1.ConditionTypeEvicting) {
255+
return metav1.ConditionFalse, nil
256+
} else {
257+
return metav1.ConditionTrue, nil
258+
}
259+
}
260+
150261
// SetupWithManager sets up the controller with the Manager.
151262
func (hec *HypervisorMaintenanceController) SetupWithManager(mgr ctrl.Manager) error {
152263
ctx := context.Background()
@@ -161,5 +272,6 @@ func (hec *HypervisorMaintenanceController) SetupWithManager(mgr ctrl.Manager) e
161272
return ctrl.NewControllerManagedBy(mgr).
162273
Named(HypervisorMaintenanceControllerName).
163274
For(&kvmv1.Hypervisor{}).
275+
Owns(&kvmv1.Eviction{}). // trigger Reconcile whenever an Own-ed eviction is created/updated/deleted
164276
Complete(hec)
165277
}

internal/controller/hypervisor_maintenance_controller_test.go

Lines changed: 161 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import (
3030
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3131
"k8s.io/apimachinery/pkg/types"
3232
ctrl "sigs.k8s.io/controller-runtime"
33+
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
34+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3335

3436
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
3537
)
@@ -72,7 +74,7 @@ var _ = Describe("HypervisorMaintenanceController", func() {
7274

7375
w.WriteHeader(http.StatusOK)
7476
_, err = fmt.Fprint(w, ServiceEnabledResponse)
75-
Expect(err).NotTo(HaveOccurred())
77+
Expect(err).To(Succeed())
7678
})
7779
}
7880

@@ -113,6 +115,16 @@ var _ = Describe("HypervisorMaintenanceController", func() {
113115
Expect(err).NotTo(HaveOccurred())
114116
})
115117

118+
AfterEach(func(ctx context.Context) {
119+
eviction := &kvmv1.Eviction{
120+
ObjectMeta: metav1.ObjectMeta{
121+
Name: hypervisorName.Name,
122+
Namespace: hypervisorName.Namespace,
123+
},
124+
}
125+
Expect(k8sclient.IgnoreNotFound(k8sClient.Delete(ctx, eviction))).To(Succeed())
126+
})
127+
116128
// Tests
117129
Context("Onboarded Hypervisor", func() {
118130
BeforeEach(func() {
@@ -127,7 +139,6 @@ var _ = Describe("HypervisorMaintenanceController", func() {
127139
Message: "random text",
128140
},
129141
)
130-
131142
Expect(k8sClient.Status().Update(ctx, hypervisor)).To(Succeed())
132143
})
133144

@@ -169,9 +180,6 @@ var _ = Describe("HypervisorMaintenanceController", func() {
169180
Expect(k8sClient.Update(ctx, hypervisor)).To(Succeed())
170181
expectedBody := fmt.Sprintf(`{"disabled_reason": "Hypervisor CRD: spec.maintenance=%v", "status": "disabled"}`, mode)
171182
mockServiceUpdate(expectedBody)
172-
req := ctrl.Request{NamespacedName: hypervisorName}
173-
_, err := controller.Reconcile(ctx, req)
174-
Expect(err).NotTo(HaveOccurred())
175183
})
176184

177185
It("should set the ConditionTypeHypervisorDisabled to true", func() {
@@ -192,5 +200,153 @@ var _ = Describe("HypervisorMaintenanceController", func() {
192200
}) // Spec.Maintenance="<mode>"
193201
}
194202

203+
Describe("Eviction reconciliation", func() {
204+
Context("Spec.Maintenance=\"\"", func() {
205+
BeforeEach(func() {
206+
hypervisor := &kvmv1.Hypervisor{}
207+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
208+
hypervisor.Spec.Maintenance = ""
209+
Expect(k8sClient.Update(ctx, hypervisor)).To(Succeed())
210+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
211+
meta.SetStatusCondition(&hypervisor.Status.Conditions,
212+
metav1.Condition{
213+
Type: kvmv1.ConditionTypeEvicting,
214+
Reason: "dontcare",
215+
Status: metav1.ConditionUnknown,
216+
Message: "dontcare",
217+
})
218+
Expect(k8sClient.Status().Update(ctx, hypervisor)).To(Succeed())
219+
expectedBody := `{"status": "enabled"}`
220+
mockServiceUpdate(expectedBody)
221+
222+
eviction := &kvmv1.Eviction{
223+
ObjectMeta: metav1.ObjectMeta{Name: hypervisorName.Name},
224+
Spec: kvmv1.EvictionSpec{
225+
Hypervisor: hypervisorName.Name,
226+
Reason: "test",
227+
},
228+
}
229+
Expect(controllerutil.SetControllerReference(hypervisor, eviction, controller.Scheme)).To(Succeed())
230+
Expect(k8sClient.Create(ctx, eviction)).To(Succeed())
231+
})
232+
233+
It("should delete the created eviction", func() {
234+
eviction := &kvmv1.Eviction{}
235+
err := k8sClient.Get(ctx, hypervisorName, eviction)
236+
By(fmt.Sprintf("%+v", *eviction))
237+
Expect(err).To(HaveOccurred())
238+
Expect(k8sclient.IgnoreNotFound(err)).To(Succeed())
239+
})
240+
}) // Spec.Maintenance=""
241+
242+
Context("Spec.Maintenance=\"ha\"", func() {
243+
BeforeEach(func() {
244+
hypervisor := &kvmv1.Hypervisor{}
245+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
246+
hypervisor.Spec.Maintenance = "ha"
247+
Expect(k8sClient.Update(ctx, hypervisor)).To(Succeed())
248+
expectedBody := `{"disabled_reason": "Hypervisor CRD: spec.maintenance=ha", "status": "disabled"}`
249+
mockServiceUpdate(expectedBody)
250+
})
251+
It("should not create an eviction resource", func() {
252+
eviction := &kvmv1.Eviction{}
253+
err := k8sClient.Get(ctx, hypervisorName, eviction)
254+
Expect(err).To(HaveOccurred())
255+
Expect(k8sclient.IgnoreNotFound(err)).To(Succeed())
256+
})
257+
}) // Spec.Maintenance="ha"
258+
259+
for _, mode := range []string{"auto", "manual"} {
260+
Context(fmt.Sprintf("Spec.Maintenance=\"%v\"", mode), func() {
261+
BeforeEach(func() {
262+
hypervisor := &kvmv1.Hypervisor{}
263+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
264+
hypervisor.Spec.Maintenance = mode
265+
Expect(k8sClient.Update(ctx, hypervisor)).To(Succeed())
266+
expectedBody := fmt.Sprintf(`{"disabled_reason": "Hypervisor CRD: spec.maintenance=%v", "status": "disabled"}`, mode)
267+
mockServiceUpdate(expectedBody)
268+
})
269+
270+
When("there is no eviction yet", func() {
271+
It("should create an eviction resource named as the hypervisor", func() {
272+
eviction := &kvmv1.Eviction{}
273+
Expect(k8sClient.Get(ctx, hypervisorName, eviction)).To(Succeed())
274+
})
275+
276+
It("should create an evicting condition", func() {
277+
hypervisor := &kvmv1.Hypervisor{}
278+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
279+
Expect(hypervisor.Status.Conditions).To(ContainElement(
280+
SatisfyAll(
281+
HaveField("Type", kvmv1.ConditionTypeEvicting),
282+
HaveField("Status", metav1.ConditionTrue),
283+
HaveField("Reason", kvmv1.ConditionReasonRunning),
284+
),
285+
))
286+
})
287+
288+
It("should reflect it in the hypervisor evicted status", func() {
289+
hypervisor := &kvmv1.Hypervisor{}
290+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
291+
Expect(hypervisor.Status.Evicted).To(BeFalse())
292+
})
293+
})
294+
295+
When("there is a finished eviction", func() {
296+
BeforeEach(func() {
297+
eviction := &kvmv1.Eviction{
298+
ObjectMeta: metav1.ObjectMeta{Name: hypervisorName.Name},
299+
Spec: kvmv1.EvictionSpec{
300+
Hypervisor: hypervisorName.Name,
301+
Reason: "test",
302+
},
303+
}
304+
hypervisor := &kvmv1.Hypervisor{}
305+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
306+
Expect(controllerutil.SetControllerReference(hypervisor, eviction, controller.Scheme)).To(Succeed())
307+
Expect(k8sClient.Create(ctx, eviction)).To(Succeed())
308+
309+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
310+
meta.SetStatusCondition(&eviction.Status.Conditions, metav1.Condition{
311+
Type: kvmv1.ConditionTypeEvicting,
312+
Status: metav1.ConditionFalse,
313+
Message: "whatever",
314+
Reason: kvmv1.ConditionReasonSucceeded,
315+
})
316+
Expect(k8sClient.Status().Update(ctx, eviction)).To(Succeed())
317+
})
318+
319+
It("should reflect it in the hypervisor evicting condition", func() {
320+
hypervisor := &kvmv1.Hypervisor{}
321+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
322+
Expect(hypervisor.Status.Conditions).To(ContainElement(
323+
SatisfyAll(
324+
HaveField("Type", kvmv1.ConditionTypeEvicting),
325+
HaveField("Status", metav1.ConditionFalse),
326+
HaveField("Reason", kvmv1.ConditionReasonSucceeded),
327+
),
328+
))
329+
})
330+
331+
It("should reflect it in the hypervisor evicted status", func() {
332+
hypervisor := &kvmv1.Hypervisor{}
333+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
334+
Expect(hypervisor.Status.Evicted).To(BeTrue())
335+
})
336+
337+
It("should set the ConditionTypeReady to false and reason to evicted", func() {
338+
updated := &kvmv1.Hypervisor{}
339+
Expect(k8sClient.Get(ctx, hypervisorName, updated)).To(Succeed())
340+
Expect(updated.Status.Conditions).To(ContainElement(
341+
SatisfyAll(
342+
HaveField("Type", kvmv1.ConditionTypeReady),
343+
HaveField("Status", metav1.ConditionFalse),
344+
HaveField("Reason", kvmv1.ConditionReasonReadyEvicted),
345+
)))
346+
})
347+
})
348+
}) // Spec.Maintenance="<mode>"
349+
}
350+
})
195351
}) // Context Onboarded Hypervisor
196352
})

0 commit comments

Comments
 (0)