client/image_tag: Wrap options and result

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2025-10-20 22:38:39 +02:00
committed by Austin Vazquez
parent 347693a580
commit 6819a9fc1e
13 changed files with 72 additions and 49 deletions

View File

@@ -115,7 +115,7 @@ type ImageAPIClient interface {
ImagePush(ctx context.Context, ref string, options ImagePushOptions) (ImagePushResponse, error)
ImageRemove(ctx context.Context, image string, options ImageRemoveOptions) (ImageRemoveResult, error)
ImageSearch(ctx context.Context, term string, options ImageSearchOptions) (ImageSearchResult, error)
ImageTag(ctx context.Context, image, ref string) error
ImageTag(ctx context.Context, options ImageTagOptions) (ImageTagResult, error)
ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error)
ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (ImageInspectResult, error)

View File

@@ -9,19 +9,29 @@ import (
"github.com/distribution/reference"
)
type ImageTagOptions struct {
Source string
Target string
}
type ImageTagResult struct{}
// ImageTag tags an image in the docker host
func (cli *Client) ImageTag(ctx context.Context, source, target string) error {
func (cli *Client) ImageTag(ctx context.Context, options ImageTagOptions) (ImageTagResult, error) {
source := options.Source
target := options.Target
if _, err := reference.ParseAnyReference(source); err != nil {
return fmt.Errorf("error parsing reference: %q is not a valid repository/tag: %w", source, err)
return ImageTagResult{}, fmt.Errorf("error parsing reference: %q is not a valid repository/tag: %w", source, err)
}
ref, err := reference.ParseNormalizedNamed(target)
if err != nil {
return fmt.Errorf("error parsing reference: %q is not a valid repository/tag: %w", target, err)
return ImageTagResult{}, fmt.Errorf("error parsing reference: %q is not a valid repository/tag: %w", target, err)
}
if _, ok := ref.(reference.Digested); ok {
return errors.New("refusing to create a tag with a digest reference")
return ImageTagResult{}, errors.New("refusing to create a tag with a digest reference")
}
ref = reference.TagNameOnly(ref)
@@ -34,5 +44,5 @@ func (cli *Client) ImageTag(ctx context.Context, source, target string) error {
resp, err := cli.post(ctx, "/images/"+source+"/tag", query, nil, nil)
defer ensureReaderClosed(resp)
return err
return ImageTagResult{}, err
}

View File

@@ -18,7 +18,7 @@ func TestImageTagError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.ImageTag(context.Background(), "image_id", "repo:tag")
_, err = client.ImageTag(context.Background(), ImageTagOptions{Source: "image_id", Target: "repo:tag"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
@@ -28,7 +28,7 @@ func TestImageTagInvalidReference(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.ImageTag(context.Background(), "image_id", "aa/asdf$$^/aa")
_, err = client.ImageTag(context.Background(), ImageTagOptions{Source: "image_id", Target: "aa/asdf$$^/aa"})
assert.Check(t, is.Error(err, `error parsing reference: "aa/asdf$$^/aa" is not a valid repository/tag: invalid reference format`))
}
@@ -43,7 +43,7 @@ func TestImageTagInvalidSourceImageName(t *testing.T) {
for _, repo := range invalidRepos {
t.Run("invalidRepo/"+repo, func(t *testing.T) {
t.Parallel()
err := client.ImageTag(ctx, "busybox", repo)
_, err := client.ImageTag(ctx, ImageTagOptions{Source: "busybox", Target: repo})
assert.Check(t, is.ErrorContains(err, "not a valid repository/tag"))
})
}
@@ -53,26 +53,26 @@ func TestImageTagInvalidSourceImageName(t *testing.T) {
for _, repotag := range invalidTags {
t.Run("invalidTag/"+repotag, func(t *testing.T) {
t.Parallel()
err := client.ImageTag(ctx, "busybox", repotag)
_, err := client.ImageTag(ctx, ImageTagOptions{Source: "busybox", Target: repotag})
assert.Check(t, is.ErrorContains(err, "not a valid repository/tag"))
})
}
t.Run("test repository name begin with '-'", func(t *testing.T) {
t.Parallel()
err := client.ImageTag(ctx, "busybox:latest", "-busybox:test")
_, err := client.ImageTag(ctx, ImageTagOptions{Source: "busybox:latest", Target: "-busybox:test"})
assert.Check(t, is.ErrorContains(err, "error parsing reference"))
})
t.Run("test namespace name begin with '-'", func(t *testing.T) {
t.Parallel()
err := client.ImageTag(ctx, "busybox:latest", "-test/busybox:test")
_, err := client.ImageTag(ctx, ImageTagOptions{Source: "busybox:latest", Target: "-test/busybox:test"})
assert.Check(t, is.ErrorContains(err, "error parsing reference"))
})
t.Run("test index name begin with '-'", func(t *testing.T) {
t.Parallel()
err := client.ImageTag(ctx, "busybox:latest", "-index:5000/busybox:test")
_, err := client.ImageTag(ctx, ImageTagOptions{Source: "busybox:latest", Target: "-index:5000/busybox:test"})
assert.Check(t, is.ErrorContains(err, "error parsing reference"))
})
}
@@ -91,7 +91,7 @@ func TestImageTagHexSource(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusOK, "OK")))
assert.NilError(t, err)
err = client.ImageTag(context.Background(), "0d409d33b27e47423b049f7f863faa08655a8c901749c2b25b93ca67d01a470d", "repo:tag")
_, err = client.ImageTag(context.Background(), ImageTagOptions{Source: "0d409d33b27e47423b049f7f863faa08655a8c901749c2b25b93ca67d01a470d", Target: "repo:tag"})
assert.NilError(t, err)
}
@@ -169,7 +169,7 @@ func TestImageTag(t *testing.T) {
}, nil
}))
assert.NilError(t, err)
err = client.ImageTag(context.Background(), "image_id", tagCase.reference)
_, err = client.ImageTag(context.Background(), ImageTagOptions{Source: "image_id", Target: tagCase.reference})
assert.NilError(t, err)
}
}

