Adds "list-tags" command to list tags with no known tag required. Fixes #276

Example: skopeo list-tags docker://docker.io/library/centos
Returns response:
{
  Repository": "docker.io/library/centos",
  "Tags": [
    "6",
    "7",
    ...
  ]
}

Signed-off-by: Zach Hill <zach@anchore.com>
This commit is contained in:
Zach Hill 2018-11-20 23:52:36 -08:00
parent c8e0250903
commit cce44c45d5
7 changed files with 345 additions and 1 deletions

142
cmd/skopeo/list_tags.go Normal file
View File

@ -0,0 +1,142 @@
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/transports"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/docker/distribution/reference"
"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) {
parts := strings.SplitN(refString, ":", 2)
if len(parts) != 2 {
return nil, errors.Errorf(`Invalid image name "%s", expected colon-separated transport:reference`, refString)
}
transport := transports.Get(parts[0])
if transport == nil || transport.Name() != docker.Transport.Name() {
return nil, errors.New("Invalid transport, can only parse docker transport references")
}
if !strings.HasPrefix(parts[1], "//") {
return nil, errors.Errorf("docker: image reference %s does not start with //", parts[1])
}
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))
}
func listDockerTags(ctx context.Context, sys *types.SystemContext, referenceInput string) (string, []string, error) {
imgRef, err := parseDockerRepositoryReference(referenceInput)
if err != nil {
return ``, nil, fmt.Errorf("Cannot parse repository reference: %v", err)
}
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()
var repositoryName string
var tagListing []string
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 errors.New("Invalid transport")
}
if transport.Name() == docker.Transport.Name() {
repositoryName, tagListing, err = listDockerTags(ctx, sys, args[0])
if err != nil {
return err
}
} else {
return fmt.Errorf("Unsupported transport '%v' for tag listing. Only '%v' currently supported", transport.Name(), docker.Transport.Name())
}
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
}

View File

@ -0,0 +1,62 @@
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", "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
{"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
} {
ref, err := parseDockerRepositoryReference(test[0])
if test[1] == "" {
assert.Error(t, err, "Expected error in parsing but no error raised for %v", test[0])
} else {
if assert.NoError(t, err, "Could not parse, got error on %v", test[0]) {
assert.Equal(t, test[1], ref.DockerReference().Name(), "Mismatched parse result for input %v", 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)
}
}
}
func TestUnsupportedRepositoryReferenceParser(t *testing.T) {
for _, test := range [][]string{
{"oci://somedir"},
{"dir:/somepath"},
{"docker-archive:/tmp/dir"},
{"container-storage:myhost.com/someimage"},
{"docker-daemon:myhost.com/someimage"},
} {
_, err := parseDockerRepositoryReference(test[0])
assert.Error(t, err, test[0])
}
}

View File

@ -102,6 +102,7 @@ func createApp() (*cli.App, *globalOptions) {
standaloneSignCmd(), standaloneSignCmd(),
standaloneVerifyCmd(), standaloneVerifyCmd(),
untrustedSignatureDumpCmd(), untrustedSignatureDumpCmd(),
tagsCmd(&opts),
} }
return app, &opts return app, &opts
} }

View File

@ -144,6 +144,20 @@ _skopeo_layers() {
_complete_ "$options_with_args" "$boolean_options" _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() { _skopeo_skopeo() {
# XXX: Changes here need to be refleceted in the manually expanded # XXX: Changes here need to be refleceted in the manually expanded
# string in the `case` statement below as well. # string in the `case` statement below as well.
@ -194,7 +208,7 @@ _cli_bash_autocomplete() {
local counter=1 local counter=1
while [ $counter -lt "$cword" ]; do while [ $counter -lt "$cword" ]; do
case "${words[$counter]}" in 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]//-/_}" command="${words[$counter]//-/_}"
cpos=$counter cpos=$counter
(( cpos++ )) (( cpos++ ))

102
docs/skopeo-list-tags.1.md Normal file
View File

@ -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/myapp",
"Tags": [
"latest",
"v1.1.1",
"v1.2.0"
]
}
```
# SEE ALSO
skopeo(1), podman-login(1), docker-login(1)
## AUTHORS
Zach Hill <zach@anchore.com>

View File

@ -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-sign(1)](skopeo-standalone-sign.1.md) | Sign an image. |
| [skopeo-standalone-verify(1)](skopeo-standalone-verify.1.md)| Verify 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-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 ## FILES
**/etc/containers/policy.json** **/etc/containers/policy.json**

View File

@ -0,0 +1,22 @@
package main
import (
"github.com/go-check/check"
)
func init() {
check.Suite(&TagListSuite{})
}
type TagListSuite struct {
registry *testRegistryV2
}
// Simple tag listing
func (s *TagListSuite) TestListSimple(c *check.C) {
assertSkopeoSucceeds(c, `.*Repository: docker\.io/library/centos.*`, "list-tags", "docker://docker.io/library/centos")
assertSkopeoSucceeds(c, `.*Repository: docker\.io/library/centos.*`, "list-tags", "docker://centos")
assertSkopeoSucceeds(c, `.*Repository: docker\.io/library/centos.*`, "list-tags", "docker://docker.io/centos")
assertSkopeoFails(c, ".*No tag or digest allowed.*", "", "list-tags", "docker://docker.io/centos:7")
assertSkopeoFails(c, ".*Unsupported transport.*", "", "list-tags", "docker-daemon://docker.io/centos:7")
}