Add until filter to docker image ls
- Fixes https://github.com/moby/moby/issues/46576

<!--
Please make sure you've read and understood our contributing guidelines;
https://github.com/moby/moby/blob/master/CONTRIBUTING.md

** Make sure all your commits include a signature generated with `git commit -s` **

For additional information on our contributing process, read our contributing
guide https://docs.docker.com/opensource/code/

If this is a bug fix, make sure your description includes "fixes #xxxx", or
"closes #xxxx"

Please provide the following information:
-->

**- What I did**
Added the ability to filter the results of `docker image ls` or `docker images` through timestamps by adding the `until` filter.

**- How I did it**
Modified the `acceptedImageFilterTags` map in `daemon/containerd/image_list.go` to include an `until` key. Then I modified `setupFilters` function in the same file to process the value assigned to the until key

**- How to verify it**
I added an integration test function `TestImagesFilterUntil`. I did not see any unit tests that test the `setupFilters` function directly, which is why I decided to add this new test instead. Let me know if there is a simpler or better way to test this. 

**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->
`docker image ls` now allows filtering by timestamp

**- A picture of a cute animal (not mandatory but encouraged)**


integration/image/list_test.go
package image // import "github.com/docker/docker/integration/image"

import (
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/docker/docker/api/types"
	containertypes "github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/versions"
	"github.com/docker/docker/integration/internal/container"
	"github.com/docker/docker/testutil"
	"github.com/google/go-cmp/cmp/cmpopts"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	"gotest.tools/v3/skip"
)

// Regression : #38171
func TestImagesFilterMultiReference(t *testing.T) {
	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
	ctx := setupTest(t)

	client := testEnv.APIClient()

	name := strings.ToLower(t.Name())
	repoTags := []string{
		name + ":v1",
		name + ":v2",
		name + ":v3",
		name + ":v4",
	}

	for _, repoTag := range repoTags {
		err := client.ImageTag(ctx, "busybox:latest", repoTag)
		assert.NilError(t, err)
	}

	filter := filters.NewArgs()
	filter.Add("reference", repoTags[0])
	filter.Add("reference", repoTags[1])
	filter.Add("reference", repoTags[2])
	options := types.ImageListOptions{
		Filters: filter,
	}
	images, err := client.ImageList(ctx, options)
	assert.NilError(t, err)

	assert.Assert(t, is.Len(images, 1))
	assert.Check(t, is.Len(images[0].RepoTags, 3))
	for _, repoTag := range images[0].RepoTags {
		if repoTag != repoTags[0] && repoTag != repoTags[1] && repoTag != repoTags[2] {
			t.Errorf("list images doesn't match any repoTag we expected, repoTag: %s", repoTag)
		}
	}
}

func TestImagesFilterBeforeSince(t *testing.T) {
	ctx := setupTest(t)

	client := testEnv.APIClient()

	name := strings.ToLower(t.Name())
	ctr := container.Create(ctx, t, client, container.WithName(name))

	imgs := make([]string, 5)
	for i := range imgs {
		if i > 0 {
			// Make really really sure each image has a distinct timestamp.
			time.Sleep(time.Millisecond)
		}
		id, err := client.ContainerCommit(ctx, ctr, containertypes.CommitOptions{Reference: fmt.Sprintf("%s:v%d", name, i)})
		assert.NilError(t, err)
		imgs[i] = id.ID
	}

	filter := filters.NewArgs(
		filters.Arg("since", imgs[0]),
		filters.Arg("before", imgs[len(imgs)-1]),
	)
	list, err := client.ImageList(ctx, types.ImageListOptions{Filters: filter})
	assert.NilError(t, err)

	var listedIDs []string
	for _, i := range list {
		t.Logf("ImageList: ID=%v RepoTags=%v", i.ID, i.RepoTags)
		listedIDs = append(listedIDs, i.ID)
	}
	// The ImageList API sorts the list by created timestamp... truncated to
	// 1-second precision. Since all the images were created within
	// milliseconds of each other, listedIDs is effectively unordered and
	// the assertion must therefore be order-independent.
	assert.DeepEqual(t, listedIDs, imgs[1:len(imgs)-1], cmpopts.SortSlices(func(a, b string) bool { return a < b }))
}