View File

@@ -344,7 +344,7 @@ func (s *DockerRegistrySuite) TestBuildCopyFromForcePull(c *testing.T) {
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
// tag the image to upload it to the private registry
ctx := testutil.GetContext(c)
err := apiClient.ImageTag(ctx, "busybox", repoName)
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox", Target: repoName})
assert.Check(c, err)
// push the image to the registry
rc, err := apiClient.ImagePush(ctx, repoName, client.ImagePushOptions{RegistryAuth: "{}"})

View File

@@ -51,7 +51,7 @@ func TestImageInspectUniqueRepoDigests(t *testing.T) {
for _, tag := range []string{"master", "newest"} {
imgName := "busybox:" + tag
err := apiClient.ImageTag(ctx, "busybox", imgName)
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox", Target: imgName})
assert.NilError(t, err)
defer func() {
_, _ = apiClient.ImageRemove(ctx, imgName, client.ImageRemoveOptions{Force: true})

View File

@@ -36,7 +36,7 @@ func TestImagesFilterMultiReference(t *testing.T) {
}
for _, repoTag := range repoTags {
err := apiClient.ImageTag(ctx, "busybox:latest", repoTag)
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: repoTag})
assert.NilError(t, err)
}
@@ -140,7 +140,7 @@ func TestAPIImagesFilters(t *testing.T) {
apiClient := testEnv.APIClient()
for _, n := range []string{"utest:tag1", "utest/docker:tag2", "utest:5000/docker:tag3"} {
err := apiClient.ImageTag(ctx, "busybox:latest", n)
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: n})
assert.NilError(t, err)
}

View File

@@ -74,10 +74,10 @@ func TestPruneLexographicalOrder(t *testing.T) {
tags := []string{"h", "a", "j", "o", "s", "q", "w", "e", "r", "t"}
for _, tag := range tags {
err = apiClient.ImageTag(ctx, id, "busybox:"+tag)
_, err = apiClient.ImageTag(ctx, client.ImageTagOptions{Source: id, Target: "busybox:" + tag})
assert.NilError(t, err)
}
err = apiClient.ImageTag(ctx, id, "busybox:z")
_, err = apiClient.ImageTag(ctx, client.ImageTagOptions{Source: id, Target: "busybox:z"})
assert.NilError(t, err)
_, err = apiClient.ImageRemove(ctx, "busybox:latest", client.ImageRemoveOptions{Force: true})
@@ -127,7 +127,8 @@ func TestPruneDontDeleteUsedImage(t *testing.T) {
// busybox:other tag pointing to the same image.
name: "two tags",
prepare: func(t *testing.T, d *daemon.Daemon, apiClient *client.Client) error {
return apiClient.ImageTag(ctx, "busybox:latest", "busybox:a")
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: "busybox:a"})
return err
},
check: func(t *testing.T, apiClient *client.Client, pruned image.PruneReport) {
if assert.Check(t, is.Len(pruned.ImagesDeleted, 1)) {

View File

@@ -216,7 +216,8 @@ func TestImagePullKeepOldAsDangling(t *testing.T) {
t.Log(inspect1)
assert.NilError(t, apiClient.ImageTag(ctx, "busybox:latest", "alpine:latest"))
_, err = apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: "alpine:latest"})
assert.NilError(t, err)
_, err = apiClient.ImageRemove(ctx, "busybox:latest", client.ImageRemoveOptions{})
assert.NilError(t, err)

View File

@@ -71,7 +71,7 @@ func TestRemoveByDigest(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
err := apiClient.ImageTag(ctx, "busybox", "test-remove-by-digest:latest")
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox", Target: "test-remove-by-digest:latest"})
assert.NilError(t, err)
inspect, err := apiClient.ImageInspect(ctx, "test-remove-by-digest")

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"testing"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@@ -12,23 +13,23 @@ import (
func TestTagUnprefixedRepoByNameOrName(t *testing.T) {
ctx := setupTest(t)
client := testEnv.APIClient()
apiClient := testEnv.APIClient()
// By name
err := client.ImageTag(ctx, "busybox:latest", "testfoobarbaz")
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: "testfoobarbaz"})
assert.NilError(t, err)
// By ID
insp, err := client.ImageInspect(ctx, "busybox")
insp, err := apiClient.ImageInspect(ctx, "busybox")
assert.NilError(t, err)
err = client.ImageTag(ctx, insp.ID, "testfoobarbaz")
_, err = apiClient.ImageTag(ctx, client.ImageTagOptions{Source: insp.ID, Target: "testfoobarbaz"})
assert.NilError(t, err)
}
func TestTagUsingDigestAlgorithmAsName(t *testing.T) {
ctx := setupTest(t)
client := testEnv.APIClient()
err := client.ImageTag(ctx, "busybox:latest", "sha256:sometag")
apiClient := testEnv.APIClient()
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: "sha256:sometag"})
assert.Check(t, is.ErrorContains(err, "refusing to create an ambiguous tag using digest algorithm as name"))
}
@@ -36,14 +37,14 @@ func TestTagUsingDigestAlgorithmAsName(t *testing.T) {
func TestTagValidPrefixedRepo(t *testing.T) {
ctx := setupTest(t)
client := testEnv.APIClient()
apiClient := testEnv.APIClient()
validRepos := []string{"fooo/bar", "fooaa/test", "foooo:t", "HOSTNAME.DOMAIN.COM:443/foo/bar"}
for _, repo := range validRepos {
t.Run(repo, func(t *testing.T) {
t.Parallel()
err := client.ImageTag(ctx, "busybox", repo)
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox", Target: repo})
assert.NilError(t, err)
})
}
@@ -52,9 +53,9 @@ func TestTagValidPrefixedRepo(t *testing.T) {
// tag an image with an existed tag name without -f option should work
func TestTagExistedNameWithoutForce(t *testing.T) {
ctx := setupTest(t)
client := testEnv.APIClient()
apiClient := testEnv.APIClient()
err := client.ImageTag(ctx, "busybox:latest", "busybox:test")
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: "busybox:test"})
assert.NilError(t, err)
}
@@ -62,7 +63,7 @@ func TestTagExistedNameWithoutForce(t *testing.T) {
// ensure all tags result in the same name
func TestTagOfficialNames(t *testing.T) {
ctx := setupTest(t)
client := testEnv.APIClient()
apiClient := testEnv.APIClient()
names := []string{
"docker.io/busybox",
@@ -74,16 +75,16 @@ func TestTagOfficialNames(t *testing.T) {
for _, name := range names {
t.Run("tag from busybox to "+name, func(t *testing.T) {
err := client.ImageTag(ctx, "busybox", name+":latest")
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox", Target: name + ":latest"})
assert.NilError(t, err)
// ensure we don't have multiple tag names.
insp, err := client.ImageInspect(ctx, "busybox")
insp, err := apiClient.ImageInspect(ctx, "busybox")
assert.NilError(t, err)
// TODO(vvoland): Not sure what's actually being tested here. Is is still doing anything useful?
assert.Assert(t, !is.Contains(insp.RepoTags, name)().Success())
err = client.ImageTag(ctx, name+":latest", "test-tag-official-names/foobar:latest")
_, err = apiClient.ImageTag(ctx, client.ImageTagOptions{Source: name + ":latest", Target: "test-tag-official-names/foobar:latest"})
assert.NilError(t, err)
})
}
@@ -92,14 +93,14 @@ func TestTagOfficialNames(t *testing.T) {
// ensure tags can not match digests
func TestTagMatchesDigest(t *testing.T) {
ctx := setupTest(t)
client := testEnv.APIClient()
apiClient := testEnv.APIClient()
digest := "busybox@sha256:abcdef76720241213f5303bda7704ec4c2ef75613173910a56fb1b6e20251507"
// test setting tag fails
err := client.ImageTag(ctx, "busybox:latest", digest)
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: digest})
assert.Check(t, is.ErrorContains(err, "refusing to create a tag with a digest reference"))
// check that no new image matches the digest
_, err = client.ImageInspect(ctx, digest)
_, err = apiClient.ImageInspect(ctx, digest)
assert.Check(t, is.ErrorContains(err, fmt.Sprintf("No such image: %s", digest)))
}

View File

@@ -70,7 +70,7 @@ func FrozenImagesLinux(ctx context.Context, apiClient client.APIClient, images .
for _, img := range loadImages {
if img.srcName != img.destName {
if err := apiClient.ImageTag(ctx, img.srcName, img.destName); err != nil {
if _, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: img.srcName, Target: img.destName}); err != nil {
return errors.Wrapf(err, "failed to tag %s as %s", img.srcName, img.destName)
}
if _, err := apiClient.ImageRemove(ctx, img.srcName, client.ImageRemoveOptions{}); err != nil {
@@ -171,7 +171,7 @@ func pullTagAndRemove(ctx context.Context, apiClient client.APIClient, ref strin
return err
}
if err := apiClient.ImageTag(ctx, ref, tag); err != nil {
if _, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: ref, Target: tag}); err != nil {
return errors.Wrapf(err, "failed to tag %s as %s", ref, tag)
}
_, err = apiClient.ImageRemove(ctx, ref, client.ImageRemoveOptions{})

View File

@@ -115,7 +115,7 @@ type ImageAPIClient interface {
ImagePush(ctx context.Context, ref string, options ImagePushOptions) (ImagePushResponse, error)
ImageRemove(ctx context.Context, image string, options ImageRemoveOptions) (ImageRemoveResult, error)
ImageSearch(ctx context.Context, term string, options ImageSearchOptions) (ImageSearchResult, error)
ImageTag(ctx context.Context, image, ref string) error
ImageTag(ctx context.Context, options ImageTagOptions) (ImageTagResult, error)
ImagesPrune(ctx context.Context, opts ImagePruneOptions) (ImagePruneResult, error)
ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (ImageInspectResult, error)

View File

@@ -9,19 +9,29 @@ import (
"github.com/distribution/reference"
)
type ImageTagOptions struct {
Source string
Target string
}
type ImageTagResult struct{}
// ImageTag tags an image in the docker host
func (cli *Client) ImageTag(ctx context.Context, source, target string) error {
func (cli *Client) ImageTag(ctx context.Context, options ImageTagOptions) (ImageTagResult, error) {
source := options.Source
target := options.Target
if _, err := reference.ParseAnyReference(source); err != nil {
return fmt.Errorf("error parsing reference: %q is not a valid repository/tag: %w", source, err)
return ImageTagResult{}, fmt.Errorf("error parsing reference: %q is not a valid repository/tag: %w", source, err)
}
ref, err := reference.ParseNormalizedNamed(target)
if err != nil {
return fmt.Errorf("error parsing reference: %q is not a valid repository/tag: %w", target, err)
return ImageTagResult{}, fmt.Errorf("error parsing reference: %q is not a valid repository/tag: %w", target, err)
}
if _, ok := ref.(reference.Digested); ok {
return errors.New("refusing to create a tag with a digest reference")
return ImageTagResult{}, errors.New("refusing to create a tag with a digest reference")
}
ref = reference.TagNameOnly(ref)
@@ -34,5 +44,5 @@ func (cli *Client) ImageTag(ctx context.Context, source, target string) error {
resp, err := cli.post(ctx, "/images/"+source+"/tag", query, nil, nil)
defer ensureReaderClosed(resp)
return err
return ImageTagResult{}, err
}