From 52c71f846bcfb0e839fc8c799e8be3929b8304ec Mon Sep 17 00:00:00 2001 From: Junguk Cho Date: Thu, 4 Nov 2021 11:09:55 -0700 Subject: [PATCH 1/6] Support graceful shutdown in pod --- npm/pkg/controlplane/controllers/v1/podController.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/npm/pkg/controlplane/controllers/v1/podController.go b/npm/pkg/controlplane/controllers/v1/podController.go index f314a922db..4557136652 100644 --- a/npm/pkg/controlplane/controllers/v1/podController.go +++ b/npm/pkg/controlplane/controllers/v1/podController.go @@ -324,7 +324,9 @@ func (c *PodController) syncPod(key string) error { return err } - // If newPodObj status is either corev1.PodSucceeded or corev1.PodFailed or DeletionTimestamp is set, start clean-up the lastly applied states. + // TODO(jungukcho): update.. + // If newPodObj status is either corev1.PodSucceeded or corev1.PodFailed or DeletionTimestamp is set, + // start clean-up the lastly applied states. if isCompletePod(pod) { if err = c.cleanUpDeletedPod(key); err != nil { return fmt.Errorf("Error: %v when when pod is in completed state.\n", err) @@ -591,7 +593,7 @@ func (c *PodController) manageNamedPortIpsets(portList []corev1.ContainerPort, p } func isCompletePod(podObj *corev1.Pod) bool { - if podObj.DeletionTimestamp != nil { + if podObj.DeletionTimestamp != nil && podObj.DeletionGracePeriodSeconds != nil && *podObj.DeletionGracePeriodSeconds == 0 { return true } @@ -625,8 +627,9 @@ func isInvalidPodUpdate(npmPod *NpmPod, newPodObj *corev1.Pod) bool { npmPod.Name == newPodObj.ObjectMeta.Name && npmPod.Phase == newPodObj.Status.Phase && npmPod.PodIP == newPodObj.Status.PodIP && - newPodObj.ObjectMeta.DeletionTimestamp == nil && - newPodObj.ObjectMeta.DeletionGracePeriodSeconds == nil && + // TODO(jungukcho): it seems it is not needed. + // newPodObj.ObjectMeta.DeletionGracePeriodSeconds != nil && + // *newPodObj.ObjectMeta.DeletionGracePeriodSeconds != 0 && reflect.DeepEqual(npmPod.Labels, newPodObj.ObjectMeta.Labels) && reflect.DeepEqual(npmPod.ContainerPorts, getContainerPortList(newPodObj)) } From b8a9a67f856928d93b9b7cbf6e6af5efcb6ac2e2 Mon Sep 17 00:00:00 2001 From: Junguk Cho Date: Thu, 4 Nov 2021 14:44:25 -0700 Subject: [PATCH 2/6] Update detailed comments and cleaning up codes --- .../controllers/v1/podController.go | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/npm/pkg/controlplane/controllers/v1/podController.go b/npm/pkg/controlplane/controllers/v1/podController.go index 4557136652..a302c8d251 100644 --- a/npm/pkg/controlplane/controllers/v1/podController.go +++ b/npm/pkg/controlplane/controllers/v1/podController.go @@ -82,6 +82,16 @@ func (nPod *NpmPod) updateNpmPodAttributes(podObj *corev1.Pod) { } } +// noUpdate evaluates whether NpmPod is required to be update given podObj. +func (nPod *NpmPod) noUpdate(podObj *corev1.Pod) bool { + return nPod.Namespace == podObj.ObjectMeta.Namespace && + nPod.Name == podObj.ObjectMeta.Name && + nPod.Phase == podObj.Status.Phase && + nPod.PodIP == podObj.Status.PodIP && + reflect.DeepEqual(nPod.Labels, podObj.ObjectMeta.Labels) && + reflect.DeepEqual(nPod.ContainerPorts, getContainerPortList(podObj)) +} + type PodController struct { podLister corelisters.PodLister workqueue workqueue.RateLimitingInterface @@ -168,7 +178,8 @@ func (c *PodController) addPod(obj interface{}) { } podObj, _ := obj.(*corev1.Pod) - // If newPodObj status is either corev1.PodSucceeded or corev1.PodFailed or DeletionTimestamp is set, do not need to add it into workqueue. + // To check whether this pod is needed to queue or not. + // If the pod are in completely terminated states, the pod is not enqueued to avoid unnecessary computation. if isCompletePod(podObj) { return } @@ -324,9 +335,9 @@ func (c *PodController) syncPod(key string) error { return err } - // TODO(jungukcho): update.. - // If newPodObj status is either corev1.PodSucceeded or corev1.PodFailed or DeletionTimestamp is set, - // start clean-up the lastly applied states. + // if this pod is completely in terminated states (which means pod is gracefully shutdown), + // NPM starts clean-up the lastly applied states even in update events. + // This proactive clean-up helps to miss stale pod object in case delete event is missed. if isCompletePod(pod) { if err = c.cleanUpDeletedPod(key); err != nil { return fmt.Errorf("Error: %v when when pod is in completed state.\n", err) @@ -339,7 +350,7 @@ func (c *PodController) syncPod(key string) error { // if pod does not have different states against lastly applied states stored in cachedNpmPod, // podController does not need to reconcile this update. // in this updatePod event, newPod was updated with states which PodController does not need to reconcile. - if isInvalidPodUpdate(cachedNpmPod, pod) { + if cachedNpmPod.noUpdate(pod) { return nil } } @@ -592,12 +603,17 @@ func (c *PodController) manageNamedPortIpsets(portList []corev1.ContainerPort, p return nil } +// isCompletePod evaluates whether this pod is completely in terminated states, +// which means pod is gracefully shutdown. func isCompletePod(podObj *corev1.Pod) bool { + // DeletionTimestamp and DeletionGracePeriodSeconds in pod are not nil, + // which means pod is expected to be deleted and + // DeletionGracePeriodSeconds value is zero, which means the pod is gracefully terminated. if podObj.DeletionTimestamp != nil && podObj.DeletionGracePeriodSeconds != nil && *podObj.DeletionGracePeriodSeconds == 0 { return true } - // K8s categorizes Succeeded and Failed pods as a terminated pod and will not restart them + // K8s categorizes Succeeded and Failed pods as a terminated pod and will not restart them. // So NPM will ignorer adding these pods if podObj.Status.Phase == corev1.PodSucceeded || podObj.Status.Phase == corev1.PodFailed { return true @@ -620,16 +636,3 @@ func getContainerPortList(podObj *corev1.Pod) []corev1.ContainerPort { } return portList } - -// (TODO): better naming? -func isInvalidPodUpdate(npmPod *NpmPod, newPodObj *corev1.Pod) bool { - return npmPod.Namespace == newPodObj.ObjectMeta.Namespace && - npmPod.Name == newPodObj.ObjectMeta.Name && - npmPod.Phase == newPodObj.Status.Phase && - npmPod.PodIP == newPodObj.Status.PodIP && - // TODO(jungukcho): it seems it is not needed. - // newPodObj.ObjectMeta.DeletionGracePeriodSeconds != nil && - // *newPodObj.ObjectMeta.DeletionGracePeriodSeconds != 0 && - reflect.DeepEqual(npmPod.Labels, newPodObj.ObjectMeta.Labels) && - reflect.DeepEqual(npmPod.ContainerPorts, getContainerPortList(newPodObj)) -} From 08a4594a5bba74c2c97872697af124afa849d1fd Mon Sep 17 00:00:00 2001 From: Junguk Cho Date: Fri, 5 Nov 2021 11:31:54 -0700 Subject: [PATCH 3/6] Add Unit tests --- .../controllers/v1/podController.go | 2 + .../controllers/v1/podController_test.go | 152 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/npm/pkg/controlplane/controllers/v1/podController.go b/npm/pkg/controlplane/controllers/v1/podController.go index a302c8d251..2c6368ec92 100644 --- a/npm/pkg/controlplane/controllers/v1/podController.go +++ b/npm/pkg/controlplane/controllers/v1/podController.go @@ -615,6 +615,8 @@ func isCompletePod(podObj *corev1.Pod) bool { // K8s categorizes Succeeded and Failed pods as a terminated pod and will not restart them. // So NPM will ignorer adding these pods + // TODO(jungukcho): what are the values of DeletionTimestamp and podObj.DeletionGracePeriodSeconds + // in either below status? if podObj.Status.Phase == corev1.PodSucceeded || podObj.Status.Phase == corev1.PodFailed { return true } diff --git a/npm/pkg/controlplane/controllers/v1/podController_test.go b/npm/pkg/controlplane/controllers/v1/podController_test.go index 948724cd12..a7938d9173 100644 --- a/npm/pkg/controlplane/controllers/v1/podController_test.go +++ b/npm/pkg/controlplane/controllers/v1/podController_test.go @@ -12,6 +12,7 @@ import ( "github.com/Azure/azure-container-networking/npm/util" testutils "github.com/Azure/azure-container-networking/test/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -690,6 +691,89 @@ func TestHasValidPodIP(t *testing.T) { } } +func TestIsCompletePod(t *testing.T) { + var zeroGracePeriod int64 = 0 + var defaultGracePeriod int64 = 30 + + type podState struct { + Phase corev1.PodPhase + DeletionTimestamp *metav1.Time + DeletionGracePeriodSeconds *int64 + } + + tests := []struct { + name string + podState podState + expectedCompletedPod bool + }{ + + { + name: "pod is in running status", + podState: podState{ + Phase: corev1.PodRunning, + DeletionTimestamp: nil, + DeletionGracePeriodSeconds: nil, + }, + expectedCompletedPod: false, + }, + { + name: "pod is in completely terminating states after graceful shutdown period", + podState: podState{ + Phase: corev1.PodRunning, + DeletionTimestamp: &metav1.Time{}, + DeletionGracePeriodSeconds: &zeroGracePeriod, + }, + expectedCompletedPod: true, + }, + { + name: "pod is in terminating states, but in graceful shutdown period", + podState: podState{ + Phase: corev1.PodRunning, + DeletionTimestamp: &metav1.Time{}, + DeletionGracePeriodSeconds: &defaultGracePeriod, + }, + expectedCompletedPod: false, + }, + { + name: "pod is in PodSucceeded status", + podState: podState{ + Phase: corev1.PodSucceeded, + DeletionTimestamp: nil, + DeletionGracePeriodSeconds: nil, + }, + expectedCompletedPod: true, + }, + { + name: "pod is in PodFailed status", + podState: podState{ + Phase: corev1.PodSucceeded, + DeletionTimestamp: nil, + DeletionGracePeriodSeconds: nil, + }, + expectedCompletedPod: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + corev1Pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: tt.podState.DeletionTimestamp, + DeletionGracePeriodSeconds: tt.podState.DeletionGracePeriodSeconds, + }, + Status: corev1.PodStatus{ + Phase: tt.podState.Phase, + }, + } + isPodCompleted := isCompletePod(corev1Pod) + require.Equal(t, tt.expectedCompletedPod, isPodCompleted) + + }) + } +} + // Extra unit test which is not quite related to PodController, // but help to understand how workqueue works to make event handler logic lock-free. // If the same key are queued into workqueue in multiple times, @@ -721,3 +805,71 @@ func TestWorkQueue(t *testing.T) { } } } + +func TestNPMPodNoUpdate(t *testing.T) { + type podInfo struct { + podName string + ns string + rv string + podIP string + labels map[string]string + isHostNewtwork bool + podPhase corev1.PodPhase + } + + labels := map[string]string{ + "app": "test-pod", + } + + tests := []struct { + name string + podInfo + updatingNPMPod bool + expectedNoUpdate bool + }{ + { + "Required update of NPMPod given Pod", + podInfo{ + podName: "test-pod-1", + ns: "test-namespace", + rv: "0", + podIP: "1.2.3.4", + labels: labels, + isHostNewtwork: NonHostNetwork, + podPhase: corev1.PodRunning, + }, + false, + false, + }, + { + "No required update of NPMPod given Pod", + podInfo{ + podName: "test-pod-2", + ns: "test-namespace", + rv: "0", + podIP: "1.2.3.4", + labels: labels, + isHostNewtwork: NonHostNetwork, + podPhase: corev1.PodRunning, + }, + true, + true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + corev1Pod := createPod(tt.podName, tt.ns, tt.rv, tt.podIP, tt.labels, tt.isHostNewtwork, tt.podPhase) + npmPod := newNpmPod(corev1Pod) + if tt.updatingNPMPod { + npmPod.appendLabels(corev1Pod.Labels, AppendToExistingLabels) + npmPod.updateNpmPodAttributes(corev1Pod) + npmPod.appendContainerPorts(corev1Pod) + } + noUpdate := npmPod.noUpdate(corev1Pod) + require.Equal(t, tt.expectedNoUpdate, noUpdate) + }) + } +} From f0ea128c5f5324e541e2841baa9307f0d36fa50c Mon Sep 17 00:00:00 2001 From: Junguk Cho Date: Fri, 5 Nov 2021 11:39:53 -0700 Subject: [PATCH 4/6] Address lint errors --- npm/pkg/controlplane/controllers/v1/podController_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/npm/pkg/controlplane/controllers/v1/podController_test.go b/npm/pkg/controlplane/controllers/v1/podController_test.go index a7938d9173..52688e10c1 100644 --- a/npm/pkg/controlplane/controllers/v1/podController_test.go +++ b/npm/pkg/controlplane/controllers/v1/podController_test.go @@ -692,7 +692,7 @@ func TestHasValidPodIP(t *testing.T) { } func TestIsCompletePod(t *testing.T) { - var zeroGracePeriod int64 = 0 + var zeroGracePeriod int64 var defaultGracePeriod int64 = 30 type podState struct { @@ -769,7 +769,6 @@ func TestIsCompletePod(t *testing.T) { } isPodCompleted := isCompletePod(corev1Pod) require.Equal(t, tt.expectedCompletedPod, isPodCompleted) - }) } } From d0414a96beef6865a1eaeedc1a3ab0f32332675d Mon Sep 17 00:00:00 2001 From: Junguk Cho Date: Mon, 8 Nov 2021 16:58:59 -0800 Subject: [PATCH 5/6] Address comments --- .../controllers/v1/podController_test.go | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/npm/pkg/controlplane/controllers/v1/podController_test.go b/npm/pkg/controlplane/controllers/v1/podController_test.go index 52688e10c1..bfd4c641bc 100644 --- a/npm/pkg/controlplane/controllers/v1/podController_test.go +++ b/npm/pkg/controlplane/controllers/v1/podController_test.go @@ -87,7 +87,7 @@ func (f *podFixture) newPodController(stopCh chan struct{}) { // f.kubeInformer.Start(stopCh) } -func createPod(name, ns, rv, podIP string, labels map[string]string, isHostNewtwork bool, podPhase corev1.PodPhase) *corev1.Pod { +func createPod(name, ns, rv, podIP string, labels map[string]string, isHostNetwork bool, podPhase corev1.PodPhase) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -96,7 +96,7 @@ func createPod(name, ns, rv, podIP string, labels map[string]string, isHostNewtw ResourceVersion: rv, }, Spec: corev1.PodSpec{ - HostNetwork: isHostNewtwork, + HostNetwork: isHostNetwork, Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ @@ -696,9 +696,9 @@ func TestIsCompletePod(t *testing.T) { var defaultGracePeriod int64 = 30 type podState struct { - Phase corev1.PodPhase - DeletionTimestamp *metav1.Time - DeletionGracePeriodSeconds *int64 + phase corev1.PodPhase + deletionTimestamp *metav1.Time + deletionGracePeriodSeconds *int64 } tests := []struct { @@ -710,45 +710,45 @@ func TestIsCompletePod(t *testing.T) { { name: "pod is in running status", podState: podState{ - Phase: corev1.PodRunning, - DeletionTimestamp: nil, - DeletionGracePeriodSeconds: nil, + phase: corev1.PodRunning, + deletionTimestamp: nil, + deletionGracePeriodSeconds: nil, }, expectedCompletedPod: false, }, { name: "pod is in completely terminating states after graceful shutdown period", podState: podState{ - Phase: corev1.PodRunning, - DeletionTimestamp: &metav1.Time{}, - DeletionGracePeriodSeconds: &zeroGracePeriod, + phase: corev1.PodRunning, + deletionTimestamp: &metav1.Time{}, + deletionGracePeriodSeconds: &zeroGracePeriod, }, expectedCompletedPod: true, }, { name: "pod is in terminating states, but in graceful shutdown period", podState: podState{ - Phase: corev1.PodRunning, - DeletionTimestamp: &metav1.Time{}, - DeletionGracePeriodSeconds: &defaultGracePeriod, + phase: corev1.PodRunning, + deletionTimestamp: &metav1.Time{}, + deletionGracePeriodSeconds: &defaultGracePeriod, }, expectedCompletedPod: false, }, { name: "pod is in PodSucceeded status", podState: podState{ - Phase: corev1.PodSucceeded, - DeletionTimestamp: nil, - DeletionGracePeriodSeconds: nil, + phase: corev1.PodSucceeded, + deletionTimestamp: nil, + deletionGracePeriodSeconds: nil, }, expectedCompletedPod: true, }, { name: "pod is in PodFailed status", podState: podState{ - Phase: corev1.PodSucceeded, - DeletionTimestamp: nil, - DeletionGracePeriodSeconds: nil, + phase: corev1.PodSucceeded, + deletionTimestamp: nil, + deletionGracePeriodSeconds: nil, }, expectedCompletedPod: true, }, @@ -760,11 +760,11 @@ func TestIsCompletePod(t *testing.T) { t.Parallel() corev1Pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - DeletionTimestamp: tt.podState.DeletionTimestamp, - DeletionGracePeriodSeconds: tt.podState.DeletionGracePeriodSeconds, + DeletionTimestamp: tt.podState.deletionTimestamp, + DeletionGracePeriodSeconds: tt.podState.deletionGracePeriodSeconds, }, Status: corev1.PodStatus{ - Phase: tt.podState.Phase, + Phase: tt.podState.phase, }, } isPodCompleted := isCompletePod(corev1Pod) @@ -807,13 +807,13 @@ func TestWorkQueue(t *testing.T) { func TestNPMPodNoUpdate(t *testing.T) { type podInfo struct { - podName string - ns string - rv string - podIP string - labels map[string]string - isHostNewtwork bool - podPhase corev1.PodPhase + podName string + ns string + rv string + podIP string + labels map[string]string + isHostNetwork bool + podPhase corev1.PodPhase } labels := map[string]string{ @@ -829,13 +829,13 @@ func TestNPMPodNoUpdate(t *testing.T) { { "Required update of NPMPod given Pod", podInfo{ - podName: "test-pod-1", - ns: "test-namespace", - rv: "0", - podIP: "1.2.3.4", - labels: labels, - isHostNewtwork: NonHostNetwork, - podPhase: corev1.PodRunning, + podName: "test-pod-1", + ns: "test-namespace", + rv: "0", + podIP: "1.2.3.4", + labels: labels, + isHostNetwork: NonHostNetwork, + podPhase: corev1.PodRunning, }, false, false, @@ -843,13 +843,13 @@ func TestNPMPodNoUpdate(t *testing.T) { { "No required update of NPMPod given Pod", podInfo{ - podName: "test-pod-2", - ns: "test-namespace", - rv: "0", - podIP: "1.2.3.4", - labels: labels, - isHostNewtwork: NonHostNetwork, - podPhase: corev1.PodRunning, + podName: "test-pod-2", + ns: "test-namespace", + rv: "0", + podIP: "1.2.3.4", + labels: labels, + isHostNetwork: NonHostNetwork, + podPhase: corev1.PodRunning, }, true, true, @@ -860,7 +860,7 @@ func TestNPMPodNoUpdate(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - corev1Pod := createPod(tt.podName, tt.ns, tt.rv, tt.podIP, tt.labels, tt.isHostNewtwork, tt.podPhase) + corev1Pod := createPod(tt.podName, tt.ns, tt.rv, tt.podIP, tt.labels, tt.isHostNetwork, tt.podPhase) npmPod := newNpmPod(corev1Pod) if tt.updatingNPMPod { npmPod.appendLabels(corev1Pod.Labels, AppendToExistingLabels) From a22f21b8a9fc72db6527e0e356969f0a0c927922 Mon Sep 17 00:00:00 2001 From: Junguk Cho Date: Tue, 9 Nov 2021 11:07:10 -0800 Subject: [PATCH 6/6] Addressed comment and add UTs --- .../controllers/v1/podController.go | 4 +- npm/util/util.go | 15 +++ npm/util/util_test.go | 106 ++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/npm/pkg/controlplane/controllers/v1/podController.go b/npm/pkg/controlplane/controllers/v1/podController.go index 2c6368ec92..c3b8acf414 100644 --- a/npm/pkg/controlplane/controllers/v1/podController.go +++ b/npm/pkg/controlplane/controllers/v1/podController.go @@ -88,7 +88,9 @@ func (nPod *NpmPod) noUpdate(podObj *corev1.Pod) bool { nPod.Name == podObj.ObjectMeta.Name && nPod.Phase == podObj.Status.Phase && nPod.PodIP == podObj.Status.PodIP && - reflect.DeepEqual(nPod.Labels, podObj.ObjectMeta.Labels) && + util.IsSameLabels(nPod.Labels, podObj.ObjectMeta.Labels) && + // TODO(jungukcho) to avoid using DeepEqual for ContainerPorts, + // it needs a precise sorting. Will optimize it later if needed. reflect.DeepEqual(nPod.ContainerPorts, getContainerPortList(podObj)) } diff --git a/npm/util/util.go b/npm/util/util.go index ea56410e27..7ac3801067 100644 --- a/npm/util/util.go +++ b/npm/util/util.go @@ -334,3 +334,18 @@ func CompareSlices(list1, list2 []string) bool { func SliceToString(list []string) string { return strings.Join(list, SetPolicyDelimiter) } + +// IsSameLabels return if all pairs of key and value in two maps are same. +// Otherwise, it returns false. +func IsSameLabels(labelA, labelB map[string]string) bool { + if len(labelA) != len(labelB) { + return false + } + + for labelKey, labelVal := range labelA { + if val, exist := labelB[labelKey]; !exist || labelVal != val { + return false + } + } + return true +} diff --git a/npm/util/util_test.go b/npm/util/util_test.go index 343e9da03e..4d8b8a7a18 100644 --- a/npm/util/util_test.go +++ b/npm/util/util_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/version" ) @@ -323,3 +324,108 @@ func TestCompareSlices(t *testing.T) { t.Errorf("TestCompareSlices failed @ slice comparison 4") } } + +func TestIsSameLabels(t *testing.T) { + var nilLabel map[string]string + tests := []struct { + name string + labelA map[string]string + labelB map[string]string + expectedIsSameLabel bool + }{ + { + name: "Empty labels", + labelA: map[string]string{}, + labelB: map[string]string{}, + expectedIsSameLabel: true, + }, + { + name: "Empty label and Nil label", + labelA: map[string]string{}, + labelB: nilLabel, + expectedIsSameLabel: true, + }, + { + name: "Same labels", + labelA: map[string]string{ + "e": "f", + "c": "d", + "a": "b", + }, + labelB: map[string]string{ + "e": "f", + "c": "d", + "a": "b", + }, + expectedIsSameLabel: true, + }, + { + name: "Same labels with different ordered addition", + labelA: map[string]string{ + "e": "f", + "c": "d", + "a": "b", + }, + labelB: map[string]string{ + "c": "d", + "e": "f", + "a": "b", + }, + expectedIsSameLabel: true, + }, + { + name: "Different length", + labelA: map[string]string{ + "e": "f", + }, + labelB: map[string]string{ + "e": "f", + "a": "b", + }, + expectedIsSameLabel: false, + }, + { + name: "Different (empty map and non-empty map)", + labelA: map[string]string{}, + labelB: map[string]string{ + "e": "f", + "c": "d", + "a": "b", + }, + expectedIsSameLabel: false, + }, + { + name: "Different (nil map and non-empty map)", + labelA: nilLabel, + labelB: map[string]string{ + "e": "f", + "c": "d", + "a": "b", + }, + expectedIsSameLabel: false, + }, + { + name: "Have a different one pair of key and value", + labelA: map[string]string{ + "e": "f", + "d": "c", + "a": "b", + }, + labelB: map[string]string{ + "e": "f", + "c": "d", + "a": "b", + }, + expectedIsSameLabel: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := IsSameLabels(tt.labelA, tt.labelB) + require.Equal(t, tt.expectedIsSameLabel, got) + }) + } +}