image_pull: fix image resolver
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
This commit is contained in:
parent
63371009ae
commit
87f1ae214f
24 changed files with 3747 additions and 235 deletions
|
@ -2,10 +2,8 @@ package storage
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/copy"
|
||||
|
@ -16,10 +14,14 @@ import (
|
|||
"github.com/containers/image/transports/alltransports"
|
||||
"github.com/containers/image/types"
|
||||
"github.com/containers/storage"
|
||||
distreference "github.com/docker/distribution/reference"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrCannotParseImageID is returned when we try to ResolveNames for an image ID
|
||||
ErrCannotParseImageID = errors.New("cannot parse an image ID")
|
||||
)
|
||||
|
||||
// ImageResult wraps a subset of information about an image: its ID, its names,
|
||||
// and the size, if known, or nil if it isn't.
|
||||
type ImageResult struct {
|
||||
|
@ -328,113 +330,27 @@ func (svc *imageService) isSecureIndex(indexName string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func isValidHostname(hostname string) bool {
|
||||
return hostname != "" && !strings.Contains(hostname, "/") &&
|
||||
(strings.Contains(hostname, ".") ||
|
||||
strings.Contains(hostname, ":") || hostname == "localhost")
|
||||
}
|
||||
|
||||
func isReferenceFullyQualified(reposName reference.Named) bool {
|
||||
indexName, _, _ := splitReposName(reposName)
|
||||
return indexName != ""
|
||||
}
|
||||
|
||||
const (
|
||||
// defaultHostname is the default built-in hostname
|
||||
defaultHostname = "docker.io"
|
||||
// legacyDefaultHostname is automatically converted to DefaultHostname
|
||||
legacyDefaultHostname = "index.docker.io"
|
||||
// defaultRepoPrefix is the prefix used for default repositories in default host
|
||||
defaultRepoPrefix = "library/"
|
||||
)
|
||||
|
||||
// splitReposName breaks a reposName into an index name and remote name
|
||||
func splitReposName(reposName reference.Named) (indexName string, remoteName reference.Named, err error) {
|
||||
var remoteNameStr string
|
||||
indexName, remoteNameStr = distreference.SplitHostname(reposName)
|
||||
if !isValidHostname(indexName) {
|
||||
// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
|
||||
// 'docker.io'
|
||||
indexName = ""
|
||||
remoteName = reposName
|
||||
} else {
|
||||
remoteName, err = withName(remoteNameStr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func validateName(name string) error {
|
||||
if err := validateID(strings.TrimPrefix(name, defaultHostname+"/")); err == nil {
|
||||
return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
||||
|
||||
// validateID checks whether an ID string is a valid image ID.
|
||||
func validateID(id string) error {
|
||||
if ok := validHex.MatchString(id); !ok {
|
||||
return fmt.Errorf("image ID %q is invalid", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// withName returns a named object representing the given string. If the input
|
||||
// is invalid ErrReferenceInvalidFormat will be returned.
|
||||
func withName(name string) (reference.Named, error) {
|
||||
name, err := normalize(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := distreference.WithName(name)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// splitHostname splits a repository name to hostname and remotename string.
|
||||
// If no valid hostname is found, empty string will be returned as a resulting
|
||||
// hostname. Repository name needs to be already validated before.
|
||||
func splitHostname(name string) (hostname, remoteName string) {
|
||||
func splitDockerDomain(name string) (domain, remainder string) {
|
||||
i := strings.IndexRune(name, '/')
|
||||
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
|
||||
hostname, remoteName = "", name
|
||||
domain, remainder = "", name
|
||||
} else {
|
||||
hostname, remoteName = name[:i], name[i+1:]
|
||||
}
|
||||
if hostname == legacyDefaultHostname {
|
||||
hostname = defaultHostname
|
||||
}
|
||||
if hostname == defaultHostname && !strings.ContainsRune(remoteName, '/') {
|
||||
remoteName = defaultRepoPrefix + remoteName
|
||||
domain, remainder = name[:i], name[i+1:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// normalize returns a repository name in its normalized form, meaning it
|
||||
// will contain library/ prefix for official images.
|
||||
func normalize(name string) (string, error) {
|
||||
host, remoteName := splitHostname(name)
|
||||
if strings.ToLower(remoteName) != remoteName {
|
||||
return "", errors.New("invalid reference format: repository name must be lowercase")
|
||||
}
|
||||
if host == defaultHostname {
|
||||
if strings.HasPrefix(remoteName, defaultRepoPrefix) {
|
||||
remoteName = strings.TrimPrefix(remoteName, defaultRepoPrefix)
|
||||
}
|
||||
return host + "/" + remoteName, nil
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (svc *imageService) ResolveNames(imageName string) ([]string, error) {
|
||||
r, err := reference.ParseNormalizedNamed(imageName)
|
||||
// This to prevent any image ID to go through this routine
|
||||
_, err := reference.ParseNormalizedNamed(imageName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "cannot specify 64-byte hexadecimal strings") {
|
||||
return nil, ErrCannotParseImageID
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if isReferenceFullyQualified(r) {
|
||||
domain, remainder := splitDockerDomain(imageName)
|
||||
if domain != "" {
|
||||
// this means the image is already fully qualified
|
||||
return []string{imageName}, nil
|
||||
}
|
||||
|
@ -446,10 +362,13 @@ func (svc *imageService) ResolveNames(imageName string) ([]string, error) {
|
|||
// this means we got an image in the form of "busybox"
|
||||
// we need to use additional registries...
|
||||
// normalize the unqualified image to be domain/repo/image...
|
||||
_, rest := splitDomain(r.Name())
|
||||
images := []string{}
|
||||
for _, r := range svc.registries {
|
||||
images = append(images, filepath.Join(r, rest))
|
||||
rem := remainder
|
||||
if r == "docker.io" && !strings.ContainsRune(remainder, '/') {
|
||||
rem = "library/" + rem
|
||||
}
|
||||
images = append(images, filepath.Join(r, rem))
|
||||
}
|
||||
return images, nil
|
||||
}
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
package storage
|
||||
|
||||
// This is a fork of docker/distribution code to be used when manipulating image
|
||||
// references.
|
||||
// DO NOT EDIT THIS FILE.
|
||||
|
||||
import "regexp"
|
||||
|
||||
var (
|
||||
// alphaNumericRegexp defines the alpha numeric atom, typically a
|
||||
// component of names. This only allows lower case characters and digits.
|
||||
alphaNumericRegexp = match(`[a-z0-9]+`)
|
||||
|
||||
// separatorRegexp defines the separators allowed to be embedded in name
|
||||
// components. This allow one period, one or two underscore and multiple
|
||||
// dashes.
|
||||
separatorRegexp = match(`(?:[._]|__|[-]*)`)
|
||||
|
||||
// nameComponentRegexp restricts registry path component names to start
|
||||
// with at least one letter or number, with following parts able to be
|
||||
// separated by one period, one or two underscore and multiple dashes.
|
||||
nameComponentRegexp = expression(
|
||||
alphaNumericRegexp,
|
||||
optional(repeated(separatorRegexp, alphaNumericRegexp)))
|
||||
|
||||
// domainComponentRegexp restricts the registry domain component of a
|
||||
// repository name to start with a component as defined by domainRegexp
|
||||
// and followed by an optional port.
|
||||
domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
|
||||
|
||||
// domainRegexp defines the structure of potential domain components
|
||||
// that may be part of image names. This is purposely a subset of what is
|
||||
// allowed by DNS to ensure backwards compatibility with Docker image
|
||||
// names.
|
||||
domainRegexp = expression(
|
||||
domainComponentRegexp,
|
||||
optional(repeated(literal(`.`), domainComponentRegexp)),
|
||||
optional(literal(`:`), match(`[0-9]+`)))
|
||||
|
||||
// NameRegexp is the format for the name component of references. The
|
||||
// regexp has capturing groups for the domain and name part omitting
|
||||
// the separating forward slash from either.
|
||||
NameRegexp = expression(
|
||||
optional(domainRegexp, literal(`/`)),
|
||||
nameComponentRegexp,
|
||||
optional(repeated(literal(`/`), nameComponentRegexp)))
|
||||
|
||||
// anchoredNameRegexp is used to parse a name value, capturing the
|
||||
// domain and trailing components.
|
||||
anchoredNameRegexp = anchored(
|
||||
optional(capture(domainRegexp), literal(`/`)),
|
||||
capture(nameComponentRegexp,
|
||||
optional(repeated(literal(`/`), nameComponentRegexp))))
|
||||
|
||||
// IdentifierRegexp is the format for string identifier used as a
|
||||
// content addressable identifier using sha256. These identifiers
|
||||
// are like digests without the algorithm, since sha256 is used.
|
||||
IdentifierRegexp = match(`([a-f0-9]{64})`)
|
||||
|
||||
// ShortIdentifierRegexp is the format used to represent a prefix
|
||||
// of an identifier. A prefix may be used to match a sha256 identifier
|
||||
// within a list of trusted identifiers.
|
||||
ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`)
|
||||
)
|
||||
|
||||
// match compiles the string to a regular expression.
|
||||
var match = regexp.MustCompile
|
||||
|
||||
// literal compiles s into a literal regular expression, escaping any regexp
|
||||
// reserved characters.
|
||||
func literal(s string) *regexp.Regexp {
|
||||
re := match(regexp.QuoteMeta(s))
|
||||
|
||||
if _, complete := re.LiteralPrefix(); !complete {
|
||||
panic("must be a literal")
|
||||
}
|
||||
|
||||
return re
|
||||
}
|
||||
|
||||
func splitDomain(name string) (string, string) {
|
||||
match := anchoredNameRegexp.FindStringSubmatch(name)
|
||||
if len(match) != 3 {
|
||||
return "", name
|
||||
}
|
||||
return match[1], match[2]
|
||||
}
|
||||
|
||||
// expression defines a full expression, where each regular expression must
|
||||
// follow the previous.
|
||||
func expression(res ...*regexp.Regexp) *regexp.Regexp {
|
||||
var s string
|
||||
for _, re := range res {
|
||||
s += re.String()
|
||||
}
|
||||
|
||||
return match(s)
|
||||
}
|
||||
|
||||
// optional wraps the expression in a non-capturing group and makes the
|
||||
// production optional.
|
||||
func optional(res ...*regexp.Regexp) *regexp.Regexp {
|
||||
return match(group(expression(res...)).String() + `?`)
|
||||
}
|
||||
|
||||
// repeated wraps the regexp in a non-capturing group to get one or more
|
||||
// matches.
|
||||
func repeated(res ...*regexp.Regexp) *regexp.Regexp {
|
||||
return match(group(expression(res...)).String() + `+`)
|
||||
}
|
||||
|
||||
// group wraps the regexp in a non-capturing group.
|
||||
func group(res ...*regexp.Regexp) *regexp.Regexp {
|
||||
return match(`(?:` + expression(res...).String() + `)`)
|
||||
}
|
||||
|
||||
// capture wraps the expression in a capturing group.
|
||||
func capture(res ...*regexp.Regexp) *regexp.Regexp {
|
||||
return match(`(` + expression(res...).String() + `)`)
|
||||
}
|
||||
|
||||
// anchored anchors the regular expression by adding start and end delimiters.
|
||||
func anchored(res ...*regexp.Regexp) *regexp.Regexp {
|
||||
return match(`^` + expression(res...).String() + `$`)
|
||||
}
|
84
pkg/storage/image_test.go
Normal file
84
pkg/storage/image_test.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolveNames(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
additionalRegistries []string
|
||||
imageName string
|
||||
expected []string
|
||||
err bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "test unqualified images get correctly qualified in order and correct tag",
|
||||
additionalRegistries: []string{"testregistry.com", "registry.access.redhat.com", "docker.io"},
|
||||
imageName: "openshift3/ose-deployer:sometag",
|
||||
expected: []string{"testregistry.com/openshift3/ose-deployer:sometag", "registry.access.redhat.com/openshift3/ose-deployer:sometag", "docker.io/openshift3/ose-deployer:sometag"},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "test unqualified images get correctly qualified in order and correct digest",
|
||||
additionalRegistries: []string{"testregistry.com", "registry.access.redhat.com", "docker.io"},
|
||||
imageName: "openshift3/ose-deployer@sha256:dc5f67a48da730d67bf4bfb8824ea8a51be26711de090d6d5a1ffff2723168a3",
|
||||
expected: []string{"testregistry.com/openshift3/ose-deployer@sha256:dc5f67a48da730d67bf4bfb8824ea8a51be26711de090d6d5a1ffff2723168a3", "registry.access.redhat.com/openshift3/ose-deployer@sha256:dc5f67a48da730d67bf4bfb8824ea8a51be26711de090d6d5a1ffff2723168a3", "docker.io/openshift3/ose-deployer@sha256:dc5f67a48da730d67bf4bfb8824ea8a51be26711de090d6d5a1ffff2723168a3"},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "test unqualified images get correctly qualified in order",
|
||||
additionalRegistries: []string{"testregistry.com", "registry.access.redhat.com", "docker.io"},
|
||||
imageName: "openshift3/ose-deployer:latest",
|
||||
expected: []string{"testregistry.com/openshift3/ose-deployer:latest", "registry.access.redhat.com/openshift3/ose-deployer:latest", "docker.io/openshift3/ose-deployer:latest"},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "test unqualified images get correctly qualified from official library",
|
||||
additionalRegistries: []string{"testregistry.com", "registry.access.redhat.com", "docker.io"},
|
||||
imageName: "nginx:latest",
|
||||
expected: []string{"testregistry.com/nginx:latest", "registry.access.redhat.com/nginx:latest", "docker.io/library/nginx:latest"},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "test qualified images returns just qualified",
|
||||
additionalRegistries: []string{"testregistry.com", "registry.access.redhat.com", "docker.io"},
|
||||
imageName: "mypersonalregistry.com/nginx:latest",
|
||||
expected: []string{"mypersonalregistry.com/nginx:latest"},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
name: "test we don't have names w/o registries",
|
||||
imageName: "openshift3/ose-deployer:latest",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "test we cannot resolve names from an image ID",
|
||||
imageName: "6ad733544a6317992a6fac4eb19fe1df577d4dec7529efec28a5bd0edad0fd30",
|
||||
err: true,
|
||||
errContains: "cannot parse an image ID",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
svc := &imageService{
|
||||
registries: c.additionalRegistries,
|
||||
}
|
||||
names, err := svc.ResolveNames(c.imageName)
|
||||
if !c.err {
|
||||
require.NoError(t, err, c.name)
|
||||
if !reflect.DeepEqual(names, c.expected) {
|
||||
t.Fatalf("Exepected: %v, Got: %v: %q", c.expected, names, c.name)
|
||||
}
|
||||
} else {
|
||||
require.Error(t, err, c.name)
|
||||
if c.errContains != "" {
|
||||
assert.Contains(t, err.Error(), c.errContains)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue