Add types.ImageReference.PolicyConfiguration{Identity,Namespaces}

This makes the core policy lookup logic Docker-independent,
with the underlying Docker-specific implementation moved to
docker/policyconfiguration/naming.go.

Does not change behavior, only docker: and atomic: transports
currently implement the policy configuration naming, in a compatible
way, and the policy configuration is still not transport-aware.  That
happens next.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This commit is contained in:
Miloslav Trmač 2016-07-14 19:55:37 +02:00
parent 2cfa81f4c1
commit be1e012a0f
15 changed files with 369 additions and 167 deletions

View File

@ -56,6 +56,26 @@ func (ref dirReference) DockerReference() reference.Named {
return nil
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref dirReference) PolicyConfigurationIdentity() string {
return ""
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref dirReference) PolicyConfigurationNamespaces() []string {
return nil
}
// NewImage returns a types.Image for this reference.
func (ref dirReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
src := newImageSource(ref)

View File

@ -73,6 +73,18 @@ func TestReferenceDockerReference(t *testing.T) {
assert.Nil(t, ref.DockerReference())
}
func TestReferencePolicyConfigurationIdentity(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
assert.Equal(t, "", ref.PolicyConfigurationIdentity())
}
func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
assert.Nil(t, ref.PolicyConfigurationNamespaces())
}
func TestReferenceNewImage(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
@ -79,6 +80,30 @@ func (ref dockerReference) DockerReference() reference.Named {
return ref.ref
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref dockerReference) PolicyConfigurationIdentity() string {
res, err := policyconfiguration.DockerReferenceIdentity(ref.ref)
if res == "" || err != nil { // Coverage: Should never happen, NewReference above should refuse values which could cause a failure.
panic(fmt.Sprintf("Internal inconsistency: policyconfiguration.DockerReferenceIdentity returned %#v, %v", res, err))
}
return res
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref dockerReference) PolicyConfigurationNamespaces() []string {
return policyconfiguration.DockerReferenceNamespaces(ref.ref)
}
// NewImage returns a types.Image for this reference.
func (ref dockerReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
return newImage(ref, certPath, tlsVerify)

View File

@ -126,6 +126,24 @@ func TestReferenceDockerReference(t *testing.T) {
}
}
func TestReferencePolicyConfigurationIdentity(t *testing.T) {
// Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference.
ref, err := ParseReference("//busybox")
require.NoError(t, err)
assert.Equal(t, "docker.io/library/busybox:latest", ref.PolicyConfigurationIdentity())
}
func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
// Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference.
ref, err := ParseReference("//busybox")
require.NoError(t, err)
assert.Equal(t, []string{
"docker.io/library/busybox",
"docker.io/library",
"docker.io",
}, ref.PolicyConfigurationNamespaces())
}
func TestReferenceNewImage(t *testing.T) {
ref, err := ParseReference("//busybox")
require.NoError(t, err)

View File

@ -0,0 +1,57 @@
package policyconfiguration
import (
"errors"
"fmt"
"strings"
"github.com/docker/docker/reference"
)
// DockerReferenceIdentity returns a string representation of the reference, suitable for policy lookup,
// as a backend for ImageReference.PolicyConfigurationIdentity.
// The reference must satisfy !reference.IsNameOnly().
func DockerReferenceIdentity(ref reference.Named) (string, error) {
res := ref.FullName()
tagged, isTagged := ref.(reference.NamedTagged)
digested, isDigested := ref.(reference.Canonical)
switch {
case isTagged && isDigested: // This should not happen, docker/reference.ParseNamed drops the tag.
return "", fmt.Errorf("Unexpected Docker reference %s with both a name and a digest", ref.String())
case !isTagged && !isDigested: // This should not happen, the caller is expected to ensure !reference.IsNameOnly()
return "", fmt.Errorf("Internal inconsistency: Docker reference %s with neither a tag nor a digest", ref.String())
case isTagged:
res = res + ":" + tagged.Tag()
case isDigested:
res = res + "@" + digested.Digest().String()
default: // Coverage: The above was supposed to be exhaustive.
return "", errors.New("Internal inconsistency, unexpected default branch")
}
return res, nil
}
// DockerReferenceNamespaces returns a list of other policy configuration namespaces to search,
// as a backend for ImageReference.PolicyConfigurationIdentity.
// The reference must satisfy !reference.IsNameOnly().
func DockerReferenceNamespaces(ref reference.Named) []string {
// Look for a match of the repository, and then of the possible parent
// namespaces. Note that this only happens on the expanded host names
// and repository names, i.e. "busybox" is looked up as "docker.io/library/busybox",
// then in its parent "docker.io/library"; in none of "busybox",
// un-namespaced "library" nor in "" supposedly implicitly representing "library/".
//
// ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last
// iteration matches the host name (for any namespace).
res := []string{}
name := ref.FullName()
for {
res = append(res, name)
lastSlash := strings.LastIndex(name, "/")
if lastSlash == -1 {
break
}
name = name[:lastSlash]
}
return res
}

View File

@ -0,0 +1,91 @@
package policyconfiguration
import (
"strings"
"testing"
"fmt"
"github.com/docker/docker/reference"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestDockerReference tests DockerReferenceIdentity and DockerReferenceNamespaces simulatenously
// to ensure they are consistent.
func TestDockerReference(t *testing.T) {
sha256Digest := "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
// Test both that DockerReferenceIdentity returns the expected value (fullName+suffix),
// and that DockerReferenceNamespaces starts with the expected value (fullName), i.e. that the two functions are
// consistent.
for inputName, expectedNS := range map[string][]string{
"example.com/ns/repo": {"example.com/ns/repo", "example.com/ns", "example.com"},
"example.com/repo": {"example.com/repo", "example.com"},
"localhost/ns/repo": {"localhost/ns/repo", "localhost/ns", "localhost"},
// Note that "localhost" is special here: notlocalhost/repo is parsed as docker.io/notlocalhost.repo:
"localhost/repo": {"localhost/repo", "localhost"},
"notlocalhost/repo": {"docker.io/notlocalhost/repo", "docker.io/notlocalhost", "docker.io"},
"docker.io/ns/repo": {"docker.io/ns/repo", "docker.io/ns", "docker.io"},
"docker.io/library/repo": {"docker.io/library/repo", "docker.io/library", "docker.io"},
"docker.io/repo": {"docker.io/library/repo", "docker.io/library", "docker.io"},
"ns/repo": {"docker.io/ns/repo", "docker.io/ns", "docker.io"},
"library/repo": {"docker.io/library/repo", "docker.io/library", "docker.io"},
"repo": {"docker.io/library/repo", "docker.io/library", "docker.io"},
} {
for inputSuffix, mappedSuffix := range map[string]string{
":tag": ":tag",
sha256Digest: sha256Digest,
// A github.com/distribution/reference value can have a tag and a digest at the same time!
// github.com/docker/reference handles that by dropping the tag. That is not obviously the
// right thing to do, but it is at least reasonable, so test that we keep behaving reasonably.
// This test case should not be construed to make this an API promise.
":tag" + sha256Digest: sha256Digest,
} {
fullInput := inputName + inputSuffix
ref, err := reference.ParseNamed(fullInput)
require.NoError(t, err, fullInput)
identity, err := DockerReferenceIdentity(ref)
require.NoError(t, err, fullInput)
assert.Equal(t, expectedNS[0]+mappedSuffix, identity, fullInput)
ns := DockerReferenceNamespaces(ref)
require.NotNil(t, ns, fullInput)
require.Len(t, ns, len(expectedNS), fullInput)
moreSpecific := identity
for i := range expectedNS {
assert.Equal(t, ns[i], expectedNS[i], fmt.Sprintf("%s item %d", fullInput, i))
assert.True(t, strings.HasPrefix(moreSpecific, ns[i]))
moreSpecific = ns[i]
}
}
}
}
// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time.
type refWithTagAndDigest struct{ reference.Canonical }
func (ref refWithTagAndDigest) Tag() string {
return "notLatest"
}
func TestDockerReferenceIdentity(t *testing.T) {
// TestDockerReference above has tested the core of the functionality, this tests only the failure cases.
// Neither a tag nor digest
parsed, err := reference.ParseNamed("busybox")
require.NoError(t, err)
id, err := DockerReferenceIdentity(parsed)
assert.Equal(t, "", id)
assert.Error(t, err)
// A github.com/distribution/reference value can have a tag and a digest at the same time!
parsed, err = reference.ParseNamed("busybox@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
require.NoError(t, err)
refDigested, ok := parsed.(reference.Canonical)
require.True(t, ok)
tagDigestRef := refWithTagAndDigest{refDigested}
id, err = DockerReferenceIdentity(tagDigestRef)
assert.Equal(t, "", id)
assert.Error(t, err)
}

View File

@ -75,6 +75,26 @@ func (ref ociReference) DockerReference() reference.Named {
return nil
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref ociReference) PolicyConfigurationIdentity() string {
return ""
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref ociReference) PolicyConfigurationNamespaces() []string {
return nil
}
// NewImage returns a types.Image for this reference.
func (ref ociReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
return nil, errors.New("Full Image support not implemented for oci: image names")

View File

@ -118,6 +118,18 @@ func TestReferenceDockerReference(t *testing.T) {
assert.Nil(t, ref.DockerReference())
}
func TestReferencePolicyConfigurationIdentity(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
assert.Equal(t, "", ref.PolicyConfigurationIdentity())
}
func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
assert.Nil(t, ref.PolicyConfigurationNamespaces())
}
func TestReferenceNewImage(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)

View File

@ -7,6 +7,7 @@ import (
"regexp"
"github.com/Sirupsen/logrus"
"github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
@ -111,6 +112,30 @@ func (ref openshiftReference) DockerReference() reference.Named {
return ref.dockerReference
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref openshiftReference) PolicyConfigurationIdentity() string {
res, err := policyconfiguration.DockerReferenceIdentity(ref.dockerReference)
if res == "" || err != nil { // Coverage: Should never happen, NewReference constructs a valid tagged reference.
panic(fmt.Sprintf("Internal inconsistency: policyconfiguration.DockerReferenceIdentity returned %#v, %v", res, err))
}
return res
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref openshiftReference) PolicyConfigurationNamespaces() []string {
return policyconfiguration.DockerReferenceNamespaces(ref.dockerReference)
}
// NewImage returns a types.Image for this reference.
func (ref openshiftReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
return nil, errors.New("Full Image support not implemented for atomic: image names")

View File

@ -71,6 +71,24 @@ func TestReferenceStringWithinTransport(t *testing.T) {
// but that is untested because it depends on per-user configuration.
}
func TestReferencePolicyConfigurationIdentity(t *testing.T) {
// Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference.
ref, err := NewReference(testBaseURL, "ns", "stream", "notlatest")
require.NoError(t, err)
assert.Equal(t, "registry.example.com:8443/ns/stream:notlatest", ref.PolicyConfigurationIdentity())
}
func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
// Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference.
ref, err := NewReference(testBaseURL, "ns", "stream", "notlatest")
require.NoError(t, err)
assert.Equal(t, []string{
"registry.example.com:8443/ns/stream",
"registry.example.com:8443/ns",
"registry.example.com:8443",
}, ref.PolicyConfigurationNamespaces())
}
func TestReferenceNewImage(t *testing.T) {
ref, err := NewReference(testBaseURL, "ns", "stream", "notlatest")
require.NoError(t, err)

View File

@ -7,12 +7,10 @@ package signature
import (
"fmt"
"strings"
"github.com/Sirupsen/logrus"
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
// PolicyRequirementError is an explanatory text for rejecting a signature or an image.
@ -127,71 +125,24 @@ func (pc *PolicyContext) Destroy() error {
return pc.changeState(pcDestroying, pcDestroyed)
}
// fullyExpandedDockerReference converts a reference.Named into a fully expanded format;
// i.e. soft of an opposite to ref.String(), which is a fully canonicalized/minimized format.
// This is guaranteed to be the same as reference.FullName(), with a tag or digest appended, if available.
// FIXME? This feels like it should be provided by skopeo/reference.
func fullyExpandedDockerReference(ref reference.Named) (string, error) {
res := ref.FullName()
tagged, isTagged := ref.(reference.NamedTagged)
digested, isDigested := ref.(reference.Canonical)
// A github.com/distribution/reference value can have a tag and a digest at the same time!
// github.com/docker/reference does not handle that, so fail.
// (Even if it were supported, the semantics of policy namespaces are unclear - should we drop
// the tag or the digest first?)
switch {
case isTagged && isDigested:
// Coverage: This should currently not happen, the way docker/reference sets up types,
// isTagged and isDigested is mutually exclusive.
return "", fmt.Errorf("Names with both a tag and digest are not currently supported")
case isTagged:
res = res + ":" + tagged.Tag()
case isDigested:
res = res + "@" + digested.Digest().String()
default:
// res is already OK.
// requirementsForImageRef selects the appropriate requirements for ref.
func (pc *PolicyContext) requirementsForImageRef(ref types.ImageReference) (PolicyRequirements, error) {
identity := ref.PolicyConfigurationIdentity()
if identity == "" {
return nil, fmt.Errorf("Can not determine policy for image %s with undefined policy configuration identity", transports.ImageName(ref))
}
return res, nil
}
// requirementsForImage selects the appropriate requirements for image.
func (pc *PolicyContext) requirementsForImage(image types.Image) (PolicyRequirements, error) {
ref := image.Reference().DockerReference()
if ref == nil {
return nil, fmt.Errorf("Can not determine policy for image %s with no known Docker reference identity", transports.ImageName(image.Reference()))
}
ref = reference.WithDefaultTag(ref) // This should not be needed, but if we did receive a name-only reference, this is a reasonable thing to do.
// Look for a full match.
fullyExpanded, err := fullyExpandedDockerReference(ref)
if err != nil { // Coverage: This cannot currently happen.
return nil, err
}
if req, ok := pc.Policy.Specific[fullyExpanded]; ok {
logrus.Debugf(" Using specific policy section %s", fullyExpanded)
if req, ok := pc.Policy.Specific[identity]; ok {
logrus.Debugf(" Using specific policy section %s", identity)
return req, nil
}
// Look for a match of the repository, and then of the possible parent
// namespaces. Note that this only happens on the expanded host names
// and repository names, i.e. "busybox" is looked up as "docker.io/library/busybox",
// then in its parent "docker.io/library"; in none of "busybox",
// un-namespaced "library" nor in "" implicitly representing "library/".
//
// ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last
// iteration matches the host name (for any namespace).
name := ref.FullName()
for {
// Look for a match of the possible parent namespaces.
for _, name := range ref.PolicyConfigurationNamespaces() {
if req, ok := pc.Policy.Specific[name]; ok {
logrus.Debugf(" Using specific policy section %s", name)
return req, nil
}
lastSlash := strings.LastIndex(name, "/")
if lastSlash == -1 {
break
}
name = name[:lastSlash]
}
logrus.Debugf(" Using default policy section")
@ -222,9 +173,9 @@ func (pc *PolicyContext) GetSignaturesWithAcceptedAuthor(image types.Image) (sig
}
}()
logrus.Debugf("GetSignaturesWithAcceptedAuthor for image %s", image.Reference().DockerReference())
logrus.Debugf("GetSignaturesWithAcceptedAuthor for image %s:%s", image.Reference().DockerReference())
reqs, err := pc.requirementsForImage(image)
reqs, err := pc.requirementsForImageRef(image.Reference())
if err != nil {
return nil, err
}
@ -308,7 +259,7 @@ func (pc *PolicyContext) IsRunningImageAllowed(image types.Image) (res bool, fin
logrus.Debugf("IsRunningImageAllowed for image %s", image.Reference().DockerReference())
reqs, err := pc.requirementsForImage(image)
reqs, err := pc.requirementsForImageRef(image.Reference())
if err != nil {
return false, err
}

View File

@ -28,6 +28,12 @@ func (ref nameOnlyImageReferenceMock) StringWithinTransport() string {
func (ref nameOnlyImageReferenceMock) DockerReference() reference.Named {
panic("unexpected call to a mock function")
}
func (ref nameOnlyImageReferenceMock) PolicyConfigurationIdentity() string {
panic("unexpected call to a mock function")
}
func (ref nameOnlyImageReferenceMock) PolicyConfigurationNamespaces() []string {
panic("unexpected call to a mock function")
}
func (ref nameOnlyImageReferenceMock) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
panic("unexpected call to a mock function")
}

View File

@ -5,6 +5,7 @@ import (
"os"
"testing"
"github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
"github.com/stretchr/testify/assert"
@ -59,47 +60,8 @@ func TestPolicyContextNewDestroy(t *testing.T) {
assert.NoError(t, err)
}
func TestFullyExpandedDockerReference(t *testing.T) {
sha256Digest := "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
// Test both that fullyExpandedDockerReference returns the expected value (fullName+suffix),
// and that .FullName returns the expected value (fullName), i.e. that the two functions are
// consistent.
for inputName, fullName := range map[string]string{
"example.com/ns/repo": "example.com/ns/repo",
"example.com/repo": "example.com/repo",
"localhost/ns/repo": "localhost/ns/repo",
// Note that "localhost" is special here: notlocalhost/repo is be parsed as docker.io/notlocalhost.repo:
"localhost/repo": "localhost/repo",
"notlocalhost/repo": "docker.io/notlocalhost/repo",
"docker.io/ns/repo": "docker.io/ns/repo",
"docker.io/library/repo": "docker.io/library/repo",
"docker.io/repo": "docker.io/library/repo",
"ns/repo": "docker.io/ns/repo",
"library/repo": "docker.io/library/repo",
"repo": "docker.io/library/repo",
} {
for inputSuffix, mappedSuffix := range map[string]string{
":tag": ":tag",
sha256Digest: sha256Digest,
"": "",
// A github.com/distribution/reference value can have a tag and a digest at the same time!
// github.com/docker/reference handles that by dropping the tag. That is not obviously the
// right thing to do, but it is at least reasonable, so test that we keep behaving reasonably.
// This test case should not be construed to make this an API promise.
":tag" + sha256Digest: sha256Digest,
} {
fullInput := inputName + inputSuffix
ref, err := reference.ParseNamed(fullInput)
require.NoError(t, err)
assert.Equal(t, fullName, ref.FullName(), fullInput)
expanded, err := fullyExpandedDockerReference(ref)
require.NoError(t, err)
assert.Equal(t, fullName+mappedSuffix, expanded, fullInput)
}
}
}
// pcImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference
// and handles PolicyConfigurationIdentity and PolicyConfigurationReference consistently.
type pcImageReferenceMock struct{ ref reference.Named }
func (ref pcImageReferenceMock) Transport() types.ImageTransport {
@ -113,6 +75,22 @@ func (ref pcImageReferenceMock) StringWithinTransport() string {
func (ref pcImageReferenceMock) DockerReference() reference.Named {
return ref.ref
}
func (ref pcImageReferenceMock) PolicyConfigurationIdentity() string {
if ref.ref == nil {
return ""
}
res, err := policyconfiguration.DockerReferenceIdentity(ref.ref)
if res == "" || err != nil {
panic(fmt.Sprintf("Internal inconsistency: policyconfiguration.DockerReferenceIdentity returned %#v, %v", res, err))
}
return res
}
func (ref pcImageReferenceMock) PolicyConfigurationNamespaces() []string {
if ref.ref == nil {
panic("unexpected call to a mock function")
}
return policyconfiguration.DockerReferenceNamespaces(ref.ref)
}
func (ref pcImageReferenceMock) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
panic("unexpected call to a mock function")
}
@ -123,7 +101,7 @@ func (ref pcImageReferenceMock) NewImageDestination(certPath string, tlsVerify b
panic("unexpected call to a mock function")
}
func TestPolicyContextRequirementsForImage(t *testing.T) {
func TestPolicyContextRequirementsForImageRef(t *testing.T) {
ktGPG := SBKeyTypeGPGKeys
prm := NewPRMMatchExact()
@ -135,35 +113,12 @@ func TestPolicyContextRequirementsForImage(t *testing.T) {
// distinct so that we can compare the values and show them when debugging the tests.
for _, scope := range []string{
"unmatched",
"hostname.com",
"hostname.com/namespace",
"hostname.com/namespace/repo",
"hostname.com/namespace/repo:latest",
"hostname.com/namespace/repo:tag2",
"localhost",
"localhost/namespace",
"localhost/namespace/repo",
"localhost/namespace/repo:latest",
"localhost/namespace/repo:tag2",
"deep.com",
"deep.com/n1",
"deep.com/n1/n2",
"deep.com/n1/n2/n3",
"deep.com/n1/n2/n3/repo",
"deep.com/n1/n2/n3/repo:tag2",
"docker.io",
"docker.io/library",
"docker.io/library/busybox",
"docker.io/namespaceindocker",
"docker.io/namespaceindocker/repo",
"docker.io/namespaceindocker/repo:tag2",
// Note: these non-fully-expanded repository names are not matched against canonical (shortened)
// Docker names; they are instead parsed as starting with hostnames.
"busybox",
"library/busybox",
"namespaceindocker",
"namespaceindocker/repo",
"namespaceindocker/repo:tag2",
} {
policy.Specific[scope] = PolicyRequirements{xNewPRSignedByKeyData(ktGPG, []byte(scope), prm)}
}
@ -173,46 +128,16 @@ func TestPolicyContextRequirementsForImage(t *testing.T) {
for input, matched := range map[string]string{
// Full match
"hostname.com/namespace/repo:latest": "hostname.com/namespace/repo:latest",
"hostname.com/namespace/repo:tag2": "hostname.com/namespace/repo:tag2",
"hostname.com/namespace/repo": "hostname.com/namespace/repo:latest",
"localhost/namespace/repo:latest": "localhost/namespace/repo:latest",
"localhost/namespace/repo:tag2": "localhost/namespace/repo:tag2",
"localhost/namespace/repo": "localhost/namespace/repo:latest",
"deep.com/n1/n2/n3/repo:tag2": "deep.com/n1/n2/n3/repo:tag2",
// Repository match
"hostname.com/namespace/repo:notlatest": "hostname.com/namespace/repo",
"localhost/namespace/repo:notlatest": "localhost/namespace/repo",
"deep.com/n1/n2/n3/repo:nottag2": "deep.com/n1/n2/n3/repo",
// Namespace match
"hostname.com/namespace/notrepo:latest": "hostname.com/namespace",
"localhost/namespace/notrepo:latest": "localhost/namespace",
"deep.com/n1/n2/n3/notrepo:tag2": "deep.com/n1/n2/n3",
"deep.com/n1/n2/notn3/repo:tag2": "deep.com/n1/n2",
"deep.com/n1/notn2/n3/repo:tag2": "deep.com/n1",
"deep.com/n1/n2/n3/repo:tag2": "deep.com/n1/n2/n3/repo:tag2",
// Namespace matches
"deep.com/n1/n2/n3/repo:nottag2": "deep.com/n1/n2/n3/repo",
"deep.com/n1/n2/n3/notrepo:tag2": "deep.com/n1/n2/n3",
"deep.com/n1/n2/notn3/repo:tag2": "deep.com/n1/n2",
"deep.com/n1/notn2/n3/repo:tag2": "deep.com/n1",
// Host name match
"hostname.com/notnamespace/repo:latest": "hostname.com",
"localhost/notnamespace/repo:latest": "localhost",
"deep.com/notn1/n2/n3/repo:tag2": "deep.com",
"deep.com/notn1/n2/n3/repo:tag2": "deep.com",
// Default
"this.doesnt/match:anything": "",
"this.doesnt/match-anything/defaulttag": "",
// docker.io canonizalication effects
"docker.io/library/busybox": "docker.io/library/busybox",
"library/busybox": "docker.io/library/busybox",
"busybox": "docker.io/library/busybox",
"docker.io/library/somethinginlibrary": "docker.io/library",
"library/somethinginlibrary": "docker.io/library",
"somethinginlibrary": "docker.io/library",
"docker.io/namespaceindocker/repo:tag2": "docker.io/namespaceindocker/repo:tag2",
"namespaceindocker/repo:tag2": "docker.io/namespaceindocker/repo:tag2",
"docker.io/namespaceindocker/repo:nottag2": "docker.io/namespaceindocker/repo",
"namespaceindocker/repo:nottag2": "docker.io/namespaceindocker/repo",
"docker.io/namespaceindocker/notrepo:tag2": "docker.io/namespaceindocker",
"namespaceindocker/notrepo:tag2": "docker.io/namespaceindocker",
"docker.io/notnamespaceindocker/repo:tag2": "docker.io",
"notnamespaceindocker/repo:tag2": "docker.io",
"this.doesnt/match:anything": "",
} {
var expected PolicyRequirements
if matched != "" {
@ -223,12 +148,12 @@ func TestPolicyContextRequirementsForImage(t *testing.T) {
expected = policy.Default
}
inputRef, err := reference.ParseNamed(input)
ref, err := reference.ParseNamed(input)
require.NoError(t, err)
reqs, err := pc.requirementsForImage(refImageMock{inputRef})
reqs, err := pc.requirementsForImageRef(pcImageReferenceMock{ref})
require.NoError(t, err)
comment := fmt.Sprintf("case %s: %#v", input, reqs[0])
// Do not sue assert.Equal, which would do a deep contents comparison; we want to compare
// Do not use assert.Equal, which would do a deep contents comparison; we want to compare
// the pointers. Also, == does not work on slices; so test that the slices start at the
// same element and have the same length.
assert.True(t, &(reqs[0]) == &(expected[0]), comment)
@ -236,7 +161,7 @@ func TestPolicyContextRequirementsForImage(t *testing.T) {
}
// Image without a Docker reference identity
_, err = pc.requirementsForImage(refImageMock{nil})
_, err = pc.requirementsForImageRef(pcImageReferenceMock{nil})
assert.Error(t, err)
}

View File

@ -95,6 +95,12 @@ func (ref refImageReferenceMock) StringWithinTransport() string {
func (ref refImageReferenceMock) DockerReference() reference.Named {
return ref.Named
}
func (ref refImageReferenceMock) PolicyConfigurationIdentity() string {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) PolicyConfigurationNamespaces() []string {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
panic("unexpected call to a mock function")
}

View File

@ -49,6 +49,22 @@ type ImageReference interface {
// not e.g. after redirect or alias processing), or nil if unknown/not applicable.
DockerReference() reference.Named
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
PolicyConfigurationIdentity() string
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
PolicyConfigurationNamespaces() []string
// NewImage returns a types.Image for this reference.
NewImage(certPath string, tlsVerify bool) (Image, error)
// NewImageSource returns a types.ImageSource for this reference.