diff --git a/cmd/evicter/controller.go b/cmd/evicter/controller.go index e44334a..9898a21 100644 --- a/cmd/evicter/controller.go +++ b/cmd/evicter/controller.go @@ -19,7 +19,7 @@ type podProvisioner interface { } type Controller struct { - podStore cache.Indexer + podStore cache.Store queue workqueue.RateLimitingInterface informer cache.Controller podProvisioner podProvisioner @@ -27,7 +27,7 @@ type Controller struct { started time.Time } -func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller, podProvisioner podProvisioner, incubationPeriodSeconds int64) *Controller { +func NewController(queue workqueue.RateLimitingInterface, indexer cache.Store, informer cache.Controller, podProvisioner podProvisioner, incubationPeriodSeconds int64) *Controller { return &Controller{ informer: informer, podStore: indexer, @@ -53,9 +53,9 @@ const ( // annotationPreventEviction is a break-glass annotation to prevent automated eviction annotationPreventEviction = "k-rail/tainted-prevent-eviction" // annotationTimestamp stores the unix timestamp when the root event happened - annotationTimestamp = "k-rail/tainted-timestamp" + annotationTimestamp = "k-rail/tainted-timestamp" // annotationReason is used to define any additional reason in a human readable form - annotationReason = "k-rail/tainted-reason" + annotationReason = "k-rail/tainted-reason" ) const defaultEvictionReason = "Tainted" @@ -89,14 +89,12 @@ func canEvict(pod *v1.Pod, incubationPeriod time.Duration) bool { if pod == nil { return false } - val, ok := pod.Annotations[annotationPreventEviction] - if ok { - if val == "yes" || val == "true" { - return false - } + switch pod.Annotations[annotationPreventEviction] { + case "yes", "true", "1", "YES", "TRUE", "Yes", "True": + return false } - val, ok = pod.Annotations[annotationTimestamp] + val, ok := pod.Annotations[annotationTimestamp] if ok { i, err := strconv.ParseInt(val, 10, 64) if err != nil { diff --git a/cmd/evicter/controller_test.go b/cmd/evicter/controller_test.go new file mode 100644 index 0000000..deaceb1 --- /dev/null +++ b/cmd/evicter/controller_test.go @@ -0,0 +1,167 @@ +package main + +import ( + "reflect" + "strconv" + "testing" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" +) + +func TestCanEvict(t *testing.T) { + now := int(time.Now().Unix()) + specs := map[string]struct { + srcAnn map[string]string + expResult bool + }{ + "with timestamp after incubation period": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + }, + expResult: true, + }, + "with timestamp in incubation period": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now), + "k-rail/tainted-reason": "test", + }, + expResult: false, + }, + "without timestamp annotation": { + srcAnn: map[string]string{ + "k-rail/tainted-reason": "test", + }, + expResult: true, + }, + "with timestamp containing non timestamp string": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": "", + "k-rail/tainted-reason": "test", + }, + expResult: true, + }, + "with preventEviction annotation": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + "k-rail/tainted-prevent-eviction": "true", + }, + expResult: false, + }, + "with preventEviction annotation - uppercase": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + "k-rail/tainted-prevent-eviction": "TRUE", + }, + expResult: false, + }, + "with preventEviction annotation - yes": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + "k-rail/tainted-prevent-eviction": "yes", + }, + expResult: false, + }, + "with preventEviction annotation - non bool": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + "k-rail/tainted-prevent-eviction": "", + }, + expResult: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + pod := v1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: spec.srcAnn}} + if got := canEvict(&pod, time.Second); spec.expResult != got { + t.Errorf("expected %v but got %v", spec.expResult, got) + } + }) + } +} + +func TestEvictPod(t *testing.T) { + now := int(time.Now().Unix()) + + specs := map[string]struct { + srcAnn map[string]string + expReason string + expMsg string + expNoEviction bool + }{ + "evicted with custom reason": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-reason": "test", + }, + expReason: "Tainted", + expMsg: "test", + }, + "evicted with default reason": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + }, + expReason: "Tainted", + expMsg: noEvictionNote, + }, + "not evicted with annotation": { + srcAnn: map[string]string{ + "k-rail/tainted-timestamp": strconv.Itoa(now - 1), + "k-rail/tainted-prevent-eviction": "yes", + }, + expNoEviction: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "myPod", Annotations: spec.srcAnn}} + store.Add(pod) + prov := &recordingPodProvisioner{} + c := NewController(nil, store, nil, prov, 1) + // when + err := c.evictPod("myPod") + // then + if err != nil { + t.Fatalf("unexpected error: %+v", err) + } + if spec.expNoEviction { + if prov.evictedPods != nil { + t.Fatalf("expected no call but got %v", prov.evictedPods) + } + return + } + // there should be 1 call + if exp, got := []*v1.Pod{pod}, prov.evictedPods; !reflect.DeepEqual(exp, got) { + t.Errorf("expected %v but got %v", exp, got) + } + if exp, got := []string{spec.expReason}, prov.reasons; !reflect.DeepEqual(exp, got) { + t.Errorf("expected %v but got %v", exp, got) + } + if exp, got := []string{spec.expMsg}, prov.msgs; !reflect.DeepEqual(exp, got) { + t.Errorf("expected %v but got %v", exp, got) + } + }) + } +} + +type recordingPodProvisioner struct { + evictedPods []*v1.Pod + reasons []string + msgs []string + result error +} + +func (r *recordingPodProvisioner) Evict(pod *v1.Pod, reason, msg string) error { + r.evictedPods = append(r.evictedPods, pod) + r.reasons = append(r.reasons, reason) + r.msgs = append(r.msgs, msg) + return r.result +} diff --git a/cmd/evicter/main.go b/cmd/evicter/main.go index 2f55c02..747bbed 100644 --- a/cmd/evicter/main.go +++ b/cmd/evicter/main.go @@ -28,7 +28,6 @@ import ( "k8s.io/klog" ) - func main() { var ( kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file: `/.kube/config`") @@ -165,7 +164,7 @@ func (p *podEvicter) Evict(pod *v1.Pod, reason, msg string) error { if err != nil { return errors.Wrap(err, "eviction") } - p.eventRecorder.Eventf(pod, v1.EventTypeNormal, reason, msg) + p.eventRecorder.Eventf(pod, v1.EventTypeNormal, reason, msg) return nil }