Skip to content

Commit

Permalink
Merge pull request #716 from zhill/tag_list
Browse files Browse the repository at this point in the history
Adds "docker-list-tags" command to list tags
  • Loading branch information
mtrmac authored Feb 25, 2020
2 parents 7c29094 + 0a91c00 commit d7a2bd7
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 1 deletion.
136 changes: 136 additions & 0 deletions cmd/skopeo/list_tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

import (
"context"
"encoding/json"
"fmt"
"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/pkg/errors"
"github.com/urfave/cli"
"strings"

"io"
)

// tagListOutput is the output format of (skopeo list-tags), primarily so that we can format it with a simple json.MarshalIndent.
type tagListOutput struct {
Repository string
Tags []string
}

type tagsOptions struct {
global *globalOptions
image *imageOptions
}

func tagsCmd(global *globalOptions) cli.Command {
sharedFlags, sharedOpts := sharedImageFlags()
imageFlags, imageOpts := dockerImageFlags(global, sharedOpts, "", "")

opts := tagsOptions{
global: global,
image: imageOpts,
}

return cli.Command{
Name: "list-tags",
Usage: "List tags in the transport/repository specified by the REPOSITORY-NAME",
Description: `
Return the list of tags from the transport/repository "REPOSITORY-NAME"
Supported transports:
docker
See skopeo-list-tags(1) section "REPOSITORY NAMES" for the expected format
`,
ArgsUsage: "REPOSITORY-NAME",
Flags: append(sharedFlags, imageFlags...),
Action: commandAction(opts.run),
}
}

// Customized version of the alltransports.ParseImageName and docker.ParseReference that does not place a default tag in the reference
// Would really love to not have this, but needed to enforce tag-less and digest-less names
func parseDockerRepositoryReference(refString string) (types.ImageReference, error) {
if !strings.HasPrefix(refString, docker.Transport.Name()+"://") {
return nil, errors.Errorf("docker: image reference %s does not start with %s://", refString, docker.Transport.Name())
}

parts := strings.SplitN(refString, ":", 2)
if len(parts) != 2 {
return nil, errors.Errorf(`Invalid image name "%s", expected colon-separated transport:reference`, refString)
}

ref, err := reference.ParseNormalizedNamed(strings.TrimPrefix(parts[1], "//"))
if err != nil {
return nil, err
}

if !reference.IsNameOnly(ref) {
return nil, errors.New(`No tag or digest allowed in reference`)
}

// Checks ok, now return a reference. This is a hack because the tag listing code expects a full image reference even though the tag is ignored
return docker.NewReference(reference.TagNameOnly(ref))
}

// List the tags from a repository contained in the imgRef reference. Any tag value in the reference is ignored
func listDockerTags(ctx context.Context, sys *types.SystemContext, imgRef types.ImageReference) (string, []string, error) {
repositoryName := imgRef.DockerReference().Name()

tags, err := docker.GetRepositoryTags(ctx, sys, imgRef)
if err != nil {
return ``, nil, fmt.Errorf("Error listing repository tags: %v", err)
}
return repositoryName, tags, nil
}

func (opts *tagsOptions) run(args []string, stdout io.Writer) (retErr error) {
ctx, cancel := opts.global.commandTimeoutContext()
defer cancel()

if len(args) != 1 {
return errorShouldDisplayUsage{errors.New("Exactly one non-option argument expected")}
}

sys, err := opts.image.newSystemContext()
if err != nil {
return err
}

transport := alltransports.TransportFromImageName(args[0])
if transport == nil {
return fmt.Errorf("Invalid %q: does not specify a transport", args[0])
}

if transport.Name() != docker.Transport.Name() {
return fmt.Errorf("Unsupported transport '%v' for tag listing. Only '%v' currently supported", transport.Name(), docker.Transport.Name())
}

// Do transport-specific parsing and validation to get an image reference
imgRef, err := parseDockerRepositoryReference(args[0])
if err != nil {
return err
}

repositoryName, tagListing, err := listDockerTags(ctx, sys, imgRef)
if err != nil {
return err
}

outputData := tagListOutput{
Repository: repositoryName,
Tags: tagListing,
}

out, err := json.MarshalIndent(outputData, "", " ")
if err != nil {
return err
}
_, err = fmt.Fprintf(stdout, "%s\n", string(out))

return err
}
56 changes: 56 additions & 0 deletions cmd/skopeo/list_tags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"github.com/containers/image/v5/transports/alltransports"
"github.com/stretchr/testify/assert"
"testing"
)

