| package daemon |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "path/filepath" |
| "testing" |
| "time" |
| |
| "github.com/google/uuid" |
| containertypes "github.com/moby/moby/api/types/container" |
| "github.com/moby/moby/v2/daemon/container" |
| "github.com/moby/moby/v2/daemon/internal/filters" |
| "github.com/moby/moby/v2/daemon/internal/image" |
| "github.com/moby/moby/v2/daemon/server/backend" |
| "github.com/opencontainers/go-digest" |
| "gotest.tools/v3/assert" |
| is "gotest.tools/v3/assert/cmp" |
| ) |
| |
| var root string |
| |
| func TestMain(m *testing.M) { |
| var err error |
| root, err = os.MkdirTemp("", "docker-container-test-") |
| if err != nil { |
| panic(err) |
| } |
| defer os.RemoveAll(root) |
| os.Exit(m.Run()) |
| } |
| |
| // This sets up a container with a name so that name filters |
| // work against it. It takes in a pointer to Daemon so that |
| // minor operations are not repeated by the caller |
| func setupContainerWithName(t *testing.T, name string, daemon *Daemon) *container.Container { |
| t.Helper() |
| var ( |
| id = uuid.New().String() |
| computedImageID = image.ID(digest.FromString(id)) |
| cRoot = filepath.Join(root, id) |
| ) |
| if err := os.MkdirAll(cRoot, 0o755); err != nil { |
| t.Fatal(err) |
| } |
| |
| c := container.NewBaseContainer(id, cRoot) |
| // these are for passing includeContainerInList |
| if name[0] != '/' { |
| name = "/" + name |
| } |
| c.Name = name |
| c.State.Running = true |
| c.HostConfig = &containertypes.HostConfig{} |
| c.Created = time.Now() |
| |
| // these are for passing the refreshImage reducer |
| c.ImageID = computedImageID |
| c.Config = &containertypes.Config{ |
| Image: computedImageID.String(), |
| } |
| |
| // this is done here to avoid requiring these |
| // operations n x number of containers in the |
| // calling function |
| daemon.containersReplica.Save(c) |
| daemon.reserveName(id, name) |
| |
| return c |
| } |
| |
| func containerListContainsName(containers []*containertypes.Summary, name string) bool { |
| for _, ctr := range containers { |
| for _, containerName := range ctr.Names { |
| if containerName == name { |
| return true |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| func TestContainerList(t *testing.T) { |
| db, err := container.NewViewDB() |
| assert.NilError(t, err) |
| d := &Daemon{ |
| containersReplica: db, |
| } |
| |
| // test list with different number of containers |
| for _, num := range []int{0, 1, 2, 4, 8, 16, 32, 64, 100} { |
| t.Run(fmt.Sprintf("%d containers", num), func(t *testing.T) { |
| db, err := container.NewViewDB() // new DB to ignore prior containers |
| assert.NilError(t, err) |
| d = &Daemon{ |
| containersReplica: db, |
| } |
| |
| // create the containers |
| containers := make([]*container.Container, num) |
| for i := range num { |
| name := fmt.Sprintf("cont-%d", i) |
| containers[i] = setupContainerWithName(t, name, d) |
| // ensure container timestamps are separated enough so the |
| // sort used by d.Containers() can deterministically sort them. |
| if i > 0 { |
| containers[i].Created = containers[i-1].Created.Add(time.Millisecond) |
| } |
| } |
| |
| // list them and verify correctness |
| containerList, err := d.Containers(context.Background(), &backend.ContainerListOptions{All: true}) |
| assert.NilError(t, err) |
| assert.Assert(t, is.Len(containerList, num)) |
| |
| for i := range num { |
| // container list should be ordered in descending creation order |
| assert.Assert(t, is.Equal(containerList[i].Names[0], containers[num-1-i].Name)) |
| } |
| }) |
| } |
| } |
| |
| func TestContainerList_InvalidFilter(t *testing.T) { |
| db, err := container.NewViewDB() |
| assert.NilError(t, err) |
| d := &Daemon{ |
| containersReplica: db, |
| } |
| |
| _, err = d.Containers(context.Background(), &backend.ContainerListOptions{ |
| Filters: filters.NewArgs(filters.Arg("invalid", "foo")), |
| }) |
| assert.Assert(t, is.Error(err, "invalid filter 'invalid'")) |
| } |
| |
| func TestContainerList_NameFilter(t *testing.T) { |
| db, err := container.NewViewDB() |
| assert.NilError(t, err) |
| d := &Daemon{ |
| containersReplica: db, |
| } |
| |
| var ( |
| one = setupContainerWithName(t, "a1", d) |
| two = setupContainerWithName(t, "a2", d) |
| three = setupContainerWithName(t, "b1", d) |
| ) |
| |
| // moby/moby #37453 - ^ regex not working due to prefix slash |
| // not being stripped |
| containerList, err := d.Containers(context.Background(), &backend.ContainerListOptions{ |
| Filters: filters.NewArgs(filters.Arg("name", "^a")), |
| }) |
| assert.NilError(t, err) |
| assert.Assert(t, is.Len(containerList, 2)) |
| assert.Assert(t, containerListContainsName(containerList, one.Name)) |
| assert.Assert(t, containerListContainsName(containerList, two.Name)) |
| |
| // Same as above but with slash prefix should produce the same result |
| containerListWithPrefix, err := d.Containers(context.Background(), &backend.ContainerListOptions{ |
| Filters: filters.NewArgs(filters.Arg("name", "^/a")), |
| }) |
| assert.NilError(t, err) |
| assert.Assert(t, is.Len(containerListWithPrefix, 2)) |
| assert.Assert(t, containerListContainsName(containerListWithPrefix, one.Name)) |
| assert.Assert(t, containerListContainsName(containerListWithPrefix, two.Name)) |
| |
| // Same as above but make sure it works for exact names |
| containerList, err = d.Containers(context.Background(), &backend.ContainerListOptions{ |
| Filters: filters.NewArgs(filters.Arg("name", "b1")), |
| }) |
| assert.NilError(t, err) |
| assert.Assert(t, is.Len(containerList, 1)) |
| assert.Assert(t, containerListContainsName(containerList, three.Name)) |
| |
| // Same as above but with slash prefix should produce the same result |
| containerListWithPrefix, err = d.Containers(context.Background(), &backend.ContainerListOptions{ |
| Filters: filters.NewArgs(filters.Arg("name", "/b1")), |
| }) |
| assert.NilError(t, err) |
| assert.Assert(t, is.Len(containerListWithPrefix, 1)) |
| assert.Assert(t, containerListContainsName(containerListWithPrefix, three.Name)) |
| } |
| |
| func TestContainerList_LimitFilter(t *testing.T) { |
| db, err := container.NewViewDB() |
| assert.NilError(t, err) |
| d := &Daemon{ |
| containersReplica: db, |
| } |
| |
| // start containers |
| num := 32 |
| for i := range num { |
| name := fmt.Sprintf("cont-%d", i) |
| setupContainerWithName(t, name, d) |
| } |
| |
| containers, err := db.Snapshot().All() |
| assert.NilError(t, err) |
| assert.Assert(t, is.Len(containers, num)) |
| |
| tests := []struct { |
| limit int |
| doc string |
| }{ |
| {limit: 0, doc: "no limit"}, |
| {limit: -1, doc: "negative limit doesn't limit"}, |
| {limit: 1, doc: "limit 1 container"}, |
| {limit: 20, doc: "limit less than num containers"}, |
| {limit: 32, doc: "limit equal num containers"}, |
| {limit: 40, doc: "limit greater than num containers"}, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.doc, func(t *testing.T) { |
| containerList, err := d.Containers(context.Background(), &backend.ContainerListOptions{Limit: tc.limit}) |
| assert.NilError(t, err) |
| expectedListLen := num |
| if tc.limit > 0 { |
| expectedListLen = min(num, tc.limit) |
| } |
| assert.Assert(t, is.Len(containerList, expectedListLen)) |
| }) |
| } |
| } |