func TestAPIImagesFilters(t *testing.T) {
	ctx := setupTest(t)
	client := testEnv.APIClient()

	for _, n := range []string{"utest:tag1", "utest/docker:tag2", "utest:5000/docker:tag3"} {
		err := client.ImageTag(ctx, "busybox:latest", n)
		assert.NilError(t, err)
	}

	testcases := []struct {
		name             string
		filters          []filters.KeyValuePair
		expectedImages   int
		expectedRepoTags int
	}{
		{
			name:             "repository regex",
			filters:          []filters.KeyValuePair{filters.Arg("reference", "utest*/*")},
			expectedImages:   1,
			expectedRepoTags: 2,
		},
		{
			name:             "image name regex",
			filters:          []filters.KeyValuePair{filters.Arg("reference", "utest*")},
			expectedImages:   1,
			expectedRepoTags: 1,
		},
		{
			name:             "image name without a tag",
			filters:          []filters.KeyValuePair{filters.Arg("reference", "utest")},
			expectedImages:   1,
			expectedRepoTags: 1,
		},
		{
			name:             "registry port regex",
			filters:          []filters.KeyValuePair{filters.Arg("reference", "*5000*/*")},
			expectedImages:   1,
			expectedRepoTags: 1,
		},
	}

	for _, tc := range testcases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx := testutil.StartSpan(ctx, t)
			images, err := client.ImageList(ctx, types.ImageListOptions{
				Filters: filters.NewArgs(tc.filters...),
			})
			assert.Check(t, err)
			assert.Assert(t, is.Len(images, tc.expectedImages))
			assert.Check(t, is.Len(images[0].RepoTags, tc.expectedRepoTags))
		})
	}
}

package image // import "github.com/docker/docker/integration/image"

import (
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/docker/docker/api/types"
	containertypes "github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/versions"
	"github.com/docker/docker/integration/internal/container"
	"github.com/docker/docker/testutil"
	"github.com/google/go-cmp/cmp/cmpopts"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	"gotest.tools/v3/skip"
)

// Regression : #38171
func TestImagesFilterMultiReference(t *testing.T) {
	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
	ctx := setupTest(t)

	client := testEnv.APIClient()

	name := strings.ToLower(t.Name())
	repoTags := []string{
		name + ":v1",
		name + ":v2",
		name + ":v3",
		name + ":v4",
	}

	for _, repoTag := range repoTags {
		err := client.ImageTag(ctx, "busybox:latest", repoTag)
		assert.NilError(t, err)
	}

	filter := filters.NewArgs()
	filter.Add("reference", repoTags[0])
	filter.Add("reference", repoTags[1])
	filter.Add("reference", repoTags[2])
	options := types.ImageListOptions{
		Filters: filter,
	}
	images, err := client.ImageList(ctx, options)
	assert.NilError(t, err)

	assert.Assert(t, is.Len(images, 1))
	assert.Check(t, is.Len(images[0].RepoTags, 3))
	for _, repoTag := range images[0].RepoTags {
		if repoTag != repoTags[0] && repoTag != repoTags[1] && repoTag != repoTags[2] {
			t.Errorf("list images doesn't match any repoTag we expected, repoTag: %s", repoTag)
		}
	}
}

func TestImagesFilterUntil(t *testing.T) {
	ctx := setupTest(t)

	client := testEnv.APIClient()

	name := strings.ToLower(t.Name())
	ctr := container.Create(ctx, t, client, container.WithName(name))

	imgs := make([]string, 5)
	for i := range imgs {
		if i > 0 {
			// Make really really sure each image has a distinct timestamp.
			time.Sleep(time.Millisecond)
		}
		id, err := client.ContainerCommit(ctx, ctr, containertypes.CommitOptions{Reference: fmt.Sprintf("%s:v%d", name, i)})
		assert.NilError(t, err)
		imgs[i] = id.ID
	}

	olderImage, _, err := client.ImageInspectWithRaw(ctx, imgs[2])
	assert.NilError(t, err)
	olderUntil := olderImage.Created

	laterImage, _, err := client.ImageInspectWithRaw(ctx, imgs[3])
	assert.NilError(t, err)
	laterUntil := laterImage.Created

	filter := filters.NewArgs(
		filters.Arg("since", imgs[0]),
		filters.Arg("until", olderUntil),
		filters.Arg("until", laterUntil),
		filters.Arg("before", imgs[len(imgs)-1]),
	)
	list, err := client.ImageList(ctx, types.ImageListOptions{Filters: filter})
	assert.NilError(t, err)

	var listedIDs []string
	for _, i := range list {
		t.Logf("ImageList: ID=%v RepoTags=%v", i.ID, i.RepoTags)
		listedIDs = append(listedIDs, i.ID)
	}
	assert.DeepEqual(t, listedIDs, imgs[1:2], cmpopts.SortSlices(func(a, b string) bool { return a < b }))
}