// Tests the kinds of inputs allowed and expected to the command
func TestDockerRepositoryReferenceParser(t *testing.T) {
for _, test := range [][]string{
{"docker://myhost.com:1000/nginx"}, //no tag
{"docker://myhost.com/nginx"}, //no port or tag
{"docker://somehost.com"}, // Valid default expansion
{"docker://nginx"}, // Valid default expansion
} {

ref, err := parseDockerRepositoryReference(test[0])
expected, err := alltransports.ParseImageName(test[0])
if assert.NoError(t, err, "Could not parse, got error on %v", test[0]) {
assert.Equal(t, expected.DockerReference().Name(), ref.DockerReference().Name(), "Mismatched parse result for input %v", test[0])
}
}

for _, test := range [][]string{
{"oci://somedir"},
{"dir:/somepath"},
{"docker-archive:/tmp/dir"},
{"container-storage:myhost.com/someimage"},
{"docker-daemon:myhost.com/someimage"},
{"docker://myhost.com:1000/nginx:foobar:foobar"}, // Invalid repository ref
{"docker://somehost.com:5000/"}, // no repo
{"docker://myhost.com:1000/nginx:latest"}, //tag not allowed
{"docker://myhost.com:1000/nginx@sha256:abcdef1234567890"}, //digest not allowed
} {
_, err := parseDockerRepositoryReference(test[0])
assert.Error(t, err, test[0])
}
}

func TestDockerRepositoryReferenceParserDrift(t *testing.T) {
for _, test := range [][]string{
{"docker://myhost.com:1000/nginx", "myhost.com:1000/nginx"}, //no tag
{"docker://myhost.com/nginx", "myhost.com/nginx"}, //no port or tag
{"docker://somehost.com", "docker.io/library/somehost.com"}, // Valid default expansion
{"docker://nginx", "docker.io/library/nginx"}, // Valid default expansion
} {

ref, err := parseDockerRepositoryReference(test[0])
ref2, err2 := alltransports.ParseImageName(test[0])

if assert.NoError(t, err, "Could not parse, got error on %v", test[0]) && assert.NoError(t, err2, "Could not parse with regular parser, got error on %v", test[0]) {
assert.Equal(t, ref.DockerReference().String(), ref2.DockerReference().String(), "Different parsing output for input %v. Repo parse = %v, regular parser = %v", test[0], ref, ref2)
}
}
}
1 change: 1 addition & 0 deletions cmd/skopeo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func createApp() (*cli.App, *globalOptions) {
standaloneSignCmd(),
standaloneVerifyCmd(),
untrustedSignatureDumpCmd(),
tagsCmd(&opts),
}
return app, &opts
}
Expand Down
16 changes: 15 additions & 1 deletion completions/bash/skopeo
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@ _skopeo_layers() {
_complete_ "$options_with_args" "$boolean_options"
}

_skopeo_list_repository_tags() {
local options_with_args="
--authfile
--creds
--cert-dir
"

local boolean_options="
--tls-verify
--no-creds
"
_complete_ "$options_with_args" "$boolean_options"
}

_skopeo_skopeo() {
# XXX: Changes here need to be refleceted in the manually expanded
# string in the `case` statement below as well.
Expand Down Expand Up @@ -194,7 +208,7 @@ _cli_bash_autocomplete() {
local counter=1
while [ $counter -lt "$cword" ]; do
case "${words[$counter]}" in
skopeo|copy|inspect|delete|manifest-digest|standalone-sign|standalone-verify|help|h)
skopeo|copy|inspect|delete|manifest-digest|standalone-sign|standalone-verify|help|h|list-repository-tags)
command="${words[$counter]//-/_}"
cpos=$counter
(( cpos++ ))
Expand Down
102 changes: 102 additions & 0 deletions docs/skopeo-list-tags.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
% skopeo-list-tags(1)

