istio.io/istio@v0.0.0-20240520182934-d79c90f27776/cni/pkg/repair/repair_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package repair
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  
    23  	corev1 "k8s.io/api/core/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	klabels "k8s.io/apimachinery/pkg/labels"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  
    28  	"istio.io/istio/cni/pkg/config"
    29  	"istio.io/istio/pkg/kube"
    30  	"istio.io/istio/pkg/monitoring/monitortest"
    31  	"istio.io/istio/pkg/slices"
    32  	"istio.io/istio/pkg/test"
    33  	"istio.io/istio/pkg/test/util/assert"
    34  	"istio.io/istio/tools/istio-iptables/pkg/constants"
    35  )
    36  
    37  func TestMatchesFilter(t *testing.T) {
    38  	makeDetectPod := func(name string, terminationMessage string, exitCode int) *corev1.Pod {
    39  		return makePod(makePodArgs{
    40  			PodName:     name,
    41  			Annotations: map[string]string{"sidecar.istio.io/status": "something"},
    42  			InitContainerStatus: &corev1.ContainerStatus{
    43  				Name: constants.ValidationContainerName,
    44  				State: corev1.ContainerState{
    45  					Waiting: &corev1.ContainerStateWaiting{
    46  						Reason:  "CrashLoopBackOff",
    47  						Message: "Back-off 5m0s restarting failed blah blah blah",
    48  					},
    49  				},
    50  				LastTerminationState: corev1.ContainerState{
    51  					Terminated: &corev1.ContainerStateTerminated{
    52  						Message:  terminationMessage,
    53  						ExitCode: int32(exitCode),
    54  					},
    55  				},
    56  			},
    57  		})
    58  	}
    59  
    60  	cases := []struct {
    61  		name   string
    62  		config config.RepairConfig
    63  		pod    *corev1.Pod
    64  		want   bool
    65  	}{
    66  		{
    67  			"Testing OK pod with only ExitCode check",
    68  			config.RepairConfig{
    69  				SidecarAnnotation: "sidecar.istio.io/status",
    70  				InitContainerName: constants.ValidationContainerName,
    71  				InitExitCode:      126,
    72  			},
    73  			workingPod,
    74  			false,
    75  		},
    76  		{
    77  			"Testing working pod (that previously died) with only ExitCode check",
    78  			config.RepairConfig{
    79  				SidecarAnnotation: "sidecar.istio.io/status",
    80  				InitContainerName: constants.ValidationContainerName,
    81  				InitExitCode:      126,
    82  			},
    83  			workingPodDiedPreviously,
    84  			false,
    85  		},
    86  		{
    87  			"Testing broken pod (in waiting state) with only ExitCode check",
    88  			config.RepairConfig{
    89  				SidecarAnnotation: "sidecar.istio.io/status",
    90  				InitContainerName: constants.ValidationContainerName,
    91  				InitExitCode:      126,
    92  			},
    93  			brokenPodWaiting,
    94  			true,
    95  		},
    96  		{
    97  			"Testing broken pod (in terminating state) with only ExitCode check",
    98  			config.RepairConfig{
    99  				SidecarAnnotation: "sidecar.istio.io/status",
   100  				InitContainerName: constants.ValidationContainerName,
   101  				InitExitCode:      126,
   102  			},
   103  			brokenPodTerminating,
   104  			true,
   105  		},
   106  		{
   107  			"Testing broken pod with wrong ExitCode",
   108  			config.RepairConfig{
   109  				SidecarAnnotation: "sidecar.istio.io/status",
   110  				InitContainerName: constants.ValidationContainerName,
   111  				InitExitCode:      55,
   112  			},
   113  			brokenPodWaiting,
   114  			false,
   115  		},
   116  		{
   117  			"Testing broken pod with no annotation (should be ignored)",
   118  			config.RepairConfig{
   119  				SidecarAnnotation: "sidecar.istio.io/status",
   120  				InitContainerName: constants.ValidationContainerName,
   121  				InitExitCode:      126,
   122  			},
   123  			brokenPodNoAnnotation,
   124  			false,
   125  		},
   126  		{
   127  			"Check termination message match false",
   128  			config.RepairConfig{
   129  				SidecarAnnotation:  "sidecar.istio.io/status",
   130  				InitContainerName:  constants.ValidationContainerName,
   131  				InitTerminationMsg: "Termination Message",
   132  			},
   133  			makeDetectPod(
   134  				"TerminationMessageMatchFalse",
   135  				"This Does Not Match",
   136  				0),
   137  			false,
   138  		},
   139  		{
   140  			"Check termination message match true",
   141  			config.RepairConfig{
   142  				SidecarAnnotation:  "sidecar.istio.io/status",
   143  				InitContainerName:  constants.ValidationContainerName,
   144  				InitTerminationMsg: "Termination Message",
   145  			},
   146  			makeDetectPod(
   147  				"TerminationMessageMatchTrue",
   148  				"Termination Message",
   149  				0),
   150  			true,
   151  		},
   152  		{
   153  			"Check termination message match true for trailing and leading space",
   154  			config.RepairConfig{
   155  				SidecarAnnotation:  "sidecar.istio.io/status",
   156  				InitContainerName:  constants.ValidationContainerName,
   157  				InitTerminationMsg: "            Termination Message",
   158  			},
   159  			makeDetectPod(
   160  				"TerminationMessageMatchTrueLeadingSpace",
   161  				"Termination Message              ",
   162  				0),
   163  			true,
   164  		},
   165  		{
   166  			"Check termination code match false",
   167  			config.RepairConfig{
   168  				SidecarAnnotation: "sidecar.istio.io/status",
   169  				InitContainerName: constants.ValidationContainerName,
   170  				InitExitCode:      126,
   171  			},
   172  			makeDetectPod(
   173  				"TerminationCodeMatchFalse",
   174  				"",
   175  				121),
   176  			false,
   177  		},
   178  		{
   179  			"Check termination code match true",
   180  			config.RepairConfig{
   181  				SidecarAnnotation: "sidecar.istio.io/status",
   182  				InitContainerName: constants.ValidationContainerName,
   183  				InitExitCode:      126,
   184  			},
   185  			makeDetectPod(
   186  				"TerminationCodeMatchTrue",
   187  				"",
   188  				126),
   189  			true,
   190  		},
   191  		{
   192  			"Check badly formatted pod",
   193  			config.RepairConfig{
   194  				SidecarAnnotation: "sidecar.istio.io/status",
   195  				InitContainerName: constants.ValidationContainerName,
   196  				InitExitCode:      126,
   197  			},
   198  			makePod(makePodArgs{
   199  				PodName:             "Test",
   200  				Annotations:         map[string]string{"sidecar.istio.io/status": "something"},
   201  				InitContainerStatus: &corev1.ContainerStatus{},
   202  			}),
   203  			false,
   204  		},
   205  	}
   206  	for _, tt := range cases {
   207  		t.Run(tt.name, func(t *testing.T) {
   208  			c := &Controller{cfg: tt.config}
   209  			assert.Equal(t, c.matchesFilter(tt.pod), tt.want)
   210  		})
   211  	}
   212  }
   213  
   214  func fakeClient(pods ...*corev1.Pod) kube.Client {
   215  	var csPods []runtime.Object
   216  
   217  	for _, pod := range pods {
   218  		csPods = append(csPods, pod.DeepCopy())
   219  	}
   220  	return kube.NewFakeClient(csPods...)
   221  }
   222  
   223  func makePodLabelMap(pods []*corev1.Pod) (podmap map[string]string) {
   224  	podmap = map[string]string{}
   225  	for _, pod := range pods {
   226  		podmap[pod.Name] = ""
   227  		for key, value := range pod.Labels {
   228  			podmap[pod.Name] = strings.Join([]string{podmap[pod.Name], fmt.Sprintf("%s=%s", key, value)}, ",")
   229  		}
   230  		podmap[pod.Name] = strings.Trim(podmap[pod.Name], " ,")
   231  	}
   232  	return
   233  }
   234  
   235  func TestLabelPods(t *testing.T) {
   236  	tests := []struct {
   237  		name       string
   238  		client     kube.Client
   239  		config     config.RepairConfig
   240  		wantLabels map[string]string
   241  		wantCount  float64
   242  		wantTags   map[string]string
   243  	}{
   244  		{
   245  			name:   "No broken pods",
   246  			client: fakeClient(workingPod, workingPodDiedPreviously),
   247  			config: config.RepairConfig{
   248  				InitContainerName:  constants.ValidationContainerName,
   249  				InitExitCode:       126,
   250  				InitTerminationMsg: "Died for some reason",
   251  				LabelKey:           "testkey",
   252  				LabelValue:         "testval",
   253  			},
   254  			wantLabels: map[string]string{workingPod.Name: "", workingPodDiedPreviously.Name: ""},
   255  			wantCount:  0,
   256  		},
   257  		{
   258  			name:   "With broken pods",
   259  			client: fakeClient(workingPod, workingPodDiedPreviously, brokenPodWaiting),
   260  			config: config.RepairConfig{
   261  				InitContainerName:  constants.ValidationContainerName,
   262  				InitExitCode:       126,
   263  				InitTerminationMsg: "Died for some reason",
   264  				LabelKey:           "testkey",
   265  				LabelValue:         "testval",
   266  			},
   267  			wantLabels: map[string]string{workingPod.Name: "", workingPodDiedPreviously.Name: "", brokenPodWaiting.Name: "testkey=testval"},
   268  			wantCount:  1,
   269  			wantTags:   map[string]string{"result": resultSuccess, "type": labelType},
   270  		},
   271  		{
   272  			name:   "With already labeled pod",
   273  			client: fakeClient(workingPod, workingPodDiedPreviously, brokenPodTerminating),
   274  			config: config.RepairConfig{
   275  				InitContainerName:  constants.ValidationContainerName,
   276  				InitExitCode:       126,
   277  				InitTerminationMsg: "Died for some reason",
   278  				LabelKey:           "testlabel",
   279  				LabelValue:         "true",
   280  			},
   281  			wantLabels: map[string]string{workingPod.Name: "", workingPodDiedPreviously.Name: "", brokenPodTerminating.Name: "testlabel=true"},
   282  			wantCount:  1,
   283  			wantTags:   map[string]string{"result": resultSkip, "type": labelType},
   284  		},
   285  	}
   286  	for _, tt := range tests {
   287  		t.Run(tt.name, func(t *testing.T) {
   288  			mt := monitortest.New(t)
   289  			tt.config.LabelPods = true
   290  			c, err := NewRepairController(tt.client, tt.config)
   291  			assert.NoError(t, err)
   292  			t.Cleanup(func() {
   293  				assert.NoError(t, c.queue.WaitForClose(time.Second))
   294  			})
   295  			stop := test.NewStop(t)
   296  			tt.client.RunAndWait(stop)
   297  			go c.Run(stop)
   298  			kube.WaitForCacheSync("test", stop, c.queue.HasSynced)
   299  
   300  			assert.EventuallyEqual(t, func() map[string]string {
   301  				havePods := c.pods.List(metav1.NamespaceAll, klabels.Everything())
   302  				slices.SortBy(havePods, func(a *corev1.Pod) string {
   303  					return a.Name
   304  				})
   305  				return makePodLabelMap(havePods)
   306  			}, tt.wantLabels)
   307  			if tt.wantCount > 0 {
   308  				mt.Assert(podsRepaired.Name(), tt.wantTags, monitortest.Exactly(tt.wantCount))
   309  			}
   310  		})
   311  	}
   312  }
   313  
   314  func TestDeletePods(t *testing.T) {
   315  	tests := []struct {
   316  		name      string
   317  		client    kube.Client
   318  		config    config.RepairConfig
   319  		wantErr   bool
   320  		wantPods  []*corev1.Pod
   321  		wantCount float64
   322  		wantTags  map[string]string
   323  	}{
   324  		{
   325  			name:   "No broken pods",
   326  			client: fakeClient(workingPod, workingPodDiedPreviously),
   327  			config: config.RepairConfig{
   328  				InitContainerName:  constants.ValidationContainerName,
   329  				InitExitCode:       126,
   330  				InitTerminationMsg: "Died for some reason",
   331  			},
   332  			wantPods:  []*corev1.Pod{workingPod, workingPodDiedPreviously},
   333  			wantErr:   false,
   334  			wantCount: 0,
   335  		},
   336  		{
   337  			name:   "With broken pods",
   338  			client: fakeClient(workingPod, workingPodDiedPreviously, brokenPodWaiting),
   339  			config: config.RepairConfig{
   340  				InitContainerName:  constants.ValidationContainerName,
   341  				InitExitCode:       126,
   342  				InitTerminationMsg: "Died for some reason",
   343  			},
   344  			wantPods:  []*corev1.Pod{workingPod, workingPodDiedPreviously},
   345  			wantErr:   false,
   346  			wantCount: 1,
   347  			wantTags:  map[string]string{"result": resultSuccess, "type": deleteType},
   348  		},
   349  	}
   350  	for _, tt := range tests {
   351  		t.Run(tt.name, func(t *testing.T) {
   352  			mt := monitortest.New(t)
   353  			tt.config.DeletePods = true
   354  			c, err := NewRepairController(tt.client, tt.config)
   355  			assert.NoError(t, err)
   356  			t.Cleanup(func() {
   357  				assert.NoError(t, c.queue.WaitForClose(time.Second))
   358  			})
   359  			stop := test.NewStop(t)
   360  			tt.client.RunAndWait(stop)
   361  			go c.Run(stop)
   362  			kube.WaitForCacheSync("test", stop, c.queue.HasSynced)
   363  
   364  			assert.EventuallyEqual(t, func() []*corev1.Pod {
   365  				havePods := c.pods.List(metav1.NamespaceAll, klabels.Everything())
   366  				slices.SortBy(havePods, func(a *corev1.Pod) string {
   367  					return a.Name
   368  				})
   369  				return havePods
   370  			}, tt.wantPods)
   371  			if tt.wantCount > 0 {
   372  				mt.Assert(podsRepaired.Name(), tt.wantTags, monitortest.Exactly(tt.wantCount))
   373  			}
   374  		})
   375  	}
   376  }