func TestImagesFilterBeforeSince(t *testing.T) {
	ctx := setupTest(t)

	client := testEnv.APIClient()

	name := strings.ToLower(t.Name())
	ctr := container.Create(ctx, t, client, container.WithName(name))

	imgs := make([]string, 5)
	for i := range imgs {
		if i > 0 {
			// Make really really sure each image has a distinct timestamp.
			time.Sleep(time.Millisecond)
		}
		id, err := client.ContainerCommit(ctx, ctr, containertypes.CommitOptions{Reference: fmt.Sprintf("%s:v%d", name, i)})
		assert.NilError(t, err)
		imgs[i] = id.ID
	}

	filter := filters.NewArgs(
		filters.Arg("since", imgs[0]),
		filters.Arg("before", imgs[len(imgs)-1]),
	)
	list, err := client.ImageList(ctx, types.ImageListOptions{Filters: filter})
	assert.NilError(t, err)

	var listedIDs []string
	for _, i := range list {
		t.Logf("ImageList: ID=%v RepoTags=%v", i.ID, i.RepoTags)
		listedIDs = append(listedIDs, i.ID)
	}
	// The ImageList API sorts the list by created timestamp... truncated to
	// 1-second precision. Since all the images were created within
	// milliseconds of each other, listedIDs is effectively unordered and
	// the assertion must therefore be order-independent.
	assert.DeepEqual(t, listedIDs, imgs[1:len(imgs)-1], cmpopts.SortSlices(func(a, b string) bool { return a < b }))
}

func TestAPIImagesFilters(t *testing.T) {
	ctx := setupTest(t)
	client := testEnv.APIClient()

	for _, n := range []string{"utest:tag1", "utest/docker:tag2", "utest:5000/docker:tag3"} {
		err := client.ImageTag(ctx, "busybox:latest", n)
		assert.NilError(t, err)
	}

	testcases := []struct {
		name             string
		filters          []filters.KeyValuePair
		expectedImages   int
		expectedRepoTags int
	}{
		{
			name:             "repository regex",
			filters:          []filters.KeyValuePair{filters.Arg("reference", "utest*/*")},
			expectedImages:   1,
			expectedRepoTags: 2,
		},
		{
			name:             "image name regex",
			filters:          []filters.KeyValuePair{filters.Arg("reference", "utest*")},
			expectedImages:   1,
			expectedRepoTags: 1,
		},
		{
			name:             "image name without a tag",
			filters:          []filters.KeyValuePair{filters.Arg("reference", "utest")},
			expectedImages:   1,
			expectedRepoTags: 1,
		},
		{
			name:             "registry port regex",
			filters:          []filters.KeyValuePair{filters.Arg("reference", "*5000*/*")},
			expectedImages:   1,
			expectedRepoTags: 1,
		},
	}

	for _, tc := range testcases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx := testutil.StartSpan(ctx, t)
			images, err := client.ImageList(ctx, types.ImageListOptions{
				Filters: filters.NewArgs(tc.filters...),
			})
			assert.Check(t, err)
			assert.Assert(t, is.Len(images, tc.expectedImages))
			assert.Check(t, is.Len(images[0].RepoTags, tc.expectedRepoTags))
		})
	}
}

The `Created` timestamp of an image is the timestamp of when the image was created. Images created by committing a container are no exception. The `Created` timestamp of the source container is irrelevant information. (Think about it. The container could have been created days, months, or years in the past. And containers are mutable: images of the same container committed at different times could be vastly different in content.) **The `olderUntil` and `laterUntil` timestamps need to be (derived from) the `Created` timestamps of the images themselves to ensure the test is not flaky.** As there is no correlation between the image timestamps and any other timestamp aside from the order of operations of the test itself, using any other timestamps is not much better than just calling `time.Now()` directly at the right points in the test.

For the same reason, only one source container is needed, just like in `TestImagesFilterBeforeSince`.

You only need the image ID to query its `Created` timestamp. Since the committed images' IDs are assigned to the `imgs` slice, the code to fetch the `olderUntil` and `laterUntil` values [can be moved to below the loop.](https://en.wikipedia.org/wiki/Loop-switch_sequence)