## NAME
skopeo\-list\-tags - Return a list of tags the transport-specific image repository

## SYNOPSIS
**skopeo list-tags** _repository-name_

Return a list of tags from _repository-name_ in a registry.

_repository-name_ name of repository to retrieve tag listing from

**--authfile** _path_

Path of the authentication file. Default is ${XDG\_RUNTIME\_DIR}/containers/auth.json, which is set using `podman login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.

**--creds** _username[:password]_ for accessing the registry

**--cert-dir** _path_ Use certificates at _path_ (\*.crt, \*.cert, \*.key) to connect to the registry

**--tls-verify** _bool-value_ Require HTTPS and verify certificates when talking to container registries (defaults to true)

**--no-creds** _bool-value_ Access the registry anonymously.

## REPOSITORY NAMES

Repository names are transport-specific references as each transport may have its own concept of a "repository" and "tags". Currently, only the Docker transport is supported.

This commands refers to repositories using a _transport_`:`_details_ format. The following formats are supported:

**docker://**_docker-repository-reference_
A repository in a registry implementing the "Docker Registry HTTP API V2". By default, uses the authorization state in either `$XDG_RUNTIME_DIR/containers/auth.json`, which is set using `(podman login)`. If the authorization state is not found there, `$HOME/.docker/config.json` is checked, which is set using `(docker login)`.
A _docker-repository-reference_ is of the form: **registryhost:port/repositoryname** which is similar to an _image-reference_ but with no tag or digest allowed as the last component (e.g no `:latest` or `@sha256:xyz`)

Examples of valid docker-repository-references:
"docker.io/myuser/myrepo"
"docker.io/nginx"
"docker.io/library/fedora"
"localhost:5000/myrepository"

Examples of invalid references:
"docker.io/nginx:latest"
"docker.io/myuser/myimage:v1.0"
"docker.io/myuser/myimage@sha256:f48c4cc192f4c3c6a069cb5cca6d0a9e34d6076ba7c214fd0cc3ca60e0af76bb"


## EXAMPLES

### Docker Transport
To get the list of tags in the "fedora" repository from the docker.io registry (the repository name expands to "library/fedora" per docker transport canonical form):
```sh
$ skopeo list-tags docker://docker.io/fedora
{
"Repository": "docker.io/library/fedora",
"Tags": [
"20",
"21",
"22",
"23",
"24",
"25",
"26-modular",
"26",
"27",
"28",
"29",
"30",
"31",
"32",
"branched",
"heisenbug",
"latest",
"modular",
"rawhide"
]
}

```

To list the tags in a local host docker/distribution registry on port 5000, in this case for the "fedora" repository:

```sh
$ skopeo list-tags docker://localhost:5000/fedora
{
"Repository": "localhost:5000/fedora",
"Tags": [
"latest",
"30",
"31"
]
}

```

# SEE ALSO
skopeo(1), podman-login(1), docker-login(1)

## AUTHORS

Zach Hill <[email protected]>

1 change: 1 addition & 0 deletions docs/skopeo.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Most commands refer to container images, using a _transport_`:`_details_ format.
| [skopeo-standalone-sign(1)](skopeo-standalone-sign.1.md) | Sign an image. |
| [skopeo-standalone-verify(1)](skopeo-standalone-verify.1.md)| Verify an image. |
| [skopeo-sync(1)](skopeo-sync.1.md)| Copy images from one or more repositories to a user specified destination. |
| [skopeo-list-tags(1)](skopeo-list-tags.1.md) | List the tags for the given transport/repository. |

## FILES
**/etc/containers/policy.json**
Expand Down

0 comments on commit d7a2bd7

Please sign in to comment.