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