Skip to content

Commit 865b456

Browse files
notandyfwiesel
authored andcommitted
EvictionController: Add bounds checks for instance slice operations
Extract slice operations into helper functions (peekInstance, popInstance, moveToBack) with proper empty slice handling to prevent potential panics when accessing OutstandingInstances. Add unit tests for the helpers.
1 parent 5ba98af commit 865b456

2 files changed

Lines changed: 121 additions & 10 deletions

File tree

internal/controller/eviction_controller.go

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,36 @@ const (
5353
defaultPollTime = 10 * time.Second
5454
)
5555

56+
// popInstance removes and returns the last instance UUID from the slice.
57+
// Returns the modified slice and the UUID (empty string if slice was empty).
58+
func popInstance(instances []string) (remaining []string, uuid string) {
59+
if len(instances) == 0 {
60+
return instances, ""
61+
}
62+
return instances[:len(instances)-1], instances[len(instances)-1]
63+
}
64+
65+
// peekInstance returns the last instance UUID without removing it.
66+
// Returns empty string if the slice is empty.
67+
func peekInstance(instances []string) string {
68+
if len(instances) == 0 {
69+
return ""
70+
}
71+
return instances[len(instances)-1]
72+
}
73+
74+
// moveToBack moves the last instance to the front of the slice,
75+
// effectively deprioritizing it. Returns the modified slice.
76+
func moveToBack(instances []string) []string {
77+
if len(instances) < 2 {
78+
return instances
79+
}
80+
uuid := instances[len(instances)-1]
81+
copy(instances[1:], instances[:len(instances)-1])
82+
instances[0] = uuid
83+
return instances
84+
}
85+
5686
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=evictions,verbs=get;list;watch;create;update;patch;delete
5787
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=evictions/status,verbs=get;update;patch
5888
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=evictions/finalizers,verbs=update
@@ -224,9 +254,11 @@ func (r *EvictionReconciler) handleNotFound(ctx context.Context, eviction, base
224254
if base == nil {
225255
base = eviction.DeepCopy()
226256
}
227-
instances := &eviction.Status.OutstandingInstances
228-
uuid := (*instances)[len(*instances)-1]
229-
*instances = (*instances)[:len(*instances)-1]
257+
var uuid string
258+
eviction.Status.OutstandingInstances, uuid = popInstance(eviction.Status.OutstandingInstances)
259+
if uuid == "" {
260+
return nil
261+
}
230262
meta.SetStatusCondition(&eviction.Status.Conditions, metav1.Condition{
231263
Type: kvmv1.ConditionTypeMigration,
232264
Status: metav1.ConditionFalse,
@@ -238,8 +270,10 @@ func (r *EvictionReconciler) handleNotFound(ctx context.Context, eviction, base
238270

239271
func (r *EvictionReconciler) evictNext(ctx context.Context, eviction *kvmv1.Eviction) (ctrl.Result, error) {
240272
base := eviction.DeepCopy()
241-
instances := &eviction.Status.OutstandingInstances
242-
uuid := (*instances)[len(*instances)-1]
273+
uuid := peekInstance(eviction.Status.OutstandingInstances)
274+
if uuid == "" {
275+
return ctrl.Result{}, nil
276+
}
243277
log := logger.FromContext(ctx).WithName("Evict").WithValues("server", uuid)
244278
logger.IntoContext(ctx, log)
245279

@@ -265,8 +299,7 @@ func (r *EvictionReconciler) evictNext(ctx context.Context, eviction *kvmv1.Evic
265299
case "ERROR":
266300
// Needs manual intervention (or another operator fixes it)
267301
// put it at the end of the list (beginning of array)
268-
copy((*instances)[1:], (*instances)[:len(*instances)-1])
269-
(*instances)[0] = uuid
302+
eviction.Status.OutstandingInstances = moveToBack(eviction.Status.OutstandingInstances)
270303
log.Info("error", "faultMessage", vm.Fault.Message)
271304
meta.SetStatusCondition(&eviction.Status.Conditions, metav1.Condition{
272305
Type: kvmv1.ConditionTypeMigration,
@@ -303,14 +336,13 @@ func (r *EvictionReconciler) evictNext(ctx context.Context, eviction *kvmv1.Evic
303336
}
304337

305338
// All done
306-
*instances = (*instances)[:len(*instances)-1]
339+
eviction.Status.OutstandingInstances, _ = popInstance(eviction.Status.OutstandingInstances)
307340
return ctrl.Result{}, r.updateStatus(ctx, eviction, base)
308341
}
309342

310343
if vm.TaskState == "deleting" { //nolint:gocritic
311344
// We just have to wait for it to be gone. Try the next one.
312-
copy((*instances)[1:], (*instances)[:len(*instances)-1])
313-
(*instances)[0] = uuid
345+
eviction.Status.OutstandingInstances = moveToBack(eviction.Status.OutstandingInstances)
314346

315347
meta.SetStatusCondition(&eviction.Status.Conditions, metav1.Condition{
316348
Type: kvmv1.ConditionTypeMigration,

internal/controller/eviction_controller_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,85 @@ import (
3535
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
3636
)
3737

38+
var _ = Describe("Instance slice helpers", func() {
39+
Describe("peekInstance", func() {
40+
It("returns empty string for empty slice", func() {
41+
Expect(peekInstance([]string{})).To(Equal(""))
42+
})
43+
44+
It("returns empty string for nil slice", func() {
45+
Expect(peekInstance(nil)).To(Equal(""))
46+
})
47+
48+
It("returns the last element", func() {
49+
Expect(peekInstance([]string{"a", "b", "c"})).To(Equal("c"))
50+
})
51+
52+
It("returns the only element for single-element slice", func() {
53+
Expect(peekInstance([]string{"only"})).To(Equal("only"))
54+
})
55+
56+
It("does not modify the slice", func() {
57+
s := []string{"a", "b", "c"}
58+
peekInstance(s)
59+
Expect(s).To(Equal([]string{"a", "b", "c"}))
60+
})
61+
})
62+
63+
Describe("popInstance", func() {
64+
It("returns empty string and unchanged slice for empty slice", func() {
65+
s, uuid := popInstance([]string{})
66+
Expect(uuid).To(Equal(""))
67+
Expect(s).To(BeEmpty())
68+
})
69+
70+
It("returns empty string and unchanged slice for nil slice", func() {
71+
s, uuid := popInstance(nil)
72+
Expect(uuid).To(Equal(""))
73+
Expect(s).To(BeNil())
74+
})
75+
76+
It("removes and returns the last element", func() {
77+
s, uuid := popInstance([]string{"a", "b", "c"})
78+
Expect(uuid).To(Equal("c"))
79+
Expect(s).To(Equal([]string{"a", "b"}))
80+
})
81+
82+
It("returns empty slice when popping last element", func() {
83+
s, uuid := popInstance([]string{"only"})
84+
Expect(uuid).To(Equal("only"))
85+
Expect(s).To(BeEmpty())
86+
})
87+
})
88+
89+
Describe("moveToBack", func() {
90+
It("returns unchanged slice for empty slice", func() {
91+
s := moveToBack([]string{})
92+
Expect(s).To(BeEmpty())
93+
})
94+
95+
It("returns unchanged slice for nil slice", func() {
96+
s := moveToBack(nil)
97+
Expect(s).To(BeNil())
98+
})
99+
100+
It("returns unchanged slice for single-element slice", func() {
101+
s := moveToBack([]string{"only"})
102+
Expect(s).To(Equal([]string{"only"}))
103+
})
104+
105+
It("moves last element to front for two elements", func() {
106+
s := moveToBack([]string{"a", "b"})
107+
Expect(s).To(Equal([]string{"b", "a"}))
108+
})
109+
110+
It("moves last element to front for multiple elements", func() {
111+
s := moveToBack([]string{"a", "b", "c", "d"})
112+
Expect(s).To(Equal([]string{"d", "a", "b", "c"}))
113+
})
114+
})
115+
})
116+
38117
var _ = Describe("Eviction Controller", func() {
39118
const (
40119
resourceName = "test-resource"

0 commit comments

Comments
 (0)