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