1
16
17 package kuberuntime
18
19 import (
20 "context"
21 "os"
22 "path/filepath"
23 "testing"
24 "time"
25
26 "github.com/golang/mock/gomock"
27 "github.com/stretchr/testify/assert"
28 v1 "k8s.io/api/core/v1"
29 "k8s.io/apimachinery/pkg/types"
30 runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
31 kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
32 containertest "k8s.io/kubernetes/pkg/kubelet/container/testing"
33 )
34
35 func TestSandboxGC(t *testing.T) {
36 fakeRuntime, _, m, err := createTestRuntimeManager()
37 assert.NoError(t, err)
38
39 podStateProvider := m.containerGC.podStateProvider.(*fakePodStateProvider)
40 makeGCSandbox := func(pod *v1.Pod, attempt uint32, state runtimeapi.PodSandboxState, hasRunningContainers, isTerminating bool, createdAt int64) sandboxTemplate {
41 return sandboxTemplate{
42 pod: pod,
43 state: state,
44 attempt: attempt,
45 createdAt: createdAt,
46 running: hasRunningContainers,
47 terminating: isTerminating,
48 }
49 }
50
51 pods := []*v1.Pod{
52 makeTestPod("foo1", "new", "1234", []v1.Container{
53 makeTestContainer("bar1", "busybox"),
54 makeTestContainer("bar2", "busybox"),
55 }),
56 makeTestPod("foo2", "new", "5678", []v1.Container{
57 makeTestContainer("bar3", "busybox"),
58 }),
59 makeTestPod("deleted", "new", "9012", []v1.Container{
60 makeTestContainer("bar4", "busybox"),
61 }),
62 }
63
64 for _, test := range []struct {
65 description string
66 sandboxes []sandboxTemplate
67 containers []containerTemplate
68 remain []int
69 evictTerminatingPods bool
70 }{
71 {
72 description: "notready sandboxes without containers for deleted pods should be garbage collected.",
73 sandboxes: []sandboxTemplate{
74 makeGCSandbox(pods[2], 0, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, false, false, 0),
75 },
76 containers: []containerTemplate{},
77 remain: []int{},
78 evictTerminatingPods: false,
79 },
80 {
81 description: "ready sandboxes without containers for deleted pods should not be garbage collected.",
82 sandboxes: []sandboxTemplate{
83 makeGCSandbox(pods[2], 0, runtimeapi.PodSandboxState_SANDBOX_READY, false, false, 0),
84 },
85 containers: []containerTemplate{},
86 remain: []int{0},
87 evictTerminatingPods: false,
88 },
89 {
90 description: "sandboxes for existing pods should not be garbage collected.",
91 sandboxes: []sandboxTemplate{
92 makeGCSandbox(pods[0], 0, runtimeapi.PodSandboxState_SANDBOX_READY, true, false, 0),
93 makeGCSandbox(pods[1], 0, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, true, false, 0),
94 },
95 containers: []containerTemplate{},
96 remain: []int{0, 1},
97 evictTerminatingPods: false,
98 },
99 {
100 description: "older exited sandboxes without containers for existing pods should be garbage collected if there are more than one exited sandboxes.",
101 sandboxes: []sandboxTemplate{
102 makeGCSandbox(pods[0], 1, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, true, false, 1),
103 makeGCSandbox(pods[0], 0, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, true, false, 0),
104 },
105 containers: []containerTemplate{},
106 remain: []int{0},
107 evictTerminatingPods: false,
108 },
109 {
110 description: "older exited sandboxes with containers for existing pods should not be garbage collected even if there are more than one exited sandboxes.",
111 sandboxes: []sandboxTemplate{
112 makeGCSandbox(pods[0], 1, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, true, false, 1),
113 makeGCSandbox(pods[0], 0, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, true, false, 0),
114 },
115 containers: []containerTemplate{
116 {pod: pods[0], container: &pods[0].Spec.Containers[0], sandboxAttempt: 0, state: runtimeapi.ContainerState_CONTAINER_EXITED},
117 },
118 remain: []int{0, 1},
119 evictTerminatingPods: false,
120 },
121 {
122 description: "non-running sandboxes for existing pods should be garbage collected if evictTerminatingPods is set.",
123 sandboxes: []sandboxTemplate{
124 makeGCSandbox(pods[0], 0, runtimeapi.PodSandboxState_SANDBOX_READY, true, true, 0),
125 makeGCSandbox(pods[1], 0, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, true, true, 0),
126 },
127 containers: []containerTemplate{},
128 remain: []int{0},
129 evictTerminatingPods: true,
130 },
131 {
132 description: "sandbox with containers should not be garbage collected.",
133 sandboxes: []sandboxTemplate{
134 makeGCSandbox(pods[0], 0, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, false, false, 0),
135 },
136 containers: []containerTemplate{
137 {pod: pods[0], container: &pods[0].Spec.Containers[0], state: runtimeapi.ContainerState_CONTAINER_EXITED},
138 },
139 remain: []int{0},
140 evictTerminatingPods: false,
141 },
142 {
143 description: "multiple sandboxes should be handled properly.",
144 sandboxes: []sandboxTemplate{
145
146 makeGCSandbox(pods[0], 1, runtimeapi.PodSandboxState_SANDBOX_READY, true, false, 1),
147
148 makeGCSandbox(pods[0], 0, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, true, false, 0),
149
150 makeGCSandbox(pods[1], 1, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, true, false, 1),
151
152 makeGCSandbox(pods[1], 0, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, true, false, 0),
153
154 makeGCSandbox(pods[2], 0, runtimeapi.PodSandboxState_SANDBOX_NOTREADY, false, true, 0),
155 },
156 containers: []containerTemplate{
157 {pod: pods[1], container: &pods[1].Spec.Containers[0], sandboxAttempt: 1, state: runtimeapi.ContainerState_CONTAINER_EXITED},
158 },
159 remain: []int{0, 2},
160 evictTerminatingPods: false,
161 },
162 } {
163 t.Run(test.description, func(t *testing.T) {
164 ctx := context.Background()
165 podStateProvider.removed = make(map[types.UID]struct{})
166 podStateProvider.terminated = make(map[types.UID]struct{})
167 fakeSandboxes := makeFakePodSandboxes(t, m, test.sandboxes)
168 fakeContainers := makeFakeContainers(t, m, test.containers)
169 for _, s := range test.sandboxes {
170 if !s.running && s.pod.Name == "deleted" {
171 podStateProvider.removed[s.pod.UID] = struct{}{}
172 }
173 if s.terminating {
174 podStateProvider.terminated[s.pod.UID] = struct{}{}
175 }
176 }
177 fakeRuntime.SetFakeSandboxes(fakeSandboxes)
178 fakeRuntime.SetFakeContainers(fakeContainers)
179
180 err := m.containerGC.evictSandboxes(ctx, test.evictTerminatingPods)
181 assert.NoError(t, err)
182 realRemain, err := fakeRuntime.ListPodSandbox(ctx, nil)
183 assert.NoError(t, err)
184 assert.Len(t, realRemain, len(test.remain))
185 for _, remain := range test.remain {
186 resp, err := fakeRuntime.PodSandboxStatus(ctx, fakeSandboxes[remain].Id, false)
187 assert.NoError(t, err)
188 assert.Equal(t, &fakeSandboxes[remain].PodSandboxStatus, resp.Status)
189 }
190 })
191 }
192 }
193
194 func makeGCContainer(podName, containerName string, attempt int, createdAt int64, state runtimeapi.ContainerState) containerTemplate {
195 container := makeTestContainer(containerName, "test-image")
196 pod := makeTestPod(podName, "test-ns", podName, []v1.Container{container})
197 return containerTemplate{
198 pod: pod,
199 container: &container,
200 attempt: attempt,
201 createdAt: createdAt,
202 state: state,
203 }
204 }
205
206 func TestContainerGC(t *testing.T) {
207 fakeRuntime, _, m, err := createTestRuntimeManager()
208 assert.NoError(t, err)
209
210 podStateProvider := m.containerGC.podStateProvider.(*fakePodStateProvider)
211 defaultGCPolicy := kubecontainer.GCPolicy{MinAge: time.Hour, MaxPerPodContainer: 2, MaxContainers: 6}
212
213 for _, test := range []struct {
214 description string
215 containers []containerTemplate
216 policy *kubecontainer.GCPolicy
217 remain []int
218 evictTerminatingPods bool
219 allSourcesReady bool
220 }{
221 {
222 description: "all containers should be removed when max container limit is 0",
223 containers: []containerTemplate{
224 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
225 },
226 policy: &kubecontainer.GCPolicy{MinAge: time.Minute, MaxPerPodContainer: 1, MaxContainers: 0},
227 remain: []int{},
228 evictTerminatingPods: false,
229 allSourcesReady: true,
230 },
231 {
232 description: "max containers should be complied when no max per pod container limit is set",
233 containers: []containerTemplate{
234 makeGCContainer("foo", "bar", 4, 4, runtimeapi.ContainerState_CONTAINER_EXITED),
235 makeGCContainer("foo", "bar", 3, 3, runtimeapi.ContainerState_CONTAINER_EXITED),
236 makeGCContainer("foo", "bar", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
237 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
238 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
239 },
240 policy: &kubecontainer.GCPolicy{MinAge: time.Minute, MaxPerPodContainer: -1, MaxContainers: 4},
241 remain: []int{0, 1, 2, 3},
242 evictTerminatingPods: false,
243 allSourcesReady: true,
244 },
245 {
246 description: "no containers should be removed if both max container and per pod container limits are not set",
247 containers: []containerTemplate{
248 makeGCContainer("foo", "bar", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
249 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
250 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
251 },
252 policy: &kubecontainer.GCPolicy{MinAge: time.Minute, MaxPerPodContainer: -1, MaxContainers: -1},
253 remain: []int{0, 1, 2},
254 evictTerminatingPods: false,
255 allSourcesReady: true,
256 },
257 {
258 description: "recently started containers should not be removed",
259 containers: []containerTemplate{
260 makeGCContainer("foo", "bar", 2, time.Now().UnixNano(), runtimeapi.ContainerState_CONTAINER_EXITED),
261 makeGCContainer("foo", "bar", 1, time.Now().UnixNano(), runtimeapi.ContainerState_CONTAINER_EXITED),
262 makeGCContainer("foo", "bar", 0, time.Now().UnixNano(), runtimeapi.ContainerState_CONTAINER_EXITED),
263 },
264 remain: []int{0, 1, 2},
265 evictTerminatingPods: false,
266 allSourcesReady: true,
267 },
268 {
269 description: "oldest containers should be removed when per pod container limit exceeded",
270 containers: []containerTemplate{
271 makeGCContainer("foo", "bar", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
272 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
273 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
274 },
275 remain: []int{0, 1},
276 evictTerminatingPods: false,
277 allSourcesReady: true,
278 },
279 {
280 description: "running containers should not be removed",
281 containers: []containerTemplate{
282 makeGCContainer("foo", "bar", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
283 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
284 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_RUNNING),
285 },
286 remain: []int{0, 1, 2},
287 evictTerminatingPods: false,
288 allSourcesReady: true,
289 },
290 {
291 description: "no containers should be removed when limits are not exceeded",
292 containers: []containerTemplate{
293 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
294 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
295 },
296 remain: []int{0, 1},
297 evictTerminatingPods: false,
298 allSourcesReady: true,
299 },
300 {
301 description: "max container count should apply per (UID, container) pair",
302 containers: []containerTemplate{
303 makeGCContainer("foo", "bar", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
304 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
305 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
306 makeGCContainer("foo1", "baz", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
307 makeGCContainer("foo1", "baz", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
308 makeGCContainer("foo1", "baz", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
309 makeGCContainer("foo2", "bar", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
310 makeGCContainer("foo2", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
311 makeGCContainer("foo2", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
312 },
313 remain: []int{0, 1, 3, 4, 6, 7},
314 evictTerminatingPods: false,
315 allSourcesReady: true,
316 },
317 {
318 description: "max limit should apply and try to keep from every pod",
319 containers: []containerTemplate{
320 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
321 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
322 makeGCContainer("foo1", "bar1", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
323 makeGCContainer("foo1", "bar1", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
324 makeGCContainer("foo2", "bar2", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
325 makeGCContainer("foo2", "bar2", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
326 makeGCContainer("foo3", "bar3", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
327 makeGCContainer("foo3", "bar3", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
328 makeGCContainer("foo4", "bar4", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
329 makeGCContainer("foo4", "bar4", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
330 },
331 remain: []int{0, 2, 4, 6, 8},
332 evictTerminatingPods: false,
333 allSourcesReady: true,
334 },
335 {
336 description: "oldest pods should be removed if limit exceeded",
337 containers: []containerTemplate{
338 makeGCContainer("foo", "bar", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
339 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
340 makeGCContainer("foo1", "bar1", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
341 makeGCContainer("foo1", "bar1", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
342 makeGCContainer("foo2", "bar2", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
343 makeGCContainer("foo3", "bar3", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
344 makeGCContainer("foo4", "bar4", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
345 makeGCContainer("foo5", "bar5", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
346 makeGCContainer("foo6", "bar6", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
347 makeGCContainer("foo7", "bar7", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
348 },
349 remain: []int{0, 2, 4, 6, 8, 9},
350 evictTerminatingPods: false,
351 allSourcesReady: true,
352 },
353 {
354 description: "all non-running containers should be removed when evictTerminatingPods is set",
355 containers: []containerTemplate{
356 makeGCContainer("foo", "bar", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
357 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
358 makeGCContainer("foo1", "bar1", 2, 2, runtimeapi.ContainerState_CONTAINER_EXITED),
359 makeGCContainer("foo1", "bar1", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
360 makeGCContainer("running", "bar2", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
361 makeGCContainer("foo3", "bar3", 0, 0, runtimeapi.ContainerState_CONTAINER_RUNNING),
362 },
363 remain: []int{4, 5},
364 evictTerminatingPods: true,
365 allSourcesReady: true,
366 },
367 {
368 description: "containers for deleted pods should be removed",
369 containers: []containerTemplate{
370 makeGCContainer("foo", "bar", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
371 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
372
373 makeGCContainer("deleted", "bar1", 2, time.Now().UnixNano(), runtimeapi.ContainerState_CONTAINER_EXITED),
374 makeGCContainer("deleted", "bar1", 1, 1, runtimeapi.ContainerState_CONTAINER_EXITED),
375 makeGCContainer("deleted", "bar1", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
376 },
377 remain: []int{0, 1, 2},
378 evictTerminatingPods: false,
379 allSourcesReady: true,
380 },
381 {
382 description: "containers for deleted pods may not be removed if allSourcesReady is set false ",
383 containers: []containerTemplate{
384 makeGCContainer("deleted", "bar1", 0, 0, runtimeapi.ContainerState_CONTAINER_EXITED),
385 },
386 remain: []int{0},
387 evictTerminatingPods: true,
388 allSourcesReady: false,
389 },
390 } {
391 t.Run(test.description, func(t *testing.T) {
392 ctx := context.Background()
393 podStateProvider.removed = make(map[types.UID]struct{})
394 podStateProvider.terminated = make(map[types.UID]struct{})
395 fakeContainers := makeFakeContainers(t, m, test.containers)
396 for _, s := range test.containers {
397 if s.pod.Name == "deleted" {
398 podStateProvider.removed[s.pod.UID] = struct{}{}
399 }
400 if s.pod.Name != "running" {
401 podStateProvider.terminated[s.pod.UID] = struct{}{}
402 }
403 }
404 fakeRuntime.SetFakeContainers(fakeContainers)
405
406 if test.policy == nil {
407 test.policy = &defaultGCPolicy
408 }
409 err := m.containerGC.evictContainers(ctx, *test.policy, test.allSourcesReady, test.evictTerminatingPods)
410 assert.NoError(t, err)
411 realRemain, err := fakeRuntime.ListContainers(ctx, nil)
412 assert.NoError(t, err)
413 assert.Len(t, realRemain, len(test.remain))
414 for _, remain := range test.remain {
415 resp, err := fakeRuntime.ContainerStatus(ctx, fakeContainers[remain].Id, false)
416 assert.NoError(t, err)
417 assert.Equal(t, &fakeContainers[remain].ContainerStatus, resp.Status)
418 }
419 })
420 }
421 }
422
423
424 func TestPodLogDirectoryGC(t *testing.T) {
425 ctx := context.Background()
426 _, _, m, err := createTestRuntimeManager()
427 assert.NoError(t, err)
428 fakeOS := m.osInterface.(*containertest.FakeOS)
429 podStateProvider := m.containerGC.podStateProvider.(*fakePodStateProvider)
430
431
432 files := []string{"123", "456", "789", "012", "name_namespace_321", "name_namespace_654"}
433 podLogsDirectory := "/var/log/pods"
434 removed := []string{
435 filepath.Join(podLogsDirectory, "789"),
436 filepath.Join(podLogsDirectory, "012"),
437 filepath.Join(podLogsDirectory, "name_namespace_654"),
438 }
439 podStateProvider.removed["012"] = struct{}{}
440 podStateProvider.removed["789"] = struct{}{}
441 podStateProvider.removed["654"] = struct{}{}
442
443 ctrl := gomock.NewController(t)
444 defer ctrl.Finish()
445
446 fakeOS.ReadDirFn = func(string) ([]os.DirEntry, error) {
447 var dirEntries []os.DirEntry
448 for _, file := range files {
449 mockDE := containertest.NewMockDirEntry(ctrl)
450 mockDE.EXPECT().Name().Return(file)
451 dirEntries = append(dirEntries, mockDE)
452 }
453 return dirEntries, nil
454 }
455
456
457 err = m.containerGC.evictPodLogsDirectories(ctx, true)
458 assert.NoError(t, err)
459 assert.Equal(t, removed, fakeOS.Removes)
460
461
462 fakeOS.Removes = []string{}
463 err = m.containerGC.evictPodLogsDirectories(ctx, false)
464 assert.NoError(t, err)
465 assert.Empty(t, fakeOS.Removes)
466 }
467
468 func TestUnknownStateContainerGC(t *testing.T) {
469 ctx := context.Background()
470 fakeRuntime, _, m, err := createTestRuntimeManager()
471 assert.NoError(t, err)
472
473
474 defaultGCPolicy := kubecontainer.GCPolicy{MinAge: time.Hour, MaxPerPodContainer: 0, MaxContainers: 0}
475
476 fakeContainers := makeFakeContainers(t, m, []containerTemplate{
477 makeGCContainer("foo", "bar", 0, 0, runtimeapi.ContainerState_CONTAINER_UNKNOWN),
478 })
479 fakeRuntime.SetFakeContainers(fakeContainers)
480
481 err = m.containerGC.evictContainers(ctx, defaultGCPolicy, true, false)
482 assert.NoError(t, err)
483
484 assert.Contains(t, fakeRuntime.GetCalls(), "StopContainer", "RemoveContainer",
485 "container in unknown state should be stopped before being removed")
486
487 remain, err := fakeRuntime.ListContainers(ctx, nil)
488 assert.NoError(t, err)
489 assert.Empty(t, remain)
490 }
491
View as plain text