diff --git a/pkg/controller/utils/affinity/affinity_test.go b/pkg/controller/utils/affinity/affinity_test.go index ad6b091b..57b78ddb 100644 --- a/pkg/controller/utils/affinity/affinity_test.go +++ b/pkg/controller/utils/affinity/affinity_test.go @@ -8,6 +8,8 @@ package affinity import ( "testing" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" ) @@ -127,3 +129,36 @@ func TestReplaceNodeNameNodeAffinity(t *testing.T) { }) } } + +func TestGetNodeNameFromAffinity(t *testing.T) { + // nil case + got := GetNodeNameFromAffinity(nil) + assert.Equal(t, got, "") + + // empty case + affinity := &v1.Affinity{} + got = GetNodeNameFromAffinity(affinity) + assert.Equal(t, got, "") + + // non-nil case + nodeName := "foo-node" + nodeNameSelReq := v1.NodeSelectorRequirement{ + Key: NodeFieldSelectorKeyNodeName, + Operator: v1.NodeSelectorOpIn, + Values: []string{nodeName}, + } + + affinity = &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchFields: []v1.NodeSelectorRequirement{nodeNameSelReq}, + }, + }, + }, + }, + } + got = GetNodeNameFromAffinity(affinity) + assert.Equal(t, got, "foo-node") +} diff --git a/pkg/controller/utils/comparison/comparison_test.go b/pkg/controller/utils/comparison/comparison_test.go index c0c0b9d1..1086f555 100644 --- a/pkg/controller/utils/comparison/comparison_test.go +++ b/pkg/controller/utils/comparison/comparison_test.go @@ -5,7 +5,15 @@ package comparison -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + datadoghqv1alpha1 "github.com/DataDog/extendeddaemonset/api/v1alpha1" +) func TestGenerateHashFromEDSResourceNodeAnnotation(t *testing.T) { type args struct { @@ -66,3 +74,94 @@ func TestGenerateHashFromEDSResourceNodeAnnotation(t *testing.T) { }) } } + +func TestGenerateMD5PodTemplateSpec(t *testing.T) { + ds := &datadoghqv1alpha1.ExtendedDaemonSet{} + ds = datadoghqv1alpha1.DefaultExtendedDaemonSet(ds, datadoghqv1alpha1.ExtendedDaemonSetSpecStrategyCanaryValidationModeAuto) + got, err := GenerateMD5PodTemplateSpec(&ds.Spec.Template) + assert.Equal(t, "a2bb34618483323482d9a56ae2515eed", got) + assert.Nil(t, err) +} + +func TestComparePodTemplateSpecMD5Hash(t *testing.T) { + // no annotation + hash := "somerandomhash" + rs := &datadoghqv1alpha1.ExtendedDaemonSetReplicaSet{} + got := ComparePodTemplateSpecMD5Hash(hash, rs) + assert.False(t, got) + + // non-matching annotation + hash = "somerandomhash" + rs = &datadoghqv1alpha1.ExtendedDaemonSetReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + string(datadoghqv1alpha1.MD5ExtendedDaemonSetAnnotationKey): "adifferenthash", + }, + }, + } + got = ComparePodTemplateSpecMD5Hash(hash, rs) + assert.False(t, got) + + // matching annotation + hash = "ahashthatmatches" + rs = &datadoghqv1alpha1.ExtendedDaemonSetReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + string(datadoghqv1alpha1.MD5ExtendedDaemonSetAnnotationKey): "ahashthatmatches", + }, + }, + } + got = ComparePodTemplateSpecMD5Hash(hash, rs) + assert.True(t, got) +} + +func TestSetMD5PodTemplateSpecAnnotation(t *testing.T) { + rs := &datadoghqv1alpha1.ExtendedDaemonSetReplicaSet{} + ds := &datadoghqv1alpha1.ExtendedDaemonSet{} + got, err := SetMD5PodTemplateSpecAnnotation(rs, ds) + assert.Equal(t, "a2bb34618483323482d9a56ae2515eed", got) + assert.Nil(t, err) +} + +func Test_StringsContains(t *testing.T) { + tests := []struct { + name string + a []string + x string + want bool + }{ + { + name: "a contains x", + a: []string{ + "hello", + "goodbye", + }, + x: "hello", + want: true, + }, + { + name: "a does not contain x", + a: []string{ + "hello", + "goodbye", + }, + x: "hi", + want: false, + }, + { + name: "a does not contain x (but it is a substring of a string in a)", + a: []string{ + "hello", + "goodbye", + }, + x: "good", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := StringsContains(tt.a, tt.x) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/controller/utils/labels_test.go b/pkg/controller/utils/labels_test.go index 7945f702..06193b28 100644 --- a/pkg/controller/utils/labels_test.go +++ b/pkg/controller/utils/labels_test.go @@ -58,3 +58,35 @@ func TestBuildInfoLabels(t *testing.T) { }) } } + +func Test_sanitizeLabelName(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "no change", + input: "hello", + want: "hello", + }, + { + name: "one invalid character", + input: "hello!", + want: "hello_", + }, + { + name: "two invalid characters", + input: "h*ello!", + want: "h_ello_", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := sanitizeLabelName(tt.input) + if got != tt.want { + t.Errorf("sanitizeLabelName() got = %#v, want %#v", got, tt.want) + } + }) + } +} diff --git a/pkg/controller/utils/list_test.go b/pkg/controller/utils/list_test.go new file mode 100644 index 00000000..4d977df1 --- /dev/null +++ b/pkg/controller/utils/list_test.go @@ -0,0 +1,52 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2019 Datadog, Inc. + +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestContainsString(t *testing.T) { + list := []string{ + "hello", + "goodbye", + "see you", + "hi!", + } + s := "hi!" + containsString := ContainsString(list, s) + assert.True(t, containsString) + + s = "see you?" + containsString = ContainsString(list, s) + assert.False(t, containsString) + + s = "see" + containsString = ContainsString(list, s) + assert.False(t, containsString) + + s = "see you" + containsString = ContainsString(list, s) + assert.True(t, containsString) +} + +func TestRemoveString(t *testing.T) { + list := []string{ + "hello", + "goodbye", + "see you", + "hi!", + } + s := "hi!" + result := RemoveString(list, s) + assert.NotContains(t, result, s) + + s = "see you?" + result = RemoveString(list, s) + assert.Equal(t, list, result) +} diff --git a/pkg/controller/utils/pod/create_test.go b/pkg/controller/utils/pod/create_test.go index 785c7a31..815c0194 100644 --- a/pkg/controller/utils/pod/create_test.go +++ b/pkg/controller/utils/pod/create_test.go @@ -8,12 +8,16 @@ package pod import ( "fmt" "testing" + "time" + + "github.com/stretchr/testify/assert" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" datadoghqv1alpha1 "github.com/DataDog/extendeddaemonset/api/v1alpha1" + datadoghqv1alpha1test "github.com/DataDog/extendeddaemonset/api/v1alpha1/test" ctrltest "github.com/DataDog/extendeddaemonset/pkg/controller/test" ) @@ -129,3 +133,36 @@ func Test_overwriteResourcesFromNode(t *testing.T) { }) } } + +func Test_overwriteResourcesFromEdsNode(t *testing.T) { + templateOriginal := &corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "container1"}, + }, + }, + } + templateCopy := templateOriginal.DeepCopy() + + // nil case, no change to template + edsNode := &datadoghqv1alpha1.ExtendedDaemonsetSetting{} + overwriteResourcesFromEdsNode(templateOriginal, edsNode) + assert.Equal(t, templateCopy, templateOriginal) + + // template changed + resourcesRef := corev1.ResourceList{ + "cpu": resource.MustParse("0.1"), + "memory": resource.MustParse("20M"), + } + edsNode = datadoghqv1alpha1test.NewExtendedDaemonsetSetting("foo", "bar", "reference", &datadoghqv1alpha1test.NewExtendedDaemonsetSettingOptions{ + CreationTime: time.Now(), + Resources: map[string]corev1.ResourceRequirements{ + "container1": { + Requests: resourcesRef, + }, + }, + }) + overwriteResourcesFromEdsNode(templateOriginal, edsNode) + assert.NotEqual(t, templateCopy, templateOriginal) + assert.Equal(t, resourcesRef, templateOriginal.Spec.Containers[0].Resources.Requests) +} diff --git a/pkg/controller/utils/pod/pod_test.go b/pkg/controller/utils/pod/pod_test.go index a67e5ce1..32858196 100644 --- a/pkg/controller/utils/pod/pod_test.go +++ b/pkg/controller/utils/pod/pod_test.go @@ -18,7 +18,353 @@ import ( ctrltest "github.com/DataDog/extendeddaemonset/pkg/controller/test" ) -func Test_HighestPodRestartCount(t *testing.T) { +func TestGetContainerStatus(t *testing.T) { + now := time.Now() + statuses := []v1.ContainerStatus{ + { + Name: "clb", + RestartCount: 10, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "CrashLoopBackOff", + FinishedAt: metav1.NewTime(now.Add(-time.Hour)), + }, + }, + }, + { + Name: "oom", + RestartCount: 1, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "OOMKilled", + FinishedAt: metav1.NewTime(now.Add(-2 * time.Hour)), + }, + }, + }, + } + + // Status exists and is found + name := "clb" + status, exists := GetContainerStatus(statuses, name) + assert.Equal(t, statuses[0], status) + assert.True(t, exists) + + // Status does not exist + name = "cla" + status, exists = GetContainerStatus(statuses, name) + assert.Equal(t, v1.ContainerStatus{}, status) + assert.False(t, exists) +} + +func TestGetExistingContainerStatus(t *testing.T) { + now := time.Now() + statuses := []v1.ContainerStatus{ + { + Name: "clb", + RestartCount: 10, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "CrashLoopBackOff", + FinishedAt: metav1.NewTime(now.Add(-time.Hour)), + }, + }, + }, + { + Name: "oom", + RestartCount: 1, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "OOMKilled", + FinishedAt: metav1.NewTime(now.Add(-2 * time.Hour)), + }, + }, + }, + } + + // Status exists + name := "clb" + status := GetExistingContainerStatus(statuses, name) + assert.Equal(t, statuses[0], status) + + // Status does not exist + name = "cla" + status = GetExistingContainerStatus(statuses, name) + assert.Equal(t, v1.ContainerStatus{}, status) +} + +func TestIsPodScheduled(t *testing.T) { + now := time.Now() + pod := ctrltest.NewPod("bar", "pod1", "node1", &ctrltest.NewPodOptions{ + ContainerStatuses: []v1.ContainerStatus{ + { + RestartCount: 10, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "CrashLoopBackOff", + FinishedAt: metav1.NewTime(now.Add(-time.Hour)), + }, + }, + }, + { + RestartCount: 1, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "OOMKilled", + FinishedAt: metav1.NewTime(now.Add(-2 * time.Hour)), + }, + }, + }, + }, + }, + ) + + want := "node1" + got, isScheduled := IsPodScheduled(pod) + assert.Equal(t, want, got) + assert.True(t, isScheduled) + + pod2 := ctrltest.NewPod("bar", "pod2", "", &ctrltest.NewPodOptions{ + ContainerStatuses: []v1.ContainerStatus{ + { + RestartCount: 10, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "CrashLoopBackOff", + FinishedAt: metav1.NewTime(now.Add(-time.Hour)), + }, + }, + }, + { + RestartCount: 1, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "OOMKilled", + FinishedAt: metav1.NewTime(now.Add(-2 * time.Hour)), + }, + }, + }, + }, + }, + ) + got, isScheduled = IsPodScheduled(pod2) + assert.Equal(t, "", got) + assert.False(t, isScheduled) +} + +func TestGetNodeNameFromPod(t *testing.T) { + now := time.Now() + pod := ctrltest.NewPod("bar", "pod1", "node1", &ctrltest.NewPodOptions{ + ContainerStatuses: []v1.ContainerStatus{ + { + RestartCount: 10, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "CrashLoopBackOff", + FinishedAt: metav1.NewTime(now.Add(-time.Hour)), + }, + }, + }, + { + RestartCount: 1, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "OOMKilled", + FinishedAt: metav1.NewTime(now.Add(-2 * time.Hour)), + }, + }, + }, + }, + }, + ) + want := "node1" + got, err := GetNodeNameFromPod(pod) + assert.Equal(t, want, got) + assert.Nil(t, err) + + pod2 := ctrltest.NewPod("bar", "pod2", "", &ctrltest.NewPodOptions{ + ContainerStatuses: []v1.ContainerStatus{ + { + RestartCount: 1, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "OOMKilled", + FinishedAt: metav1.NewTime(now.Add(-2 * time.Hour)), + }, + }, + }, + }, + }, + ) + got, err = GetNodeNameFromPod(pod2) + assert.Equal(t, "", got) + assert.NotNil(t, err) +} + +func TestIsPodReady(t *testing.T) { + pod := ctrltest.NewPod("bar", "pod1", "node1", &ctrltest.NewPodOptions{}) + isReady := IsPodReady(pod) + assert.False(t, isReady) + + pod2 := ctrltest.NewPod("bar", "pod2", "node1", &ctrltest.NewPodOptions{}) + pod2.Status.Conditions = []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + } + isReady = IsPodReady(pod2) + assert.True(t, isReady) +} + +func TestIsCannotStartReason(t *testing.T) { + for _, reason := range cannotStartReasons { + cannotStart := IsCannotStartReason(reason) + assert.True(t, cannotStart) + } + + reason := "ICanStart" + cannotStart := IsCannotStartReason(reason) + assert.False(t, cannotStart) +} + +func TestCannotStart(t *testing.T) { + now := metav1.Now() + pod := newPod(now, true, 5) + cannotStart, reason := CannotStart(pod) + assert.False(t, cannotStart) + assert.Equal(t, datadoghqv1alpha1.ExtendedDaemonSetStatusReasonUnknown, reason) + + pod.Status.ContainerStatuses = []v1.ContainerStatus{ + { + + RestartCount: 10, + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "CrashLoopBackOff", + }, + }, + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "ErrImagePull", + }, + }, + }, + } + cannotStart, reason = CannotStart(pod) + assert.True(t, cannotStart) + assert.Equal(t, datadoghqv1alpha1.ExtendedDaemonSetStatusReasonErrImagePull, reason) +} + +func TestPendingCreate(t *testing.T) { + now := metav1.Now() + pod := newPod(now, true, 5) + isPendingCreate := PendingCreate(pod) + assert.False(t, isPendingCreate) + + pod.Status.ContainerStatuses = []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "ContainerCreating", + }, + }, + }, + } + isPendingCreate = PendingCreate(pod) + assert.True(t, isPendingCreate) +} + +func TestHasPodSchedulerIssue(t *testing.T) { + pod := ctrltest.NewPod("bar", "pod1", "node1", &ctrltest.NewPodOptions{}) + hasIssue := HasPodSchedulerIssue(pod) + assert.False(t, hasIssue) + + // Has scheduler issue because pod creation time was too long ago + pod2 := ctrltest.NewPod("bar", "pod2", "", &ctrltest.NewPodOptions{}) + hasIssue = HasPodSchedulerIssue(pod2) + assert.True(t, hasIssue) + + // Has scheduler issue because pod deletion time was too long ago + pod3 := pod.DeepCopy() + deletionTS := metav1.NewTime(time.Now().Add(-100 * time.Second)) + gracePeriod := int64(10) + pod3.DeletionTimestamp = &deletionTS + pod3.DeletionGracePeriodSeconds = &gracePeriod + hasIssue = HasPodSchedulerIssue(pod3) + assert.True(t, hasIssue) +} + +func TestUpdatePodCondition(t *testing.T) { + status := &v1.PodStatus{ + Conditions: []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + }, + } + condition := &v1.PodCondition{ + Type: v1.PodReady, + Status: v1.ConditionTrue, + } + changed := UpdatePodCondition(status, condition) + // Condition did not change + assert.False(t, changed) + + condition2 := &v1.PodCondition{ + Type: v1.PodReady, + Status: v1.ConditionFalse, + } + changed = UpdatePodCondition(status, condition2) + // Condition changed + assert.True(t, changed) +} + +func TestIsEvicted(t *testing.T) { + status := &v1.PodStatus{ + Conditions: []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + }, + } + isEvicted := IsEvicted(status) + assert.False(t, isEvicted) + + status2 := &v1.PodStatus{ + Phase: v1.PodFailed, + Reason: "Evicted", + } + isEvicted = IsEvicted(status2) + assert.True(t, isEvicted) +} + +func TestSortPodByCreationTime(t *testing.T) { + time1 := time.Now() + time2 := time1.Add(10 * time.Second) + time3 := time2.Add(20 * time.Second) + pod1 := ctrltest.NewPod("bar", "pod1", "node1", &ctrltest.NewPodOptions{ + CreationTimestamp: metav1.NewTime(time1), + }) + + pod2 := ctrltest.NewPod("bar", "pod2", "node1", &ctrltest.NewPodOptions{ + CreationTimestamp: metav1.NewTime(time2), + }) + + pod3 := ctrltest.NewPod("bar", "pod3", "node1", &ctrltest.NewPodOptions{ + CreationTimestamp: metav1.NewTime(time3), + }) + pods := []*v1.Pod{ + pod2, + pod3, + pod1, + } + podList := SortPodByCreationTime(pods) + assert.Equal(t, []*v1.Pod{pod3, pod2, pod1}, podList) +} + +func Test_HighestRestartCount(t *testing.T) { tests := []struct { name string pod *v1.Pod