github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/container_list_linux_test.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"os"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/containerd/nerdctl/pkg/formatter"
    27  	"github.com/containerd/nerdctl/pkg/strutil"
    28  	"github.com/containerd/nerdctl/pkg/tabutil"
    29  	"github.com/containerd/nerdctl/pkg/testutil"
    30  	"gotest.tools/v3/assert"
    31  )
    32  
    33  type psTestContainer struct {
    34  	name    string
    35  	labels  map[string]string
    36  	volumes []string
    37  	network string
    38  }
    39  
    40  // When keepAlive is false, the container will exit immediately with status 1.
    41  func preparePsTestContainer(t *testing.T, identity string, keepAlive bool) (*testutil.Base, psTestContainer) {
    42  	base := testutil.NewBase(t)
    43  
    44  	base.Cmd("pull", testutil.CommonImage).AssertOK()
    45  
    46  	testContainerName := testutil.Identifier(t) + identity
    47  	rwVolName := testContainerName + "-rw"
    48  	// A container can mount named and anonymous volumes
    49  	rwDir, err := os.MkdirTemp(t.TempDir(), "rw")
    50  	if err != nil {
    51  		t.Fatal(err)
    52  	}
    53  	base.Cmd("network", "create", testContainerName).AssertOK()
    54  	t.Cleanup(func() {
    55  		base.Cmd("rm", "-f", testContainerName).AssertOK()
    56  		base.Cmd("volume", "rm", "-f", rwVolName).Run()
    57  		base.Cmd("network", "rm", testContainerName).Run()
    58  		os.RemoveAll(rwDir)
    59  	})
    60  
    61  	// A container can have multiple labels.
    62  	// Therefore, this test container has multiple labels to check it.
    63  	testLabels := make(map[string]string)
    64  	keys := []string{
    65  		testutil.Identifier(t) + identity,
    66  		testutil.Identifier(t) + identity,
    67  	}
    68  	// fill the value of testLabels
    69  	for _, k := range keys {
    70  		testLabels[k] = k
    71  	}
    72  	base.Cmd("volume", "create", rwVolName).AssertOK()
    73  	mnt1 := fmt.Sprintf("%s:/%s_mnt1", rwDir, identity)
    74  	mnt2 := fmt.Sprintf("%s:/%s_mnt3", rwVolName, identity)
    75  
    76  	args := []string{
    77  		"run",
    78  		"-d",
    79  		"--name",
    80  		testContainerName,
    81  		"--label",
    82  		formatter.FormatLabels(testLabels),
    83  		"-v", mnt1,
    84  		"-v", mnt2,
    85  		"--net", testContainerName,
    86  	}
    87  	if keepAlive {
    88  		args = append(args, testutil.CommonImage, "top")
    89  	} else {
    90  		args = append(args, "--restart=no", testutil.CommonImage, "false")
    91  	}
    92  
    93  	base.Cmd(args...).AssertOK()
    94  	if keepAlive {
    95  		base.EnsureContainerStarted(testContainerName)
    96  	} else {
    97  		base.EnsureContainerExited(testContainerName, 1)
    98  	}
    99  
   100  	// dd if=/dev/zero of=test_file bs=1M count=25
   101  	// let the container occupy 25MiB space.
   102  	if keepAlive {
   103  		base.Cmd("exec", testContainerName, "dd", "if=/dev/zero", "of=/test_file", "bs=1M", "count=25").AssertOK()
   104  	}
   105  	volumes := []string{}
   106  	volumes = append(volumes, strings.Split(mnt1, ":")...)
   107  	volumes = append(volumes, strings.Split(mnt2, ":")...)
   108  
   109  	return base, psTestContainer{
   110  		name:    testContainerName,
   111  		labels:  testLabels,
   112  		volumes: volumes,
   113  		network: testContainerName,
   114  	}
   115  }
   116  
   117  func TestContainerList(t *testing.T) {
   118  	base, testContainer := preparePsTestContainer(t, "list", true)
   119  
   120  	// hope there are no tests running parallel
   121  	base.Cmd("ps", "-n", "1", "-s").AssertOutWithFunc(func(stdout string) error {
   122  		// An example of nerdctl/docker ps -n 1 -s
   123  		// CONTAINER ID    IMAGE                               COMMAND    CREATED           STATUS    PORTS    NAMES            SIZE
   124  		// be8d386c991e    docker.io/library/busybox:latest    "top"      1 second ago    Up                 c1       16.0 KiB (virtual 1.3 MiB)
   125  
   126  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   127  		if len(lines) < 2 {
   128  			return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   129  		}
   130  
   131  		tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE")
   132  		err := tab.ParseHeader(lines[0])
   133  		if err != nil {
   134  			return fmt.Errorf("failed to parse header: %v", err)
   135  		}
   136  
   137  		container, _ := tab.ReadRow(lines[1], "NAMES")
   138  		assert.Equal(t, container, testContainer.name)
   139  
   140  		image, _ := tab.ReadRow(lines[1], "IMAGE")
   141  		assert.Equal(t, image, testutil.CommonImage)
   142  
   143  		size, _ := tab.ReadRow(lines[1], "SIZE")
   144  
   145  		// there is some difference between nerdctl and docker in calculating the size of the container
   146  		expectedSize := "26.2MB (virtual "
   147  		if base.Target != testutil.Docker {
   148  			expectedSize = "25.0 MiB (virtual "
   149  		}
   150  
   151  		if !strings.Contains(size, expectedSize) {
   152  			return fmt.Errorf("expect container size %s, but got %s", expectedSize, size)
   153  		}
   154  
   155  		return nil
   156  	})
   157  }
   158  
   159  func TestContainerListWideMode(t *testing.T) {
   160  	testutil.DockerIncompatible(t)
   161  	base, testContainer := preparePsTestContainer(t, "listWithMode", true)
   162  
   163  	// hope there are no tests running parallel
   164  	base.Cmd("ps", "-n", "1", "--format", "wide").AssertOutWithFunc(func(stdout string) error {
   165  
   166  		// An example of nerdctl ps --format wide
   167  		// CONTAINER ID    IMAGE                               PLATFORM       COMMAND    CREATED              STATUS    PORTS    NAMES            RUNTIME                  SIZE
   168  		// 17181f208b61    docker.io/library/busybox:latest    linux/amd64    "top"      About an hour ago    Up                 busybox-17181    io.containerd.runc.v2    16.0 KiB (virtual 1.3 MiB)
   169  
   170  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   171  		if len(lines) < 2 {
   172  			return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   173  		}
   174  
   175  		tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE")
   176  		err := tab.ParseHeader(lines[0])
   177  		if err != nil {
   178  			return fmt.Errorf("failed to parse header: %v", err)
   179  		}
   180  
   181  		container, _ := tab.ReadRow(lines[1], "NAMES")
   182  		assert.Equal(t, container, testContainer.name)
   183  
   184  		image, _ := tab.ReadRow(lines[1], "IMAGE")
   185  		assert.Equal(t, image, testutil.CommonImage)
   186  
   187  		runtime, _ := tab.ReadRow(lines[1], "RUNTIME")
   188  		assert.Equal(t, runtime, "io.containerd.runc.v2")
   189  
   190  		size, _ := tab.ReadRow(lines[1], "SIZE")
   191  		expectedSize := "25.0 MiB (virtual "
   192  		if !strings.Contains(size, expectedSize) {
   193  			return fmt.Errorf("expect container size %s, but got %s", expectedSize, size)
   194  		}
   195  		return nil
   196  	})
   197  }
   198  
   199  func TestContainerListWithLabels(t *testing.T) {
   200  	base, testContainer := preparePsTestContainer(t, "listWithLabels", true)
   201  
   202  	// hope there are no tests running parallel
   203  	base.Cmd("ps", "-n", "1", "--format", "{{.Labels}}").AssertOutWithFunc(func(stdout string) error {
   204  
   205  		// An example of nerdctl ps --format "{{.Labels}}"
   206  		// key1=value1,key2=value2,key3=value3
   207  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   208  		if len(lines) != 1 {
   209  			return fmt.Errorf("expected 1 line, got %d", len(lines))
   210  		}
   211  
   212  		// check labels using map
   213  		// 1. the results has no guarantee to show the same order.
   214  		// 2. the results has no guarantee to show only configured labels.
   215  		labelsMap, err := strutil.ParseCSVMap(lines[0])
   216  		if err != nil {
   217  			return fmt.Errorf("failed to parse labels: %v", err)
   218  		}
   219  
   220  		for i := range testContainer.labels {
   221  			if value, ok := labelsMap[i]; ok {
   222  				assert.Equal(t, value, testContainer.labels[i])
   223  			}
   224  		}
   225  		return nil
   226  	})
   227  }
   228  
   229  func TestContainerListWithNames(t *testing.T) {
   230  	base, testContainer := preparePsTestContainer(t, "listWithNames", true)
   231  
   232  	// hope there are no tests running parallel
   233  	base.Cmd("ps", "-n", "1", "--format", "{{.Names}}").AssertOutWithFunc(func(stdout string) error {
   234  
   235  		// An example of nerdctl ps --format "{{.Names}}"
   236  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   237  		if len(lines) != 1 {
   238  			return fmt.Errorf("expected 1 line, got %d", len(lines))
   239  		}
   240  
   241  		assert.Equal(t, lines[0], testContainer.name)
   242  
   243  		return nil
   244  	})
   245  }
   246  
   247  func TestContainerListWithFilter(t *testing.T) {
   248  	base, testContainerA := preparePsTestContainer(t, "listWithFilterA", true)
   249  	_, testContainerB := preparePsTestContainer(t, "listWithFilterB", true)
   250  	_, testContainerC := preparePsTestContainer(t, "listWithFilterC", false)
   251  
   252  	base.Cmd("ps", "--filter", "name="+testContainerA.name).AssertOutWithFunc(func(stdout string) error {
   253  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   254  		if len(lines) < 2 {
   255  			return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   256  		}
   257  
   258  		tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   259  		err := tab.ParseHeader(lines[0])
   260  		if err != nil {
   261  			return fmt.Errorf("failed to parse header: %v", err)
   262  		}
   263  
   264  		containerName, _ := tab.ReadRow(lines[1], "NAMES")
   265  		assert.Equal(t, containerName, testContainerA.name)
   266  		id, _ := tab.ReadRow(lines[1], "CONTAINER ID")
   267  		base.Cmd("ps", "-q", "--filter", "id="+id).AssertOutWithFunc(func(stdout string) error {
   268  			lines := strings.Split(strings.TrimSpace(stdout), "\n")
   269  			if len(lines) != 1 {
   270  				return fmt.Errorf("expected 1 line, got %d", len(lines))
   271  			}
   272  			if lines[0] != id {
   273  				return errors.New("failed to filter by id")
   274  			}
   275  			return nil
   276  		})
   277  		base.Cmd("ps", "-q", "--filter", "id="+id+id).AssertOutWithFunc(func(stdout string) error {
   278  			lines := strings.Split(strings.TrimSpace(stdout), "\n")
   279  			if len(lines) > 0 {
   280  				for _, line := range lines {
   281  					if line != "" {
   282  						return fmt.Errorf("unexpected container found: %s", line)
   283  					}
   284  				}
   285  			}
   286  			return nil
   287  		})
   288  		base.Cmd("ps", "-q", "--filter", "id=").AssertOutWithFunc(func(stdout string) error {
   289  			lines := strings.Split(strings.TrimSpace(stdout), "\n")
   290  			if len(lines) > 0 {
   291  				for _, line := range lines {
   292  					if line != "" {
   293  						return fmt.Errorf("unexpected container found: %s", line)
   294  					}
   295  				}
   296  			}
   297  			return nil
   298  		})
   299  		return nil
   300  	})
   301  
   302  	base.Cmd("ps", "-q", "--filter", "name="+testContainerA.name+testContainerA.name).AssertOutWithFunc(func(stdout string) error {
   303  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   304  		if len(lines) > 0 {
   305  			for _, line := range lines {
   306  				if line != "" {
   307  					return fmt.Errorf("unexpected container found: %s", line)
   308  				}
   309  			}
   310  		}
   311  		return nil
   312  	})
   313  
   314  	base.Cmd("ps", "-q", "--filter", "name=").AssertOutWithFunc(func(stdout string) error {
   315  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   316  		if len(lines) == 0 {
   317  			return errors.New("expect at least 1 container, got 0")
   318  		}
   319  		return nil
   320  	})
   321  
   322  	base.Cmd("ps", "--filter", "name=listWithFilter").AssertOutWithFunc(func(stdout string) error {
   323  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   324  		if len(lines) < 3 {
   325  			return fmt.Errorf("expected at least 3 lines, got %d", len(lines))
   326  		}
   327  
   328  		tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   329  		err := tab.ParseHeader(lines[0])
   330  		if err != nil {
   331  			return fmt.Errorf("failed to parse header: %v", err)
   332  		}
   333  		containerNames := map[string]struct{}{
   334  			testContainerA.name: {}, testContainerB.name: {},
   335  		}
   336  		for idx, line := range lines {
   337  			if idx == 0 {
   338  				continue
   339  			}
   340  			containerName, _ := tab.ReadRow(line, "NAMES")
   341  			if _, ok := containerNames[containerName]; !ok {
   342  				return fmt.Errorf("unexpected container %s found", containerName)
   343  			}
   344  		}
   345  		return nil
   346  	})
   347  
   348  	// docker filter by id only support full ID no truncate
   349  	// https://github.com/docker/for-linux/issues/258
   350  	// yet nerdctl also support truncate ID
   351  	base.Cmd("ps", "--no-trunc", "--filter", "since="+testContainerA.name).AssertOutWithFunc(func(stdout string) error {
   352  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   353  		if len(lines) < 2 {
   354  			return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   355  		}
   356  
   357  		tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   358  		err := tab.ParseHeader(lines[0])
   359  		if err != nil {
   360  			return fmt.Errorf("failed to parse header: %v", err)
   361  		}
   362  		var id string
   363  		for idx, line := range lines {
   364  			if idx == 0 {
   365  				continue
   366  			}
   367  			containerName, _ := tab.ReadRow(line, "NAMES")
   368  			if containerName != testContainerB.name {
   369  				return fmt.Errorf("unexpected container %s found", containerName)
   370  			}
   371  			id, _ = tab.ReadRow(line, "CONTAINER ID")
   372  		}
   373  		base.Cmd("ps", "--filter", "before="+id).AssertOutWithFunc(func(stdout string) error {
   374  			lines := strings.Split(strings.TrimSpace(stdout), "\n")
   375  			if len(lines) < 2 {
   376  				return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   377  			}
   378  
   379  			tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   380  			err := tab.ParseHeader(lines[0])
   381  			if err != nil {
   382  				return fmt.Errorf("failed to parse header: %v", err)
   383  			}
   384  			foundA := false
   385  			for idx, line := range lines {
   386  				if idx == 0 {
   387  					continue
   388  				}
   389  				containerName, _ := tab.ReadRow(line, "NAMES")
   390  				if containerName == testContainerA.name {
   391  					foundA = true
   392  					break
   393  				}
   394  			}
   395  			// there are other containers such as **wordpress** could be listed since
   396  			// their created times are ahead of testContainerB too
   397  			if !foundA {
   398  				return fmt.Errorf("expected container %s not found", testContainerA.name)
   399  			}
   400  			return nil
   401  		})
   402  		return nil
   403  	})
   404  
   405  	// docker filter by id only support full ID no truncate
   406  	// https://github.com/docker/for-linux/issues/258
   407  	// yet nerdctl also support truncate ID
   408  	base.Cmd("ps", "--no-trunc", "--filter", "before="+testContainerB.name).AssertOutWithFunc(func(stdout string) error {
   409  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   410  		if len(lines) < 2 {
   411  			return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   412  		}
   413  
   414  		tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   415  		err := tab.ParseHeader(lines[0])
   416  		if err != nil {
   417  			return fmt.Errorf("failed to parse header: %v", err)
   418  		}
   419  		foundA := false
   420  		var id string
   421  		for idx, line := range lines {
   422  			if idx == 0 {
   423  				continue
   424  			}
   425  			containerName, _ := tab.ReadRow(line, "NAMES")
   426  			if containerName == testContainerA.name {
   427  				foundA = true
   428  				id, _ = tab.ReadRow(line, "CONTAINER ID")
   429  				break
   430  			}
   431  		}
   432  		// there are other containers such as **wordpress** could be listed since
   433  		// their created times are ahead of testContainerB too
   434  		if !foundA {
   435  			return fmt.Errorf("expected container %s not found", testContainerA.name)
   436  		}
   437  		base.Cmd("ps", "--filter", "since="+id).AssertOutWithFunc(func(stdout string) error {
   438  			lines := strings.Split(strings.TrimSpace(stdout), "\n")
   439  			if len(lines) < 2 {
   440  				return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   441  			}
   442  
   443  			tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   444  			err := tab.ParseHeader(lines[0])
   445  			if err != nil {
   446  				return fmt.Errorf("failed to parse header: %v", err)
   447  			}
   448  			for idx, line := range lines {
   449  				if idx == 0 {
   450  					continue
   451  				}
   452  				containerName, _ := tab.ReadRow(line, "NAMES")
   453  				if containerName != testContainerB.name {
   454  					return fmt.Errorf("unexpected container %s found", containerName)
   455  				}
   456  			}
   457  			return nil
   458  		})
   459  		return nil
   460  	})
   461  
   462  	for _, testContainer := range []psTestContainer{testContainerA, testContainerB} {
   463  		for _, volume := range testContainer.volumes {
   464  			base.Cmd("ps", "--filter", "volume="+volume).AssertOutWithFunc(func(stdout string) error {
   465  				lines := strings.Split(strings.TrimSpace(stdout), "\n")
   466  				if len(lines) < 2 {
   467  					return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   468  				}
   469  
   470  				tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   471  				err := tab.ParseHeader(lines[0])
   472  				if err != nil {
   473  					return fmt.Errorf("failed to parse header: %v", err)
   474  				}
   475  				containerName, _ := tab.ReadRow(lines[1], "NAMES")
   476  				assert.Equal(t, containerName, testContainer.name)
   477  				return nil
   478  			})
   479  		}
   480  	}
   481  
   482  	base.Cmd("ps", "--filter", "network="+testContainerA.network).AssertOutWithFunc(func(stdout string) error {
   483  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   484  		if len(lines) < 2 {
   485  			return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   486  		}
   487  
   488  		tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   489  		err := tab.ParseHeader(lines[0])
   490  		if err != nil {
   491  			return fmt.Errorf("failed to parse header: %v", err)
   492  		}
   493  		containerName, _ := tab.ReadRow(lines[1], "NAMES")
   494  		assert.Equal(t, containerName, testContainerA.name)
   495  		return nil
   496  	})
   497  
   498  	for key, value := range testContainerB.labels {
   499  		base.Cmd("ps", "--filter", "label="+key+"="+value).AssertOutWithFunc(func(stdout string) error {
   500  			lines := strings.Split(strings.TrimSpace(stdout), "\n")
   501  			if len(lines) < 2 {
   502  				return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   503  			}
   504  
   505  			tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   506  			err := tab.ParseHeader(lines[0])
   507  			if err != nil {
   508  				return fmt.Errorf("failed to parse header: %v", err)
   509  			}
   510  			containerNames := map[string]struct{}{
   511  				testContainerB.name: {},
   512  			}
   513  			for idx, line := range lines {
   514  				if idx == 0 {
   515  					continue
   516  				}
   517  				containerName, _ := tab.ReadRow(line, "NAMES")
   518  				if _, ok := containerNames[containerName]; !ok {
   519  					return fmt.Errorf("unexpected container %s found", containerName)
   520  				}
   521  			}
   522  			return nil
   523  		})
   524  	}
   525  
   526  	base.Cmd("ps", "-a", "--filter", "exited=1").AssertOutWithFunc(func(stdout string) error {
   527  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   528  		if len(lines) < 2 {
   529  			return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   530  		}
   531  
   532  		tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   533  		err := tab.ParseHeader(lines[0])
   534  		if err != nil {
   535  			return fmt.Errorf("failed to parse header: %v", err)
   536  		}
   537  		containerNames := map[string]struct{}{
   538  			testContainerC.name: {},
   539  		}
   540  		for idx, line := range lines {
   541  			if idx == 0 {
   542  				continue
   543  			}
   544  			containerName, _ := tab.ReadRow(line, "NAMES")
   545  			if _, ok := containerNames[containerName]; !ok {
   546  				return fmt.Errorf("unexpected container %s found", containerName)
   547  			}
   548  		}
   549  		return nil
   550  	})
   551  
   552  	base.Cmd("ps", "-a", "--filter", "status=exited").AssertOutWithFunc(func(stdout string) error {
   553  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
   554  		if len(lines) < 2 {
   555  			return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
   556  		}
   557  
   558  		tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
   559  		err := tab.ParseHeader(lines[0])
   560  		if err != nil {
   561  			return fmt.Errorf("failed to parse header: %v", err)
   562  		}
   563  		containerNames := map[string]struct{}{
   564  			testContainerC.name: {},
   565  		}
   566  		for idx, line := range lines {
   567  			if idx == 0 {
   568  				continue
   569  			}
   570  			containerName, _ := tab.ReadRow(line, "NAMES")
   571  			if _, ok := containerNames[containerName]; !ok {
   572  				return fmt.Errorf("unexpected container %s found", containerName)
   573  			}
   574  		}
   575  		return nil
   576  	})
   577  }