Switch to github.com/golang/dep for vendoring

Signed-off-by: Mrunal Patel <mrunalp@gmail.com>
This commit is contained in:
Mrunal Patel 2017-01-31 16:45:59 -08:00
parent d6ab91be27
commit 8e5b17cf13
15431 changed files with 3971413 additions and 8881 deletions

9
vendor/github.com/containers/image/.pullapprove.yml generated vendored Normal file
View file

@ -0,0 +1,9 @@
approve_by_comment: true
approve_regex: '^(Approved|lgtm|LGTM|:shipit:|:star:|:\+1:|:ship:)'
reject_regex: ^Rejected
reset_on_push: false
reviewers:
teams:
- image-maintainers
name: default
required: 2

18
vendor/github.com/containers/image/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,18 @@
---
language: go
sudo: required
notifications:
email: false
go:
- 1.7
install: make deps
script: make .gitvalidation && make validate && make test && make test-skopeo
dist: trusty
os:
- linux
addons:
apt:
packages:
- btrfs-tools
- libdevmapper-dev
- libgpgme11-dev

3
vendor/github.com/containers/image/MAINTAINERS generated vendored Normal file
View file

@ -0,0 +1,3 @@
Antonio Murdaca <runcom@redhat.com> (@runcom)
Brandon Philips <brandon.philips@coreos.com> (@philips)
Miloslav Trmac <mitr@redhat.com> (@mtrmac)

60
vendor/github.com/containers/image/Makefile generated vendored Normal file
View file

@ -0,0 +1,60 @@
.PHONY: all deps test validate lint
# Which github repostiory and branch to use for testing with skopeo
SKOPEO_REPO = projectatomic/skopeo
SKOPEO_BRANCH = master
# Set SUDO=sudo to run container integration tests using sudo.
SUDO =
BUILDTAGS = btrfs_noversion libdm_no_deferred_remove
BUILDFLAGS := -tags "$(BUILDTAGS)"
all: deps .gitvalidation test validate
deps:
go get -t $(BUILDFLAGS) ./...
go get -u $(BUILDFLAGS) github.com/golang/lint/golint
go get $(BUILDFLAGS) github.com/vbatts/git-validation
test:
@go test $(BUILDFLAGS) -cover ./...
# This is not run as part of (make all), but Travis CI does run this.
# Demonstarting a working version of skopeo (possibly with modified SKOPEO_REPO/SKOPEO_BRANCH, e.g.
# make test-skopeo SKOPEO_REPO=runcom/skopeo-1 SKOPEO_BRANCH=oci-3 SUDO=sudo
# ) is a requirement before merging; note that Travis will only test
# the master branch of the upstream repo.
test-skopeo:
@echo === Testing skopeo build
@export GOPATH=$$(mktemp -d) && \
skopeo_path=$${GOPATH}/src/github.com/projectatomic/skopeo && \
vendor_path=$${skopeo_path}/vendor/github.com/containers/image && \
git clone -b $(SKOPEO_BRANCH) https://github.com/$(SKOPEO_REPO) $${skopeo_path} && \
rm -rf $${vendor_path} && cp -r . $${vendor_path} && \
cd $${skopeo_path} && \
make BUILDTAGS="$(BUILDTAGS)" binary-local test-all-local && \
$(SUDO) make check && \
rm -rf $${skopeo_path}
validate: lint
@go vet ./...
@test -z "$$(gofmt -s -l . | tee /dev/stderr)"
lint:
@out="$$(golint ./...)"; \
if [ -n "$$(golint ./...)" ]; then \
echo "$$out"; \
exit 1; \
fi
.PHONY: .gitvalidation
EPOCH_TEST_COMMIT ?= e68e0e1110e64f906f9b482e548f17d73e02e6b1
# When this is running in travis, it will only check the travis commit range
.gitvalidation:
@which git-validation > /dev/null 2>/dev/null || (echo "ERROR: git-validation not found. Consider 'make deps' target" && false)
ifeq ($(TRAVIS),true)
@git-validation -q -run DCO,short-subject,dangling-whitespace
else
@git-validation -q -run DCO,short-subject,dangling-whitespace -range $(EPOCH_TEST_COMMIT)..HEAD
endif

21
vendor/github.com/containers/image/README.md generated vendored Normal file
View file

@ -0,0 +1,21 @@
[![GoDoc](https://godoc.org/github.com/containers/image?status.svg)](https://godoc.org/github.com/containers/image) [![Build Status](https://travis-ci.org/containers/image.svg?branch=master)](https://travis-ci.org/containers/image)
=
`image` is a set of Go libraries aimed at working in various way with containers' images and container image registries.
The containers/image library allows application to pull and push images from container image registries, like the upstream docker registry. It also implements "simple image signing".
The containers/image library also allows you to inspect a repository on a container registry without pulling down the image. This means it fetches the repository's manifest and it is able to show you a `docker inspect`-like json output about a whole repository or a tag. This library, in contrast to `docker inspect`, helps you gather useful information about a repository or a tag without requiring you to run `docker pull`.
The containers/image library also allows you to translate from one image format to another, for example docker container images to OCI images. It also allows you to copy container images between various registries, possibly converting them as necessary, and to sign and verify images.
The [skopeo](https://github.com/projectatomic/skopeo) tool uses the containers/image library and takes advantage of its many features.
## License
ASL 2.0
## Contact
- Mailing list: [containers-dev](https://groups.google.com/forum/?hl=en#!forum/containers-dev)
- IRC: #[container-projects](irc://irc.freenode.net:6667/#container-projects) on freenode.net

View file

@ -0,0 +1,86 @@
package copy
import (
"bytes"
"io"
"io/ioutil"
"os"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDetectCompression(t *testing.T) {
cases := []struct {
filename string
unimplemented bool
}{
{"fixtures/Hello.uncompressed", false},
{"fixtures/Hello.gz", false},
{"fixtures/Hello.bz2", false},
{"fixtures/Hello.xz", true},
}
// The original stream is preserved.
for _, c := range cases {
originalContents, err := ioutil.ReadFile(c.filename)
require.NoError(t, err, c.filename)
stream, err := os.Open(c.filename)
require.NoError(t, err, c.filename)
defer stream.Close()
_, updatedStream, err := detectCompression(stream)
require.NoError(t, err, c.filename)
updatedContents, err := ioutil.ReadAll(updatedStream)
require.NoError(t, err, c.filename)
assert.Equal(t, originalContents, updatedContents, c.filename)
}
// The correct decompressor is chosen, and the result is as expected.
for _, c := range cases {
stream, err := os.Open(c.filename)
require.NoError(t, err, c.filename)
defer stream.Close()
decompressor, updatedStream, err := detectCompression(stream)
require.NoError(t, err, c.filename)
var uncompressedStream io.Reader
switch {
case decompressor == nil:
uncompressedStream = updatedStream
case c.unimplemented:
_, err := decompressor(updatedStream)
assert.Error(t, err)
continue
default:
s, err := decompressor(updatedStream)
require.NoError(t, err)
uncompressedStream = s
}
uncompressedContents, err := ioutil.ReadAll(uncompressedStream)
require.NoError(t, err, c.filename)
assert.Equal(t, []byte("Hello"), uncompressedContents, c.filename)
}
// Empty input is handled reasonably.
decompressor, updatedStream, err := detectCompression(bytes.NewReader([]byte{}))
require.NoError(t, err)
assert.Nil(t, decompressor)
updatedContents, err := ioutil.ReadAll(updatedStream)
require.NoError(t, err)
assert.Equal(t, []byte{}, updatedContents)
// Error reading input
reader, writer := io.Pipe()
defer reader.Close()
writer.CloseWithError(errors.New("Expected error reading input in detectCompression"))
_, _, err = detectCompression(reader)
assert.Error(t, err)
}

123
vendor/github.com/containers/image/copy/copy_test.go generated vendored Normal file
View file

@ -0,0 +1,123 @@
package copy
import (
"bytes"
"io"
"os"
"testing"
"time"
"github.com/pkg/errors"
"github.com/opencontainers/go-digest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewDigestingReader(t *testing.T) {
// Only the failure cases, success is tested in TestDigestingReaderRead below.
source := bytes.NewReader([]byte("abc"))
for _, input := range []digest.Digest{
"abc", // Not algo:hexvalue
"crc32:", // Unknown algorithm, empty value
"crc32:012345678", // Unknown algorithm
"sha256:", // Empty value
"sha256:0", // Invalid hex value
"sha256:01", // Invalid length of hex value
} {
_, err := newDigestingReader(source, input)
assert.Error(t, err, input.String())
}
}
func TestDigestingReaderRead(t *testing.T) {
cases := []struct {
input []byte
digest digest.Digest
}{
{[]byte(""), "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
{[]byte("abc"), "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
{make([]byte, 65537, 65537), "sha256:3266304f31be278d06c3bd3eb9aa3e00c59bedec0a890de466568b0b90b0e01f"},
}
// Valid input
for _, c := range cases {
source := bytes.NewReader(c.input)
reader, err := newDigestingReader(source, c.digest)
require.NoError(t, err, c.digest.String())
dest := bytes.Buffer{}
n, err := io.Copy(&dest, reader)
assert.NoError(t, err, c.digest.String())
assert.Equal(t, int64(len(c.input)), n, c.digest.String())
assert.Equal(t, c.input, dest.Bytes(), c.digest.String())
assert.False(t, reader.validationFailed, c.digest.String())
}
// Modified input
for _, c := range cases {
source := bytes.NewReader(bytes.Join([][]byte{c.input, []byte("x")}, nil))
reader, err := newDigestingReader(source, c.digest)
require.NoError(t, err, c.digest.String())
dest := bytes.Buffer{}
_, err = io.Copy(&dest, reader)
assert.Error(t, err, c.digest.String())
assert.True(t, reader.validationFailed)
}
}
func goDiffIDComputationGoroutineWithTimeout(layerStream io.ReadCloser, decompressor decompressorFunc) *diffIDResult {
ch := make(chan diffIDResult)
go diffIDComputationGoroutine(ch, layerStream, nil)
timeout := time.After(time.Second)
select {
case res := <-ch:
return &res
case <-timeout:
return nil
}
}
func TestDiffIDComputationGoroutine(t *testing.T) {
stream, err := os.Open("fixtures/Hello.uncompressed")
require.NoError(t, err)
res := goDiffIDComputationGoroutineWithTimeout(stream, nil)
require.NotNil(t, res)
assert.NoError(t, res.err)
assert.Equal(t, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969", res.digest.String())
// Error reading input
reader, writer := io.Pipe()
writer.CloseWithError(errors.New("Expected error reading input in diffIDComputationGoroutine"))
res = goDiffIDComputationGoroutineWithTimeout(reader, nil)
require.NotNil(t, res)
assert.Error(t, res.err)
}
func TestComputeDiffID(t *testing.T) {
for _, c := range []struct {
filename string
decompressor decompressorFunc
result digest.Digest
}{
{"fixtures/Hello.uncompressed", nil, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
{"fixtures/Hello.gz", nil, "sha256:0bd4409dcd76476a263b8f3221b4ce04eb4686dec40bfdcc2e86a7403de13609"},
{"fixtures/Hello.gz", gzipDecompressor, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
} {
stream, err := os.Open(c.filename)
require.NoError(t, err, c.filename)
defer stream.Close()
diffID, err := computeDiffID(stream, c.decompressor)
require.NoError(t, err, c.filename)
assert.Equal(t, c.result, diffID)
}
// Error initializing decompression
_, err := computeDiffID(bytes.NewReader([]byte{}), gzipDecompressor)
assert.Error(t, err)
// Error reading input
reader, writer := io.Pipe()
defer reader.Close()
writer.CloseWithError(errors.New("Expected error reading input in computeDiffID"))
_, err = computeDiffID(reader, nil)
assert.Error(t, err)
}

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
Hello

Binary file not shown.

View file

@ -0,0 +1,162 @@
package directory
import (
"bytes"
"io/ioutil"
"os"
"testing"
"github.com/containers/image/types"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDestinationReference(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
dest, err := ref.NewImageDestination(nil)
require.NoError(t, err)
defer dest.Close()
ref2 := dest.Reference()
assert.Equal(t, tmpDir, ref2.StringWithinTransport())
}
func TestGetPutManifest(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
man := []byte("test-manifest")
dest, err := ref.NewImageDestination(nil)
require.NoError(t, err)
defer dest.Close()
err = dest.PutManifest(man)
assert.NoError(t, err)
err = dest.Commit()
assert.NoError(t, err)
src, err := ref.NewImageSource(nil, nil)
require.NoError(t, err)
defer src.Close()
m, mt, err := src.GetManifest()
assert.NoError(t, err)
assert.Equal(t, man, m)
assert.Equal(t, "", mt)
}
func TestGetPutBlob(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
blob := []byte("test-blob")
dest, err := ref.NewImageDestination(nil)
require.NoError(t, err)
defer dest.Close()
compress := dest.ShouldCompressLayers()
assert.False(t, compress)
info, err := dest.PutBlob(bytes.NewReader(blob), types.BlobInfo{Digest: digest.Digest("sha256:digest-test"), Size: int64(9)})
assert.NoError(t, err)
err = dest.Commit()
assert.NoError(t, err)
assert.Equal(t, int64(9), info.Size)
assert.Equal(t, digest.FromBytes(blob), info.Digest)
src, err := ref.NewImageSource(nil, nil)
require.NoError(t, err)
defer src.Close()
rc, size, err := src.GetBlob(info)
assert.NoError(t, err)
defer rc.Close()
b, err := ioutil.ReadAll(rc)
assert.NoError(t, err)
assert.Equal(t, blob, b)
assert.Equal(t, int64(len(blob)), size)
}
// readerFromFunc allows implementing Reader by any function, e.g. a closure.
type readerFromFunc func([]byte) (int, error)
func (fn readerFromFunc) Read(p []byte) (int, error) {
return fn(p)
}
// TestPutBlobDigestFailure simulates behavior on digest verification failure.
func TestPutBlobDigestFailure(t *testing.T) {
const digestErrorString = "Simulated digest error"
const blobDigest = digest.Digest("sha256:test-digest")
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
dirRef, ok := ref.(dirReference)
require.True(t, ok)
blobPath := dirRef.layerPath(blobDigest)
firstRead := true
reader := readerFromFunc(func(p []byte) (int, error) {
_, err := os.Lstat(blobPath)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
if firstRead {
if len(p) > 0 {
firstRead = false
}
for i := 0; i < len(p); i++ {
p[i] = 0xAA
}
return len(p), nil
}
return 0, errors.Errorf(digestErrorString)
})
dest, err := ref.NewImageDestination(nil)
require.NoError(t, err)
defer dest.Close()
_, err = dest.PutBlob(reader, types.BlobInfo{Digest: blobDigest, Size: -1})
assert.Error(t, err)
assert.Contains(t, digestErrorString, err.Error())
err = dest.Commit()
assert.NoError(t, err)
_, err = os.Lstat(blobPath)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
}
func TestGetPutSignatures(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
dest, err := ref.NewImageDestination(nil)
require.NoError(t, err)
defer dest.Close()
signatures := [][]byte{
[]byte("sig1"),
[]byte("sig2"),
}
err = dest.SupportsSignatures()
assert.NoError(t, err)
err = dest.PutSignatures(signatures)
assert.NoError(t, err)
err = dest.Commit()
assert.NoError(t, err)
src, err := ref.NewImageSource(nil, nil)
require.NoError(t, err)
defer src.Close()
sigs, err := src.GetSignatures()
assert.NoError(t, err)
assert.Equal(t, signatures, sigs)
}
func TestSourceReference(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
src, err := ref.NewImageSource(nil, nil)
require.NoError(t, err)
defer src.Close()
ref2 := src.Reference()
assert.Equal(t, tmpDir, ref2.StringWithinTransport())
}

View file

@ -0,0 +1,232 @@
package directory
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/containers/image/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTransportName(t *testing.T) {
assert.Equal(t, "dir", Transport.Name())
}
func TestTransportParseReference(t *testing.T) {
testNewReference(t, Transport.ParseReference)
}
func TestTransportValidatePolicyConfigurationScope(t *testing.T) {
for _, scope := range []string{
"/etc",
"/this/does/not/exist",
} {
err := Transport.ValidatePolicyConfigurationScope(scope)
assert.NoError(t, err, scope)
}
for _, scope := range []string{
"relative/path",
"/double//slashes",
"/has/./dot",
"/has/dot/../dot",
"/trailing/slash/",
"/",
} {
err := Transport.ValidatePolicyConfigurationScope(scope)
assert.Error(t, err, scope)
}
}
func TestNewReference(t *testing.T) {
testNewReference(t, NewReference)
}
// testNewReference is a test shared for Transport.ParseReference and NewReference.
func testNewReference(t *testing.T, fn func(string) (types.ImageReference, error)) {
tmpDir, err := ioutil.TempDir("", "dir-transport-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
for _, path := range []string{
"/",
"/etc",
tmpDir,
"relativepath",
tmpDir + "/thisdoesnotexist",
} {
ref, err := fn(path)
require.NoError(t, err, path)
dirRef, ok := ref.(dirReference)
require.True(t, ok)
assert.Equal(t, path, dirRef.path, path)
}
_, err = fn(tmpDir + "/thisparentdoesnotexist/something")
assert.Error(t, err)
}
// refToTempDir creates a temporary directory and returns a reference to it.
// The caller should
// defer os.RemoveAll(tmpDir)
func refToTempDir(t *testing.T) (ref types.ImageReference, tmpDir string) {
tmpDir, err := ioutil.TempDir("", "dir-transport-test")
require.NoError(t, err)
ref, err = NewReference(tmpDir)
require.NoError(t, err)
return ref, tmpDir
}
func TestReferenceTransport(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
assert.Equal(t, Transport, ref.Transport())
}
func TestReferenceStringWithinTransport(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
assert.Equal(t, tmpDir, ref.StringWithinTransport())
}
func TestReferenceDockerReference(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
assert.Nil(t, ref.DockerReference())
}
func TestReferencePolicyConfigurationIdentity(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity())
// A non-canonical path. Test just one, the various other cases are
// tested in explicitfilepath.ResolvePathToFullyExplicit.
ref, err := NewReference(tmpDir + "/.")
require.NoError(t, err)
assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity())
// "/" as a corner case.
ref, err = NewReference("/")
require.NoError(t, err)
assert.Equal(t, "/", ref.PolicyConfigurationIdentity())
}
func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
// We don't really know enough to make a full equality test here.
ns := ref.PolicyConfigurationNamespaces()
require.NotNil(t, ns)
assert.NotEmpty(t, ns)
assert.Equal(t, filepath.Dir(tmpDir), ns[0])
// Test with a known path which should exist. Test just one non-canonical
// path, the various other cases are tested in explicitfilepath.ResolvePathToFullyExplicit.
//
// It would be nice to test a deeper hierarchy, but it is not obvious what
// deeper path is always available in the various distros, AND is not likely
// to contains a symbolic link.
for _, path := range []string{"/etc/skel", "/etc/skel/./."} {
_, err := os.Lstat(path)
require.NoError(t, err)
ref, err := NewReference(path)
require.NoError(t, err)
ns := ref.PolicyConfigurationNamespaces()
require.NotNil(t, ns)
assert.Equal(t, []string{"/etc"}, ns)
}
// "/" as a corner case.
ref, err := NewReference("/")
require.NoError(t, err)
assert.Equal(t, []string{}, ref.PolicyConfigurationNamespaces())
}
func TestReferenceNewImage(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
dest, err := ref.NewImageDestination(nil)
require.NoError(t, err)
defer dest.Close()
mFixture, err := ioutil.ReadFile("../manifest/fixtures/v2s1.manifest.json")
require.NoError(t, err)
err = dest.PutManifest(mFixture)
assert.NoError(t, err)
err = dest.Commit()
assert.NoError(t, err)
img, err := ref.NewImage(nil)
assert.NoError(t, err)
defer img.Close()
}
func TestReferenceNewImageNoValidManifest(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
dest, err := ref.NewImageDestination(nil)
require.NoError(t, err)
defer dest.Close()
err = dest.PutManifest([]byte(`{"schemaVersion":1}`))
assert.NoError(t, err)
err = dest.Commit()
assert.NoError(t, err)
_, err = ref.NewImage(nil)
assert.Error(t, err)
}
func TestReferenceNewImageSource(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
src, err := ref.NewImageSource(nil, nil)
assert.NoError(t, err)
defer src.Close()
}
func TestReferenceNewImageDestination(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
dest, err := ref.NewImageDestination(nil)
assert.NoError(t, err)
defer dest.Close()
}
func TestReferenceDeleteImage(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
err := ref.DeleteImage(nil)
assert.Error(t, err)
}
func TestReferenceManifestPath(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
dirRef, ok := ref.(dirReference)
require.True(t, ok)
assert.Equal(t, tmpDir+"/manifest.json", dirRef.manifestPath())
}
func TestReferenceLayerPath(t *testing.T) {
const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
dirRef, ok := ref.(dirReference)
require.True(t, ok)
assert.Equal(t, tmpDir+"/"+hex+".tar", dirRef.layerPath("sha256:"+hex))
}
func TestReferenceSignaturePath(t *testing.T) {
ref, tmpDir := refToTempDir(t)
defer os.RemoveAll(tmpDir)
dirRef, ok := ref.(dirReference)
require.True(t, ok)
assert.Equal(t, tmpDir+"/signature-1", dirRef.signaturePath(0))
assert.Equal(t, tmpDir+"/signature-10", dirRef.signaturePath(9))
}

View file

@ -0,0 +1,173 @@
package explicitfilepath
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type pathResolvingTestCase struct {
setup func(*testing.T, string) string
expected string
}
var testCases = []pathResolvingTestCase{
{ // A straightforward subdirectory hierarchy
func(t *testing.T, top string) string {
err := os.MkdirAll(filepath.Join(top, "dir1/dir2/dir3"), 0755)
require.NoError(t, err)
return "dir1/dir2/dir3"
},
"dir1/dir2/dir3",
},
{ // Missing component
func(t *testing.T, top string) string {
return "thisismissing/dir2"
},
"",
},
{ // Symlink on the path
func(t *testing.T, top string) string {
err := os.MkdirAll(filepath.Join(top, "dir1/dir2"), 0755)
require.NoError(t, err)
err = os.Symlink("dir1", filepath.Join(top, "link1"))
require.NoError(t, err)
return "link1/dir2"
},
"dir1/dir2",
},
{ // Trailing symlink
func(t *testing.T, top string) string {
err := os.MkdirAll(filepath.Join(top, "dir1/dir2"), 0755)
require.NoError(t, err)
err = os.Symlink("dir2", filepath.Join(top, "dir1/link2"))
require.NoError(t, err)
return "dir1/link2"
},
"dir1/dir2",
},
{ // Symlink pointing nowhere, as a non-final component
func(t *testing.T, top string) string {
err := os.Symlink("thisismissing", filepath.Join(top, "link1"))
require.NoError(t, err)
return "link1/dir2"
},
"",
},
{ // Trailing symlink pointing nowhere (but note that a missing non-symlink would be accepted)
func(t *testing.T, top string) string {
err := os.Symlink("thisismissing", filepath.Join(top, "link1"))
require.NoError(t, err)
return "link1"
},
"",
},
{ // Relative components in a path
func(t *testing.T, top string) string {
err := os.MkdirAll(filepath.Join(top, "dir1/dir2/dir3"), 0755)
require.NoError(t, err)
return "dir1/./dir2/../dir2/dir3"
},
"dir1/dir2/dir3",
},
{ // Trailing relative components
func(t *testing.T, top string) string {
err := os.MkdirAll(filepath.Join(top, "dir1/dir2"), 0755)
require.NoError(t, err)
return "dir1/dir2/.."
},
"dir1",
},
{ // Relative components in symlink
func(t *testing.T, top string) string {
err := os.MkdirAll(filepath.Join(top, "dir1/dir2"), 0755)
require.NoError(t, err)
err = os.Symlink("../dir1/dir2", filepath.Join(top, "dir1/link2"))
require.NoError(t, err)
return "dir1/link2"
},
"dir1/dir2",
},
{ // Relative component pointing "into" a symlink
func(t *testing.T, top string) string {
err := os.MkdirAll(filepath.Join(top, "dir1/dir2/dir3"), 0755)
require.NoError(t, err)
err = os.Symlink("dir3", filepath.Join(top, "dir1/dir2/link3"))
require.NoError(t, err)
return "dir1/dir2/link3/../.."
},
"dir1",
},
{ // Unreadable directory
func(t *testing.T, top string) string {
err := os.MkdirAll(filepath.Join(top, "unreadable/dir2"), 0755)
require.NoError(t, err)
err = os.Chmod(filepath.Join(top, "unreadable"), 000)
require.NoError(t, err)
return "unreadable/dir2"
},
"",
},
}
func testPathsAreSameFile(t *testing.T, path1, path2, description string) {
fi1, err := os.Stat(path1)
require.NoError(t, err)
fi2, err := os.Stat(path2)
require.NoError(t, err)
assert.True(t, os.SameFile(fi1, fi2), description)
}
func runPathResolvingTestCase(t *testing.T, f func(string) (string, error), c pathResolvingTestCase, suffix string) {
topDir, err := ioutil.TempDir("", "pathResolving")
defer func() {
// Clean up after the "Unreadable directory" case; os.RemoveAll just fails.
_ = os.Chmod(filepath.Join(topDir, "unreadable"), 0755) // Ignore errors, especially if this does not exist.
os.RemoveAll(topDir)
}()
input := c.setup(t, topDir) + suffix // Do not call filepath.Join() on input, it calls filepath.Clean() internally!
description := fmt.Sprintf("%s vs. %s%s", input, c.expected, suffix)
fullOutput, err := ResolvePathToFullyExplicit(topDir + "/" + input)
if c.expected == "" {
assert.Error(t, err, description)
} else {
require.NoError(t, err, input)
fullExpected := topDir + "/" + c.expected + suffix
assert.Equal(t, fullExpected, fullOutput)
// Either the two paths resolve to the same existing file, or to the same name in the same existing parent.
if _, err := os.Lstat(fullExpected); err == nil {
testPathsAreSameFile(t, fullOutput, fullExpected, description)
} else {
require.True(t, os.IsNotExist(err))
_, err := os.Stat(fullOutput)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
parentExpected, fileExpected := filepath.Split(fullExpected)
parentOutput, fileOutput := filepath.Split(fullOutput)
assert.Equal(t, fileExpected, fileOutput)
testPathsAreSameFile(t, parentOutput, parentExpected, description)
}
}
}
func TestResolvePathToFullyExplicit(t *testing.T) {
for _, c := range testCases {
runPathResolvingTestCase(t, ResolvePathToFullyExplicit, c, "")
runPathResolvingTestCase(t, ResolvePathToFullyExplicit, c, "/trailing")
}
}
func TestResolveExistingPathToFullyExplicit(t *testing.T) {
for _, c := range testCases {
runPathResolvingTestCase(t, resolveExistingPathToFullyExplicit, c, "")
}
}

29
vendor/github.com/containers/image/doc.go generated vendored Normal file
View file

@ -0,0 +1,29 @@
// Package image provides libraries and commands to interact with containers images.
//
// package main
//
// import (
// "fmt"
//
// "github.com/containers/image/docker"
// )
//
// func main() {
// ref, err := docker.ParseReference("fedora")
// if err != nil {
// panic(err)
// }
// img, err := ref.NewImage(nil)
// if err != nil {
// panic(err)
// }
// defer img.Close()
// b, _, err := img.Manifest()
// if err != nil {
// panic(err)
// }
// fmt.Printf("%s", string(b))
// }
//
// TODO(runcom)
package image

View file

@ -14,7 +14,7 @@ import (
"github.com/containers/image/docker/reference"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
"github.com/docker/engine-api/client"
"github.com/docker/docker/client"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"golang.org/x/net/context"
@ -95,9 +95,12 @@ func (d *daemonImageDestination) Close() {
if !d.committed {
logrus.Debugf("docker-daemon: Closing tar stream to abort loading")
// In principle, goroutineCancel() should abort the HTTP request and stop the process from continuing.
// In practice, though, https://github.com/docker/engine-api/blob/master/client/transport/cancellable/cancellable.go
// currently just runs the HTTP request to completion in a goroutine, and returns early if the context is canceled
// without terminating the HTTP request at all. So we need this CloseWithError to terminate sending the HTTP request Body
// In practice, though, various HTTP implementations used by client.Client.ImageLoad() (including
// https://github.com/golang/net/blob/master/context/ctxhttp/ctxhttp_pre17.go and the
// net/http version with native Context support in Go 1.7) do not always actually immediately cancel
// the operation: they may process the HTTP request, or a part of it, to completion in a goroutine, and
// return early if the context is canceled without terminating the goroutine at all.
// So we need this CloseWithError to terminate sending the HTTP request Body
// immediately, and hopefully, through terminating the sending which uses "Transfer-Encoding: chunked"" without sending
// the terminating zero-length chunk, prevent the docker daemon from processing the tar stream at all.
// Whether that works or not, closing the PipeWriter seems desirable in any case.
@ -144,6 +147,10 @@ func (d *daemonImageDestination) AcceptsForeignLayerURLs() bool {
// to any other readers for download using the supplied digest.
// If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST 1) fail, and 2) delete any data stored so far.
func (d *daemonImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobInfo) (types.BlobInfo, error) {
if inputInfo.Digest.String() == "" {
return types.BlobInfo{}, errors.Errorf(`Can not stream a blob with unknown digest to "docker-daemon:"`)
}
if ok, size, err := d.HasBlob(inputInfo); err == nil && ok {
return types.BlobInfo{Digest: inputInfo.Digest, Size: size}, nil
}

View file

@ -11,7 +11,7 @@ import (
"github.com/containers/image/manifest"
"github.com/containers/image/types"
"github.com/docker/engine-api/client"
"github.com/docker/docker/client"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"golang.org/x/net/context"

View file

@ -0,0 +1,236 @@
package daemon
import (
"testing"
"github.com/containers/image/docker/reference"
"github.com/containers/image/types"
"github.com/opencontainers/go-digest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
sha256digest = "sha256:" + sha256digestHex
)
func TestTransportName(t *testing.T) {
assert.Equal(t, "docker-daemon", Transport.Name())
}
func TestTransportParseReference(t *testing.T) {
testParseReference(t, Transport.ParseReference)
}
func TestTransportValidatePolicyConfigurationScope(t *testing.T) {
for _, scope := range []string{ // A semi-representative assortment of values; everything is rejected.
sha256digestHex,
sha256digest,
"docker.io/library/busybox:latest",
"docker.io",
"",
} {
err := Transport.ValidatePolicyConfigurationScope(scope)
assert.Error(t, err, scope)
}
}
func TestParseReference(t *testing.T) {
testParseReference(t, ParseReference)
}
// testParseReference is a test shared for Transport.ParseReference and ParseReference.
func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) {
for _, c := range []struct{ input, expectedID, expectedRef string }{
{sha256digest, sha256digest, ""}, // Valid digest format
{"sha512:" + sha256digestHex + sha256digestHex, "", ""}, // Non-digest.Canonical digest
{"sha256:ab", "", ""}, // Invalid digest value (too short)
{sha256digest + "ab", "", ""}, // Invalid digest value (too long)
{"sha256:XX23456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "", ""}, // Invalid digest value
{"UPPERCASEISINVALID", "", ""}, // Invalid reference input
{"busybox", "", ""}, // Missing tag or digest
{"busybox:latest", "", "busybox:latest"}, // Explicit tag
{"busybox@" + sha256digest, "", "busybox@" + sha256digest}, // Explicit digest
// 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.
// FIXME? Instead work extra hard to reject such input?
{"busybox:latest@" + sha256digest, "", "busybox@" + sha256digest}, // Both tag and digest
{"docker.io/library/busybox:latest", "", "busybox:latest"}, // All implied values explicitly specified
} {
ref, err := fn(c.input)
if c.expectedID == "" && c.expectedRef == "" {
assert.Error(t, err, c.input)
} else {
require.NoError(t, err, c.input)
daemonRef, ok := ref.(daemonReference)
require.True(t, ok, c.input)
// If we don't reject the input, the interpretation must be consistent for reference.ParseIDOrReference
dockerID, dockerRef, err := reference.ParseIDOrReference(c.input)
require.NoError(t, err, c.input)
if c.expectedRef == "" {
assert.Equal(t, c.expectedID, daemonRef.id.String(), c.input)
assert.Nil(t, daemonRef.ref, c.input)
assert.Equal(t, c.expectedID, dockerID.String(), c.input)
assert.Nil(t, dockerRef, c.input)
} else {
assert.Equal(t, "", daemonRef.id.String(), c.input)
require.NotNil(t, daemonRef.ref, c.input)
assert.Equal(t, c.expectedRef, daemonRef.ref.String(), c.input)
assert.Equal(t, "", dockerID.String(), c.input)
require.NotNil(t, dockerRef, c.input)
assert.Equal(t, c.expectedRef, dockerRef.String(), c.input)
}
}
}
}
// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time.
type refWithTagAndDigest struct{ reference.Canonical }
func (ref refWithTagAndDigest) Tag() string {
return "notLatest"
}
// A common list of reference formats to test for the various ImageReference methods.
// (For IDs it is much simpler, we simply use them unmodified)
var validNamedReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{
{"busybox:notlatest", "busybox:notlatest", "busybox:notlatest"}, // Explicit tag
{"busybox" + sha256digest, "busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest
{"docker.io/library/busybox:latest", "busybox:latest", "busybox:latest"}, // All implied values explicitly specified
{"example.com/ns/foo:bar", "example.com/ns/foo:bar", "example.com/ns/foo:bar"}, // All values explicitly specified
}
func TestNewReference(t *testing.T) {
// An ID reference.
id, err := digest.Parse(sha256digest)
require.NoError(t, err)
ref, err := NewReference(id, nil)
require.NoError(t, err)
daemonRef, ok := ref.(daemonReference)
require.True(t, ok)
assert.Equal(t, id, daemonRef.id)
assert.Nil(t, daemonRef.ref)
// Named references
for _, c := range validNamedReferenceTestCases {
parsed, err := reference.ParseNamed(c.input)
require.NoError(t, err)
ref, err := NewReference("", parsed)
require.NoError(t, err, c.input)
daemonRef, ok := ref.(daemonReference)
require.True(t, ok, c.input)
assert.Equal(t, "", daemonRef.id.String())
require.NotNil(t, daemonRef.ref)
assert.Equal(t, c.dockerRef, daemonRef.ref.String(), c.input)
}
// Both an ID and a named reference provided
parsed, err := reference.ParseNamed("busybox:latest")
require.NoError(t, err)
_, err = NewReference(id, parsed)
assert.Error(t, err)
// A reference with neither a tag nor digest
parsed, err = reference.ParseNamed("busybox")
require.NoError(t, err)
_, err = NewReference("", parsed)
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@" + sha256digest)
require.NoError(t, err)
refDigested, ok := parsed.(reference.Canonical)
require.True(t, ok)
tagDigestRef := refWithTagAndDigest{refDigested}
_, err = NewReference("", tagDigestRef)
assert.Error(t, err)
}
func TestReferenceTransport(t *testing.T) {
ref, err := ParseReference(sha256digest)
require.NoError(t, err)
assert.Equal(t, Transport, ref.Transport())
ref, err = ParseReference("busybox:latest")
require.NoError(t, err)
assert.Equal(t, Transport, ref.Transport())
}
func TestReferenceStringWithinTransport(t *testing.T) {
ref, err := ParseReference(sha256digest)
require.NoError(t, err)
assert.Equal(t, sha256digest, ref.StringWithinTransport())
for _, c := range validNamedReferenceTestCases {
ref, err := ParseReference(c.input)
require.NoError(t, err, c.input)
stringRef := ref.StringWithinTransport()
assert.Equal(t, c.stringWithinTransport, stringRef, c.input)
// Do one more round to verify that the output can be parsed, to an equal value.
ref2, err := Transport.ParseReference(stringRef)
require.NoError(t, err, c.input)
stringRef2 := ref2.StringWithinTransport()
assert.Equal(t, stringRef, stringRef2, c.input)
}
}
func TestReferenceDockerReference(t *testing.T) {
ref, err := ParseReference(sha256digest)
require.NoError(t, err)
assert.Nil(t, ref.DockerReference())
for _, c := range validNamedReferenceTestCases {
ref, err := ParseReference(c.input)
require.NoError(t, err, c.input)
dockerRef := ref.DockerReference()
require.NotNil(t, dockerRef, c.input)
assert.Equal(t, c.dockerRef, dockerRef.String(), c.input)
}
}
func TestReferencePolicyConfigurationIdentity(t *testing.T) {
ref, err := ParseReference(sha256digest)
require.NoError(t, err)
assert.Equal(t, "", ref.PolicyConfigurationIdentity())
for _, c := range validNamedReferenceTestCases {
ref, err := ParseReference(c.input)
require.NoError(t, err, c.input)
assert.Equal(t, "", ref.PolicyConfigurationIdentity(), c.input)
}
}
func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
ref, err := ParseReference(sha256digest)
require.NoError(t, err)
assert.Empty(t, ref.PolicyConfigurationNamespaces())
for _, c := range validNamedReferenceTestCases {
ref, err := ParseReference(c.input)
require.NoError(t, err, c.input)
assert.Empty(t, ref.PolicyConfigurationNamespaces(), c.input)
}
}
// daemonReference.NewImage, daemonReference.NewImageSource, openshiftReference.NewImageDestination
// untested because just creating the objects immediately connects to the daemon.
func TestReferenceDeleteImage(t *testing.T) {
ref, err := ParseReference(sha256digest)
require.NoError(t, err)
err = ref.DeleteImage(nil)
assert.Error(t, err)
for _, c := range validNamedReferenceTestCases {
ref, err := ParseReference(c.input)
require.NoError(t, err, c.input)
err = ref.DeleteImage(nil)
assert.Error(t, err, c.input)
}
}

View file

@ -45,14 +45,20 @@ var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
// dockerClient is configuration for dealing with a single Docker registry.
type dockerClient struct {
ctx *types.SystemContext
registry string
username string
password string
wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty
scheme string // Cache of a value returned by a successful ping() if not empty
client *http.Client
signatureBase signatureStorageBase
ctx *types.SystemContext
registry string
username string
password string
scheme string // Cache of a value returned by a successful ping() if not empty
client *http.Client
signatureBase signatureStorageBase
challenges []challenge
scope authScope
}
type authScope struct {
remoteName string
actions string
}
// this is cloned from docker/go-connections because upstream docker has changed
@ -147,7 +153,7 @@ func hasFile(files []os.FileInfo, name string) bool {
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool) (*dockerClient, error) {
func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
registry := ref.ref.Hostname()
if registry == dockerHostname {
registry = dockerRegistry
@ -184,6 +190,10 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool)
password: password,
client: client,
signatureBase: sigBase,
scope: authScope{
actions: actions,
remoteName: ref.ref.RemoteName(),
},
}, nil
}
@ -191,12 +201,9 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool)
// url is NOT an absolute URL, but a path relative to the /v2/ top-level API path. The host name and schema is taken from the client or autodetected.
func (c *dockerClient) makeRequest(method, url string, headers map[string][]string, stream io.Reader) (*http.Response, error) {
if c.scheme == "" {
pr, err := c.ping()
if err != nil {
if err := c.ping(); err != nil {
return nil, err
}
c.wwwAuthenticate = pr.WWWAuthenticate
c.scheme = pr.scheme
}
url = fmt.Sprintf(baseURL, c.scheme, c.registry) + url
@ -224,7 +231,7 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
if c.ctx != nil && c.ctx.DockerRegistryUserAgent != "" {
req.Header.Add("User-Agent", c.ctx.DockerRegistryUserAgent)
}
if c.wwwAuthenticate != "" && sendAuth {
if sendAuth {
if err := c.setupRequestAuth(req); err != nil {
return nil, err
}
@ -237,78 +244,30 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
return res, nil
}
// we're using the challenges from the /v2/ ping response and not the one from the destination
// URL in this request because:
//
// 1) docker does that as well
// 2) gcr.io is sending 401 without a WWW-Authenticate header in the real request
//
// debugging: https://github.com/containers/image/pull/211#issuecomment-273426236 and follows up
func (c *dockerClient) setupRequestAuth(req *http.Request) error {
tokens := strings.SplitN(strings.TrimSpace(c.wwwAuthenticate), " ", 2)
if len(tokens) != 2 {
return errors.Errorf("expected 2 tokens in WWW-Authenticate: %d, %s", len(tokens), c.wwwAuthenticate)
if len(c.challenges) == 0 {
return nil
}
switch tokens[0] {
case "Basic":
// assume just one...
challenge := c.challenges[0]
switch challenge.Scheme {
case "basic":
req.SetBasicAuth(c.username, c.password)
return nil
case "Bearer":
// FIXME? This gets a new token for every API request;
// we may be easily able to reuse a previous token, e.g.
// for OpenShift the token only identifies the user and does not vary
// across operations. Should we just try the request first, and
// only get a new token on failure?
// OTOH what to do with the single-use body stream in that case?
// Try performing the request, expecting it to fail.
testReq := *req
// Do not use the body stream, or we couldn't reuse it for the "real" call later.
testReq.Body = nil
testReq.ContentLength = 0
res, err := c.client.Do(&testReq)
if err != nil {
return err
}
chs := parseAuthHeader(res.Header)
// We could end up in this "if" statement if the /v2/ call (during ping)
// returned 401 with a valid WWW-Authenticate=Bearer header.
// That doesn't **always** mean, however, that the specific API request
// (different from /v2/) actually needs to be authorized.
// One example of this _weird_ scenario happens with GCR.io docker
// registries.
if res.StatusCode != http.StatusUnauthorized || chs == nil || len(chs) == 0 {
// With gcr.io, the /v2/ call returns a 401 with a valid WWW-Authenticate=Bearer
// header but the repository could be _public_ (no authorization is needed).
// Hence, the registry response contains no challenges and the status
// code is not 401.
// We just skip this case as it's not standard on docker/distribution
// registries (https://github.com/docker/distribution/blob/master/docs/spec/api.md#api-version-check)
if res.StatusCode != http.StatusUnauthorized {
return nil
}
// gcr.io private repositories pull instead requires us to send user:pass pair in
// order to retrieve a token and setup the correct Bearer token.
// try again one last time with Basic Auth
testReq2 := *req
// Do not use the body stream, or we couldn't reuse it for the "real" call later.
testReq2.Body = nil
testReq2.ContentLength = 0
testReq2.SetBasicAuth(c.username, c.password)
res, err := c.client.Do(&testReq2)
if err != nil {
return err
}
chs = parseAuthHeader(res.Header)
if res.StatusCode != http.StatusUnauthorized || chs == nil || len(chs) == 0 {
// no need for bearer? wtf?
return nil
}
}
// Arbitrarily use the first challenge, there is no reason to expect more than one.
challenge := chs[0]
if challenge.Scheme != "bearer" { // Another artifact of trying to handle WWW-Authenticate before it actually happens.
return errors.Errorf("Unimplemented: WWW-Authenticate Bearer replaced by %#v", challenge.Scheme)
}
case "bearer":
realm, ok := challenge.Parameters["realm"]
if !ok {
return errors.Errorf("missing realm in bearer auth challenge")
}
service, _ := challenge.Parameters["service"] // Will be "" if not present
scope, _ := challenge.Parameters["scope"] // Will be "" if not present
scope := fmt.Sprintf("repository:%s:%s", c.scope.remoteName, c.scope.actions)
token, err := c.getBearerToken(realm, service, scope)
if err != nil {
return err
@ -316,8 +275,7 @@ func (c *dockerClient) setupRequestAuth(req *http.Request) error {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return nil
}
return errors.Errorf("no handler for %s authentication", tokens[0])
// support docker bearer with authconfig's Auth string? see docker2aci
return errors.Errorf("no handler for %s authentication", challenge.Scheme)
}
func (c *dockerClient) getBearerToken(realm, service, scope string) (string, error) {
@ -427,39 +385,31 @@ func getAuth(ctx *types.SystemContext, registry string) (string, string, error)
return "", "", nil
}
type pingResponse struct {
WWWAuthenticate string
APIVersion string
scheme string
}
func (c *dockerClient) ping() (*pingResponse, error) {
ping := func(scheme string) (*pingResponse, error) {
func (c *dockerClient) ping() error {
ping := func(scheme string) error {
url := fmt.Sprintf(baseURL, scheme, c.registry)
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true)
logrus.Debugf("Ping %s err %#v", url, err)
if err != nil {
return nil, err
return err
}
defer resp.Body.Close()
logrus.Debugf("Ping %s status %d", scheme+"://"+c.registry+"/v2/", resp.StatusCode)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
return nil, errors.Errorf("error pinging repository, response code %d", resp.StatusCode)
return errors.Errorf("error pinging repository, response code %d", resp.StatusCode)
}
pr := &pingResponse{}
pr.WWWAuthenticate = resp.Header.Get("WWW-Authenticate")
pr.APIVersion = resp.Header.Get("Docker-Distribution-Api-Version")
pr.scheme = scheme
return pr, nil
c.challenges = parseAuthHeader(resp.Header)
c.scheme = scheme
return nil
}
pr, err := ping("https")
err := ping("https")
if err != nil && c.ctx != nil && c.ctx.DockerInsecureSkipTLSVerify {
pr, err = ping("http")
err = ping("http")
}
if err != nil {
err = errors.Wrap(err, "pinging docker registry returned")
if c.ctx != nil && c.ctx.DockerDisableV1Ping {
return nil, err
return err
}
// best effort to understand if we're talking to a V1 registry
pingV1 := func(scheme string) bool {
@ -484,7 +434,7 @@ func (c *dockerClient) ping() (*pingResponse, error) {
err = ErrV1NotSupported
}
}
return pr, err
return err
}
func getDefaultConfigDir(confPath string) string {

View file

@ -0,0 +1,432 @@
package docker
import (
"encoding/base64"
"encoding/json"
//"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/containers/image/types"
"github.com/containers/storage/pkg/homedir"
)
func TestGetAuth(t *testing.T) {
origHomeDir := homedir.Get()
tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
if err != nil {
t.Fatal(err)
}
t.Logf("using temporary home directory: %q", tmpDir)
// override homedir
os.Setenv(homedir.Key(), tmpDir)
defer func() {
err := os.RemoveAll(tmpDir)
if err != nil {
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
}
os.Setenv(homedir.Key(), origHomeDir)
}()
configDir := filepath.Join(tmpDir, ".docker")
if err := os.Mkdir(configDir, 0750); err != nil {
t.Fatal(err)
}
configPath := filepath.Join(configDir, "config.json")
for _, tc := range []struct {
name string
hostname string
authConfig testAuthConfig
expectedUsername string
expectedPassword string
expectedError error
ctx *types.SystemContext
}{
{
name: "empty hostname",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{"localhost:5000": testAuthConfigData{"bob", "password"}}),
},
{
name: "no auth config",
hostname: "index.docker.io",
},
{
name: "match one",
hostname: "example.org",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}),
expectedUsername: "joe",
expectedPassword: "mypass",
},
{
name: "match none",
hostname: "registry.example.org",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}),
},
{
name: "match docker.io",
hostname: "docker.io",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
"example.org": testAuthConfigData{"example", "org"},
"index.docker.io": testAuthConfigData{"index", "docker.io"},
"docker.io": testAuthConfigData{"docker", "io"},
}),
expectedUsername: "docker",
expectedPassword: "io",
},
{
name: "match docker.io normalized",
hostname: "docker.io",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
"example.org": testAuthConfigData{"bob", "pw"},
"https://index.docker.io/v1": testAuthConfigData{"alice", "wp"},
}),
expectedUsername: "alice",
expectedPassword: "wp",
},
{
name: "normalize registry",
hostname: "https://docker.io/v1",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
"docker.io": testAuthConfigData{"user", "pw"},
"localhost:5000": testAuthConfigData{"joe", "pass"},
}),
expectedUsername: "user",
expectedPassword: "pw",
},
{
name: "match localhost",
hostname: "http://localhost",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
"docker.io": testAuthConfigData{"user", "pw"},
"localhost": testAuthConfigData{"joe", "pass"},
"example.com": testAuthConfigData{"alice", "pwd"},
}),
expectedUsername: "joe",
expectedPassword: "pass",
},
{
name: "match ip",
hostname: "10.10.3.56:5000",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
"10.10.30.45": testAuthConfigData{"user", "pw"},
"localhost": testAuthConfigData{"joe", "pass"},
"10.10.3.56": testAuthConfigData{"alice", "pwd"},
"10.10.3.56:5000": testAuthConfigData{"me", "mine"},
}),
expectedUsername: "me",
expectedPassword: "mine",
},
{
name: "match port",
hostname: "https://localhost:5000",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
"https://127.0.0.1:5000": testAuthConfigData{"user", "pw"},
"http://localhost": testAuthConfigData{"joe", "pass"},
"https://localhost:5001": testAuthConfigData{"alice", "pwd"},
"localhost:5000": testAuthConfigData{"me", "mine"},
}),
expectedUsername: "me",
expectedPassword: "mine",
},
{
name: "use system context",
hostname: "example.org",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
"example.org": testAuthConfigData{"user", "pw"},
}),
expectedUsername: "foo",
expectedPassword: "bar",
ctx: &types.SystemContext{
DockerAuthConfig: &types.DockerAuthConfig{
Username: "foo",
Password: "bar",
},
},
},
} {
contents, err := json.MarshalIndent(&tc.authConfig, "", " ")
if err != nil {
t.Errorf("[%s] failed to marshal authConfig: %v", tc.name, err)
continue
}
if err := ioutil.WriteFile(configPath, contents, 0640); err != nil {
t.Errorf("[%s] failed to write file %q: %v", tc.name, configPath, err)
continue
}
var ctx *types.SystemContext
if tc.ctx != nil {
ctx = tc.ctx
}
username, password, err := getAuth(ctx, tc.hostname)
if err == nil && tc.expectedError != nil {
t.Errorf("[%s] got unexpected non error and username=%q, password=%q", tc.name, username, password)
continue
}
if err != nil && tc.expectedError == nil {
t.Errorf("[%s] got unexpected error: %#+v", tc.name, err)
continue
}
if !reflect.DeepEqual(err, tc.expectedError) {
t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError)
continue
}
if username != tc.expectedUsername {
t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, username, tc.expectedUsername)
}
if password != tc.expectedPassword {
t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, password, tc.expectedPassword)
}
}
}
func TestGetAuthFromLegacyFile(t *testing.T) {
origHomeDir := homedir.Get()
tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
if err != nil {
t.Fatal(err)
}
t.Logf("using temporary home directory: %q", tmpDir)
// override homedir
os.Setenv(homedir.Key(), tmpDir)
defer func() {
err := os.RemoveAll(tmpDir)
if err != nil {
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
}
os.Setenv(homedir.Key(), origHomeDir)
}()
configPath := filepath.Join(tmpDir, ".dockercfg")
for _, tc := range []struct {
name string
hostname string
authConfig testAuthConfig
expectedUsername string
expectedPassword string
expectedError error
}{
{
name: "normalize registry",
hostname: "https://docker.io/v1",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
"docker.io": testAuthConfigData{"user", "pw"},
"localhost:5000": testAuthConfigData{"joe", "pass"},
}),
expectedUsername: "user",
expectedPassword: "pw",
},
{
name: "ignore schema and path",
hostname: "http://index.docker.io/v1",
authConfig: makeTestAuthConfig(testAuthConfigDataMap{
"docker.io/v2": testAuthConfigData{"user", "pw"},
"https://localhost/v1": testAuthConfigData{"joe", "pwd"},
}),
expectedUsername: "user",
expectedPassword: "pw",
},
} {
contents, err := json.MarshalIndent(&tc.authConfig.Auths, "", " ")
if err != nil {
t.Errorf("[%s] failed to marshal authConfig: %v", tc.name, err)
continue
}
if err := ioutil.WriteFile(configPath, contents, 0640); err != nil {
t.Errorf("[%s] failed to write file %q: %v", tc.name, configPath, err)
continue
}
username, password, err := getAuth(nil, tc.hostname)
if err == nil && tc.expectedError != nil {
t.Errorf("[%s] got unexpected non error and username=%q, password=%q", tc.name, username, password)
continue
}
if err != nil && tc.expectedError == nil {
t.Errorf("[%s] got unexpected error: %#+v", tc.name, err)
continue
}
if !reflect.DeepEqual(err, tc.expectedError) {
t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError)
continue
}
if username != tc.expectedUsername {
t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, username, tc.expectedUsername)
}
if password != tc.expectedPassword {
t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, password, tc.expectedPassword)
}
}
}
func TestGetAuthPreferNewConfig(t *testing.T) {
origHomeDir := homedir.Get()
tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
if err != nil {
t.Fatal(err)
}
t.Logf("using temporary home directory: %q", tmpDir)
// override homedir
os.Setenv(homedir.Key(), tmpDir)
defer func() {
err := os.RemoveAll(tmpDir)
if err != nil {
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
}
os.Setenv(homedir.Key(), origHomeDir)
}()
configDir := filepath.Join(tmpDir, ".docker")
if err := os.Mkdir(configDir, 0750); err != nil {
t.Fatal(err)
}
for _, data := range []struct {
path string
ac interface{}
}{
{
filepath.Join(configDir, "config.json"),
makeTestAuthConfig(testAuthConfigDataMap{
"https://index.docker.io/v1/": testAuthConfigData{"alice", "pass"},
}),
},
{
filepath.Join(tmpDir, ".dockercfg"),
makeTestAuthConfig(testAuthConfigDataMap{
"https://index.docker.io/v1/": testAuthConfigData{"bob", "pw"},
}).Auths,
},
} {
contents, err := json.MarshalIndent(&data.ac, "", " ")
if err != nil {
t.Fatalf("failed to marshal authConfig: %v", err)
}
if err := ioutil.WriteFile(data.path, contents, 0640); err != nil {
t.Fatalf("failed to write file %q: %v", data.path, err)
}
}
username, password, err := getAuth(nil, "index.docker.io")
if err != nil {
t.Fatalf("got unexpected error: %#+v", err)
}
if username != "alice" {
t.Fatalf("got unexpected user name: %q != %q", username, "alice")
}
if password != "pass" {
t.Fatalf("got unexpected user name: %q != %q", password, "pass")
}
}
func TestGetAuthFailsOnBadInput(t *testing.T) {
origHomeDir := homedir.Get()
tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
if err != nil {
t.Fatal(err)
}
t.Logf("using temporary home directory: %q", tmpDir)
// override homedir
os.Setenv(homedir.Key(), tmpDir)
defer func() {
err := os.RemoveAll(tmpDir)
if err != nil {
t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err)
}
os.Setenv(homedir.Key(), origHomeDir)
}()
configDir := filepath.Join(tmpDir, ".docker")
if err := os.Mkdir(configDir, 0750); err != nil {
t.Fatal(err)
}
configPath := filepath.Join(configDir, "config.json")
// no config file present
username, password, err := getAuth(nil, "index.docker.io")
if err != nil {
t.Fatalf("got unexpected error: %#+v", err)
}
if len(username) > 0 || len(password) > 0 {
t.Fatalf("got unexpected not empty username/password: %q/%q", username, password)
}
if err := ioutil.WriteFile(configPath, []byte("Json rocks! Unless it doesn't."), 0640); err != nil {
t.Fatalf("failed to write file %q: %v", configPath, err)
}
username, password, err = getAuth(nil, "index.docker.io")
if err == nil {
t.Fatalf("got unexpected non-error: username=%q, password=%q", username, password)
}
if _, ok := err.(*json.SyntaxError); !ok {
t.Fatalf("expected os.PathError, not: %#+v", err)
}
// remove the invalid config file
os.RemoveAll(configPath)
// no config file present
username, password, err = getAuth(nil, "index.docker.io")
if err != nil {
t.Fatalf("got unexpected error: %#+v", err)
}
if len(username) > 0 || len(password) > 0 {
t.Fatalf("got unexpected not empty username/password: %q/%q", username, password)
}
configPath = filepath.Join(tmpDir, ".dockercfg")
if err := ioutil.WriteFile(configPath, []byte("I'm certainly not a json string."), 0640); err != nil {
t.Fatalf("failed to write file %q: %v", configPath, err)
}
username, password, err = getAuth(nil, "index.docker.io")
if err == nil {
t.Fatalf("got unexpected non-error: username=%q, password=%q", username, password)
}
if _, ok := err.(*json.SyntaxError); !ok {
t.Fatalf("expected os.PathError, not: %#+v", err)
}
}
type testAuthConfigData struct {
username string
password string
}
type testAuthConfigDataMap map[string]testAuthConfigData
type testAuthConfigEntry struct {
Auth string `json:"auth,omitempty"`
}
type testAuthConfig struct {
Auths map[string]testAuthConfigEntry `json:"auths"`
}
// encodeAuth creates an auth value from given authConfig data to be stored in auth config file.
// Inspired by github.com/docker/docker/cliconfig/config.go v1.10.3.
func encodeAuth(authConfig *testAuthConfigData) string {
authStr := authConfig.username + ":" + authConfig.password
msg := []byte(authStr)
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
base64.StdEncoding.Encode(encoded, msg)
return string(encoded)
}
func makeTestAuthConfig(authConfigData map[string]testAuthConfigData) testAuthConfig {
ac := testAuthConfig{
Auths: make(map[string]testAuthConfigEntry),
}
for host, data := range authConfigData {
ac.Auths[host] = testAuthConfigEntry{
Auth: encodeAuth(&data),
}
}
return ac
}

View file

@ -17,6 +17,21 @@ import (
"github.com/pkg/errors"
)
var manifestMIMETypes = []string{
// TODO(runcom): we'll add OCI as part of another PR here
manifest.DockerV2Schema2MediaType,
manifest.DockerV2Schema1SignedMediaType,
manifest.DockerV2Schema1MediaType,
}
func supportedManifestMIMETypesMap() map[string]bool {
m := make(map[string]bool, len(manifestMIMETypes))
for _, mt := range manifestMIMETypes {
m[mt] = true
}
return m
}
type dockerImageDestination struct {
ref dockerReference
c *dockerClient
@ -26,7 +41,7 @@ type dockerImageDestination struct {
// newImageDestination creates a new ImageDestination for the specified image reference.
func newImageDestination(ctx *types.SystemContext, ref dockerReference) (types.ImageDestination, error) {
c, err := newDockerClient(ctx, ref, true)
c, err := newDockerClient(ctx, ref, true, "push")
if err != nil {
return nil, err
}
@ -47,12 +62,7 @@ func (d *dockerImageDestination) Close() {
}
func (d *dockerImageDestination) SupportedManifestMIMETypes() []string {
return []string{
// TODO(runcom): we'll add OCI as part of another PR here
manifest.DockerV2Schema2MediaType,
manifest.DockerV2Schema1SignedMediaType,
manifest.DockerV2Schema1MediaType,
}
return manifestMIMETypes
}
// SupportsSignatures returns an error (to be displayed to the user) if the destination certainly can't store signatures.

View file

@ -32,13 +32,24 @@ type dockerImageSource struct {
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
// The caller must call .Close() on the returned ImageSource.
func newImageSource(ctx *types.SystemContext, ref dockerReference, requestedManifestMIMETypes []string) (*dockerImageSource, error) {
c, err := newDockerClient(ctx, ref, false)
c, err := newDockerClient(ctx, ref, false, "pull")
if err != nil {
return nil, err
}
if requestedManifestMIMETypes == nil {
requestedManifestMIMETypes = manifest.DefaultRequestedManifestMIMETypes
}
supportedMIMEs := supportedManifestMIMETypesMap()
acceptableRequestedMIMEs := false
for _, mtrequested := range requestedManifestMIMETypes {
if supportedMIMEs[mtrequested] {
acceptableRequestedMIMEs = true
break
}
}
if !acceptableRequestedMIMEs {
requestedManifestMIMETypes = manifest.DefaultRequestedManifestMIMETypes
}
return &dockerImageSource{
ref: ref,
requestedManifestMIMETypes: requestedManifestMIMETypes,
@ -250,7 +261,7 @@ func (s *dockerImageSource) getOneSignature(url *url.URL) (signature []byte, mis
// deleteImage deletes the named image from the registry, if supported.
func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
c, err := newDockerClient(ctx, ref, true)
c, err := newDockerClient(ctx, ref, true, "push")
if err != nil {
return err
}

View file

@ -0,0 +1,24 @@
package docker
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSimplifyContentType(t *testing.T) {
for _, c := range []struct{ input, expected string }{
{"", ""},
{"application/json", "application/json"},
{"application/json;charset=utf-8", "application/json"},
{"application/json; charset=utf-8", "application/json"},
{"application/json ; charset=utf-8", "application/json"},
{"application/json\t;\tcharset=utf-8", "application/json"},
{"application/json ;charset=utf-8", "application/json"},
{`application/json; charset="utf-8"`, "application/json"},
{"completely invalid", ""},
} {
out := simplifyContentType(c.input)
assert.Equal(t, c.expected, out, c.input)
}
}

View file

@ -0,0 +1,204 @@
package docker
import (
"testing"
"github.com/containers/image/docker/reference"
"github.com/containers/image/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
sha256digest = "@sha256:" + sha256digestHex
)
func TestTransportName(t *testing.T) {
assert.Equal(t, "docker", Transport.Name())
}
func TestTransportParseReference(t *testing.T) {
testParseReference(t, Transport.ParseReference)
}
func TestTransportValidatePolicyConfigurationScope(t *testing.T) {
for _, scope := range []string{
"docker.io/library/busybox" + sha256digest,
"docker.io/library/busybox:notlatest",
"docker.io/library/busybox",
"docker.io/library",
"docker.io",
} {
err := Transport.ValidatePolicyConfigurationScope(scope)
assert.NoError(t, err, scope)
}
}
func TestParseReference(t *testing.T) {
testParseReference(t, ParseReference)
}
// testParseReference is a test shared for Transport.ParseReference and ParseReference.
func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) {
for _, c := range []struct{ input, expected string }{
{"busybox", ""}, // Missing // prefix
{"//busybox:notlatest", "busybox:notlatest"}, // Explicit tag
{"//busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest
{"//busybox", "busybox:latest"}, // Default tag
// 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.
// FIXME? Instead work extra hard to reject such input?
{"//busybox:latest" + sha256digest, "busybox" + sha256digest}, // Both tag and digest
{"//docker.io/library/busybox:latest", "busybox:latest"}, // All implied values explicitly specified
{"//UPPERCASEISINVALID", ""}, // Invalid input
} {
ref, err := fn(c.input)
if c.expected == "" {
assert.Error(t, err, c.input)
} else {
require.NoError(t, err, c.input)
dockerRef, ok := ref.(dockerReference)
require.True(t, ok, c.input)
assert.Equal(t, c.expected, dockerRef.ref.String(), c.input)
}
}
}
// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time.
type refWithTagAndDigest struct{ reference.Canonical }
func (ref refWithTagAndDigest) Tag() string {
return "notLatest"
}
// A common list of reference formats to test for the various ImageReference methods.
var validReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{
{"busybox:notlatest", "busybox:notlatest", "//busybox:notlatest"}, // Explicit tag
{"busybox" + sha256digest, "busybox" + sha256digest, "//busybox" + sha256digest}, // Explicit digest
{"docker.io/library/busybox:latest", "busybox:latest", "//busybox:latest"}, // All implied values explicitly specified
{"example.com/ns/foo:bar", "example.com/ns/foo:bar", "//example.com/ns/foo:bar"}, // All values explicitly specified
}
func TestNewReference(t *testing.T) {
for _, c := range validReferenceTestCases {
parsed, err := reference.ParseNamed(c.input)
require.NoError(t, err)
ref, err := NewReference(parsed)
require.NoError(t, err, c.input)
dockerRef, ok := ref.(dockerReference)
require.True(t, ok, c.input)
assert.Equal(t, c.dockerRef, dockerRef.ref.String(), c.input)
}
// Neither a tag nor digest
parsed, err := reference.ParseNamed("busybox")
require.NoError(t, err)
_, err = NewReference(parsed)
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" + sha256digest)
require.NoError(t, err)
refDigested, ok := parsed.(reference.Canonical)
require.True(t, ok)
tagDigestRef := refWithTagAndDigest{refDigested}
_, err = NewReference(tagDigestRef)
assert.Error(t, err)
}
func TestReferenceTransport(t *testing.T) {
ref, err := ParseReference("//busybox")
require.NoError(t, err)
assert.Equal(t, Transport, ref.Transport())
}
func TestReferenceStringWithinTransport(t *testing.T) {
for _, c := range validReferenceTestCases {
ref, err := ParseReference("//" + c.input)
require.NoError(t, err, c.input)
stringRef := ref.StringWithinTransport()
assert.Equal(t, c.stringWithinTransport, stringRef, c.input)
// Do one more round to verify that the output can be parsed, to an equal value.
ref2, err := Transport.ParseReference(stringRef)
require.NoError(t, err, c.input)
stringRef2 := ref2.StringWithinTransport()
assert.Equal(t, stringRef, stringRef2, c.input)
}
}
func TestReferenceDockerReference(t *testing.T) {
for _, c := range validReferenceTestCases {
ref, err := ParseReference("//" + c.input)
require.NoError(t, err, c.input)
dockerRef := ref.DockerReference()
require.NotNil(t, dockerRef, c.input)
assert.Equal(t, c.dockerRef, dockerRef.String(), c.input)
}
}
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)
img, err := ref.NewImage(&types.SystemContext{RegistriesDirPath: "/this/doesnt/exist"})
assert.NoError(t, err)
defer img.Close()
}
func TestReferenceNewImageSource(t *testing.T) {
ref, err := ParseReference("//busybox")
require.NoError(t, err)
src, err := ref.NewImageSource(&types.SystemContext{RegistriesDirPath: "/this/doesnt/exist"}, nil)
assert.NoError(t, err)
defer src.Close()
}
func TestReferenceNewImageDestination(t *testing.T) {
ref, err := ParseReference("//busybox")
require.NoError(t, err)
dest, err := ref.NewImageDestination(&types.SystemContext{RegistriesDirPath: "/this/doesnt/exist"})
assert.NoError(t, err)
defer dest.Close()
}
func TestReferenceTagOrDigest(t *testing.T) {
for input, expected := range map[string]string{
"//busybox:notlatest": "notlatest",
"//busybox" + sha256digest: "sha256:" + sha256digestHex,
} {
ref, err := ParseReference(input)
require.NoError(t, err, input)
dockerRef, ok := ref.(dockerReference)
require.True(t, ok, input)
tod, err := dockerRef.tagOrDigest()
require.NoError(t, err, input)
assert.Equal(t, expected, tod, input)
}
// Invalid input
ref, err := reference.ParseNamed("busybox")
require.NoError(t, err)
dockerRef := dockerReference{ref: ref}
_, err = dockerRef.tagOrDigest()
assert.Error(t, err)
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,14 @@
docker:
example.com:
sigstore: https://sigstore.example.com
registry.test.example.com:
sigstore: http://registry.test.example.com/sigstore
registry.test.example.com:8888:
sigstore: http://registry.test.example.com:8889/sigstore
sigstore-staging: https://registry.test.example.com:8889/sigstore/specialAPIserverWhichDoesntExist
localhost:
sigstore: file:///home/mitr/mydevelopment1
localhost:8080:
sigstore: file:///home/mitr/mydevelopment2
localhost/invalid/url/test:
sigstore: ":emptyscheme"

View file

@ -0,0 +1,12 @@
default-docker:
sigstore: file:///mnt/companywide/signatures/for/other/repositories
docker:
docker.io/contoso:
sigstore: https://sigstore.contoso.com/fordocker
docker.io/centos:
sigstore: https://sigstore.centos.org/
docker.io/centos/mybetaprooduct:
sigstore: http://localhost:9999/mybetaWIP/sigstore
sigstore-staging: file:///srv/mybetaWIP/sigstore
docker.io/centos/mybetaproduct:latest:
sigstore: https://sigstore.centos.org/

View file

@ -0,0 +1 @@
}

View file

@ -0,0 +1,277 @@
package docker
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/containers/image/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func dockerRefFromString(t *testing.T, s string) dockerReference {
ref, err := ParseReference(s)
require.NoError(t, err, s)
dockerRef, ok := ref.(dockerReference)
require.True(t, ok, s)
return dockerRef
}
func TestConfiguredSignatureStorageBase(t *testing.T) {
// Error reading configuration directory (/dev/null is not a directory)
_, err := configuredSignatureStorageBase(&types.SystemContext{RegistriesDirPath: "/dev/null"},
dockerRefFromString(t, "//busybox"), false)
assert.Error(t, err)
// No match found
emptyDir, err := ioutil.TempDir("", "empty-dir")
require.NoError(t, err)
defer os.RemoveAll(emptyDir)
base, err := configuredSignatureStorageBase(&types.SystemContext{RegistriesDirPath: emptyDir},
dockerRefFromString(t, "//this/is/not/in/the:configuration"), false)
assert.NoError(t, err)
assert.Nil(t, base)
// Invalid URL
_, err = configuredSignatureStorageBase(&types.SystemContext{RegistriesDirPath: "fixtures/registries.d"},
dockerRefFromString(t, "//localhost/invalid/url/test"), false)
assert.Error(t, err)
// Success
base, err = configuredSignatureStorageBase(&types.SystemContext{RegistriesDirPath: "fixtures/registries.d"},
dockerRefFromString(t, "//example.com/my/project"), false)
assert.NoError(t, err)
require.NotNil(t, base)
assert.Equal(t, "https://sigstore.example.com/example.com/my/project", (*url.URL)(base).String())
}
func TestRegistriesDirPath(t *testing.T) {
const nondefaultPath = "/this/is/not/the/default/registries.d"
const variableReference = "$HOME"
const rootPrefix = "/root/prefix"
for _, c := range []struct {
ctx *types.SystemContext
expected string
}{
// The common case
{nil, systemRegistriesDirPath},
// There is a context, but it does not override the path.
{&types.SystemContext{}, systemRegistriesDirPath},
// Path overridden
{&types.SystemContext{RegistriesDirPath: nondefaultPath}, nondefaultPath},
// Root overridden
{
&types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix},
filepath.Join(rootPrefix, systemRegistriesDirPath),
},
// Root and path overrides present simultaneously,
{
&types.SystemContext{
RootForImplicitAbsolutePaths: rootPrefix,
RegistriesDirPath: nondefaultPath,
},
nondefaultPath,
},
// No environment expansion happens in the overridden paths
{&types.SystemContext{RegistriesDirPath: variableReference}, variableReference},
} {
path := registriesDirPath(c.ctx)
assert.Equal(t, c.expected, path)
}
}
func TestLoadAndMergeConfig(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "merge-config")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// No registries.d exists
config, err := loadAndMergeConfig(filepath.Join(tmpDir, "thisdoesnotexist"))
require.NoError(t, err)
assert.Equal(t, &registryConfiguration{Docker: map[string]registryNamespace{}}, config)
// Empty registries.d directory
emptyDir := filepath.Join(tmpDir, "empty")
err = os.Mkdir(emptyDir, 0755)
require.NoError(t, err)
config, err = loadAndMergeConfig(emptyDir)
require.NoError(t, err)
assert.Equal(t, &registryConfiguration{Docker: map[string]registryNamespace{}}, config)
// Unreadable registries.d directory
unreadableDir := filepath.Join(tmpDir, "unreadable")
err = os.Mkdir(unreadableDir, 0000)
require.NoError(t, err)
config, err = loadAndMergeConfig(unreadableDir)
assert.Error(t, err)
// An unreadable file in a registries.d directory
unreadableFileDir := filepath.Join(tmpDir, "unreadableFile")
err = os.Mkdir(unreadableFileDir, 0755)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(unreadableFileDir, "0.yaml"), []byte("{}"), 0644)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(unreadableFileDir, "1.yaml"), nil, 0000)
require.NoError(t, err)
config, err = loadAndMergeConfig(unreadableFileDir)
assert.Error(t, err)
// Invalid YAML
invalidYAMLDir := filepath.Join(tmpDir, "invalidYAML")
err = os.Mkdir(invalidYAMLDir, 0755)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(invalidYAMLDir, "0.yaml"), []byte("}"), 0644)
require.NoError(t, err)
config, err = loadAndMergeConfig(invalidYAMLDir)
assert.Error(t, err)
// Duplicate DefaultDocker
duplicateDefault := filepath.Join(tmpDir, "duplicateDefault")
err = os.Mkdir(duplicateDefault, 0755)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(duplicateDefault, "0.yaml"),
[]byte("default-docker:\n sigstore: file:////tmp/something"), 0644)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(duplicateDefault, "1.yaml"),
[]byte("default-docker:\n sigstore: file:////tmp/different"), 0644)
require.NoError(t, err)
config, err = loadAndMergeConfig(duplicateDefault)
require.Error(t, err)
assert.Contains(t, err.Error(), "0.yaml")
assert.Contains(t, err.Error(), "1.yaml")
// Duplicate DefaultDocker
duplicateNS := filepath.Join(tmpDir, "duplicateNS")
err = os.Mkdir(duplicateNS, 0755)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(duplicateNS, "0.yaml"),
[]byte("docker:\n example.com:\n sigstore: file:////tmp/something"), 0644)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(duplicateNS, "1.yaml"),
[]byte("docker:\n example.com:\n sigstore: file:////tmp/different"), 0644)
require.NoError(t, err)
config, err = loadAndMergeConfig(duplicateNS)
assert.Error(t, err)
assert.Contains(t, err.Error(), "0.yaml")
assert.Contains(t, err.Error(), "1.yaml")
// A fully worked example, including an empty-dictionary file and a non-.yaml file
config, err = loadAndMergeConfig("fixtures/registries.d")
require.NoError(t, err)
assert.Equal(t, &registryConfiguration{
DefaultDocker: &registryNamespace{SigStore: "file:///mnt/companywide/signatures/for/other/repositories"},
Docker: map[string]registryNamespace{
"example.com": {SigStore: "https://sigstore.example.com"},
"registry.test.example.com": {SigStore: "http://registry.test.example.com/sigstore"},
"registry.test.example.com:8888": {SigStore: "http://registry.test.example.com:8889/sigstore", SigStoreStaging: "https://registry.test.example.com:8889/sigstore/specialAPIserverWhichDoesntExist"},
"localhost": {SigStore: "file:///home/mitr/mydevelopment1"},
"localhost:8080": {SigStore: "file:///home/mitr/mydevelopment2"},
"localhost/invalid/url/test": {SigStore: ":emptyscheme"},
"docker.io/contoso": {SigStore: "https://sigstore.contoso.com/fordocker"},
"docker.io/centos": {SigStore: "https://sigstore.centos.org/"},
"docker.io/centos/mybetaprooduct": {
SigStore: "http://localhost:9999/mybetaWIP/sigstore",
SigStoreStaging: "file:///srv/mybetaWIP/sigstore",
},
"docker.io/centos/mybetaproduct:latest": {SigStore: "https://sigstore.centos.org/"},
},
}, config)
}
func TestRegistryConfigurationSignaureTopLevel(t *testing.T) {
config := registryConfiguration{
DefaultDocker: &registryNamespace{SigStore: "=default", SigStoreStaging: "=default+w"},
Docker: map[string]registryNamespace{},
}
for _, ns := range []string{
"localhost",
"localhost:5000",
"example.com",
"example.com/ns1",
"example.com/ns1/ns2",
"example.com/ns1/ns2/repo",
"example.com/ns1/ns2/repo:notlatest",
} {
config.Docker[ns] = registryNamespace{SigStore: ns, SigStoreStaging: ns + "+w"}
}
for _, c := range []struct{ input, expected string }{
{"example.com/ns1/ns2/repo:notlatest", "example.com/ns1/ns2/repo:notlatest"},
{"example.com/ns1/ns2/repo:unmatched", "example.com/ns1/ns2/repo"},
{"example.com/ns1/ns2/notrepo:notlatest", "example.com/ns1/ns2"},
{"example.com/ns1/notns2/repo:notlatest", "example.com/ns1"},
{"example.com/notns1/ns2/repo:notlatest", "example.com"},
{"unknown.example.com/busybox", "=default"},
{"localhost:5000/busybox", "localhost:5000"},
{"localhost/busybox", "localhost"},
{"localhost:9999/busybox", "=default"},
} {
dr := dockerRefFromString(t, "//"+c.input)
res := config.signatureTopLevel(dr, false)
assert.Equal(t, c.expected, res, c.input)
res = config.signatureTopLevel(dr, true) // test that forWriting is correctly propagated
assert.Equal(t, c.expected+"+w", res, c.input)
}
config = registryConfiguration{
Docker: map[string]registryNamespace{
"unmatched": {SigStore: "a", SigStoreStaging: "b"},
},
}
dr := dockerRefFromString(t, "//thisisnotmatched")
res := config.signatureTopLevel(dr, false)
assert.Equal(t, "", res)
res = config.signatureTopLevel(dr, true)
assert.Equal(t, "", res)
}
func TestRegistryNamespaceSignatureTopLevel(t *testing.T) {
for _, c := range []struct {
ns registryNamespace
forWriting bool
expected string
}{
{registryNamespace{SigStoreStaging: "a", SigStore: "b"}, true, "a"},
{registryNamespace{SigStoreStaging: "a", SigStore: "b"}, false, "b"},
{registryNamespace{SigStore: "b"}, true, "b"},
{registryNamespace{SigStore: "b"}, false, "b"},
{registryNamespace{SigStoreStaging: "a"}, true, "a"},
{registryNamespace{SigStoreStaging: "a"}, false, ""},
{registryNamespace{}, true, ""},
{registryNamespace{}, false, ""},
} {
res := c.ns.signatureTopLevel(c.forWriting)
assert.Equal(t, c.expected, res, fmt.Sprintf("%#v %v", c.ns, c.forWriting))
}
}
func TestSignatureStorageBaseSignatureStorageURL(t *testing.T) {
const md = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
assert.True(t, signatureStorageURL(nil, md, 0) == nil)
for _, c := range []struct {
base string
index int
expected string
}{
{"file:///tmp", 0, "file:///tmp@" + md + "/signature-1"},
{"file:///tmp", 1, "file:///tmp@" + md + "/signature-2"},
{"https://localhost:5555/root", 0, "https://localhost:5555/root@" + md + "/signature-1"},
{"https://localhost:5555/root", 1, "https://localhost:5555/root@" + md + "/signature-2"},
{"http://localhost:5555/root", 0, "http://localhost:5555/root@" + md + "/signature-1"},
{"http://localhost:5555/root", 1, "http://localhost:5555/root@" + md + "/signature-2"},
} {
url, err := url.Parse(c.base)
require.NoError(t, err)
expectedURL, err := url.Parse(c.expected)
require.NoError(t, err)
res := signatureStorageURL(url, md, c.index)
assert.Equal(t, expectedURL, res, c.expected)
}
}

View file

@ -0,0 +1,91 @@
package policyconfiguration
import (
"strings"
"testing"
"fmt"
"github.com/containers/image/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

@ -56,7 +56,7 @@ type Canonical interface {
// returned.
// If an error was encountered it is returned, along with a nil Reference.
func ParseNamed(s string) (Named, error) {
named, err := distreference.ParseNamed(s)
named, err := distreference.ParseNormalizedNamed(s)
if err != nil {
return nil, errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", s)
}

View file

@ -0,0 +1,272 @@
package reference
import (
"testing"
_ "crypto/sha256"
)
func TestValidateReferenceName(t *testing.T) {
validRepoNames := []string{
"docker/docker",
"library/debian",
"debian",
"docker.io/docker/docker",
"docker.io/library/debian",
"docker.io/debian",
"index.docker.io/docker/docker",
"index.docker.io/library/debian",
"index.docker.io/debian",
"127.0.0.1:5000/docker/docker",
"127.0.0.1:5000/library/debian",
"127.0.0.1:5000/debian",
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
}
invalidRepoNames := []string{
"https://github.com/docker/docker",
"docker/Docker",
"-docker",
"-docker/docker",
"-docker.io/docker/docker",
"docker///docker",
"docker.io/docker/Docker",
"docker.io/docker///docker",
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
"docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
}
for _, name := range invalidRepoNames {
_, err := ParseNamed(name)
if err == nil {
t.Fatalf("Expected invalid repo name for %q", name)
}
}
for _, name := range validRepoNames {
_, err := ParseNamed(name)
if err != nil {
t.Fatalf("Error parsing repo name %s, got: %q", name, err)
}
}
}
func TestValidateRemoteName(t *testing.T) {
validRepositoryNames := []string{
// Sanity check.
"docker/docker",
// Allow 64-character non-hexadecimal names (hexadecimal names are forbidden).
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
// Allow embedded hyphens.
"docker-rules/docker",
// Allow multiple hyphens as well.
"docker---rules/docker",
//Username doc and image name docker being tested.
"doc/docker",
// single character names are now allowed.
"d/docker",
"jess/t",
// Consecutive underscores.
"dock__er/docker",
}
for _, repositoryName := range validRepositoryNames {
_, err := ParseNamed(repositoryName)
if err != nil {
t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err)
}
}
invalidRepositoryNames := []string{
// Disallow capital letters.
"docker/Docker",
// Only allow one slash.
"docker///docker",
// Disallow 64-character hexadecimal.
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
// Disallow leading and trailing hyphens in namespace.
"-docker/docker",
"docker-/docker",
"-docker-/docker",
// Don't allow underscores everywhere (as opposed to hyphens).
"____/____",
"_docker/_docker",
// Disallow consecutive periods.
"dock..er/docker",
"dock_.er/docker",
"dock-.er/docker",
// No repository.
"docker/",
//namespace too long
"this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker",
}
for _, repositoryName := range invalidRepositoryNames {
if _, err := ParseNamed(repositoryName); err == nil {
t.Errorf("Repository name should be invalid: %v", repositoryName)
}
}
}
func TestParseRepositoryInfo(t *testing.T) {
type tcase struct {
RemoteName, NormalizedName, FullName, AmbiguousName, Hostname string
}
tcases := []tcase{
{
RemoteName: "fooo/bar",
NormalizedName: "fooo/bar",
FullName: "docker.io/fooo/bar",
AmbiguousName: "index.docker.io/fooo/bar",
Hostname: "docker.io",
},
{
RemoteName: "library/ubuntu",
NormalizedName: "ubuntu",
FullName: "docker.io/library/ubuntu",
AmbiguousName: "library/ubuntu",
Hostname: "docker.io",
},
{
RemoteName: "nonlibrary/ubuntu",
NormalizedName: "nonlibrary/ubuntu",
FullName: "docker.io/nonlibrary/ubuntu",
AmbiguousName: "",
Hostname: "docker.io",
},
{
RemoteName: "other/library",
NormalizedName: "other/library",
FullName: "docker.io/other/library",
AmbiguousName: "",
Hostname: "docker.io",
},
{
RemoteName: "private/moonbase",
NormalizedName: "127.0.0.1:8000/private/moonbase",
FullName: "127.0.0.1:8000/private/moonbase",
AmbiguousName: "",
Hostname: "127.0.0.1:8000",
},
{
RemoteName: "privatebase",
NormalizedName: "127.0.0.1:8000/privatebase",
FullName: "127.0.0.1:8000/privatebase",
AmbiguousName: "",
Hostname: "127.0.0.1:8000",
},
{
RemoteName: "private/moonbase",
NormalizedName: "example.com/private/moonbase",
FullName: "example.com/private/moonbase",
AmbiguousName: "",
Hostname: "example.com",
},
{
RemoteName: "privatebase",
NormalizedName: "example.com/privatebase",
FullName: "example.com/privatebase",
AmbiguousName: "",
Hostname: "example.com",
},
{
RemoteName: "private/moonbase",
NormalizedName: "example.com:8000/private/moonbase",
FullName: "example.com:8000/private/moonbase",
AmbiguousName: "",
Hostname: "example.com:8000",
},
{
RemoteName: "privatebasee",
NormalizedName: "example.com:8000/privatebasee",
FullName: "example.com:8000/privatebasee",
AmbiguousName: "",
Hostname: "example.com:8000",
},
{
RemoteName: "library/ubuntu-12.04-base",
NormalizedName: "ubuntu-12.04-base",
FullName: "docker.io/library/ubuntu-12.04-base",
AmbiguousName: "index.docker.io/library/ubuntu-12.04-base",
Hostname: "docker.io",
},
}
for _, tcase := range tcases {
refStrings := []string{tcase.NormalizedName, tcase.FullName}
if tcase.AmbiguousName != "" {
refStrings = append(refStrings, tcase.AmbiguousName)
}
var refs []Named
for _, r := range refStrings {
named, err := ParseNamed(r)
if err != nil {
t.Fatal(err)
}
refs = append(refs, named)
named, err = WithName(r)
if err != nil {
t.Fatal(err)
}
refs = append(refs, named)
}
for _, r := range refs {
if expected, actual := tcase.NormalizedName, r.Name(); expected != actual {
t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.FullName, r.FullName(); expected != actual {
t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.Hostname, r.Hostname(); expected != actual {
t.Fatalf("Invalid hostname for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.RemoteName, r.RemoteName(); expected != actual {
t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual)
}
}
}
}
func TestParseReferenceWithTagAndDigest(t *testing.T) {
ref, err := ParseNamed("busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")
if err != nil {
t.Fatal(err)
}
if _, isTagged := ref.(NamedTagged); isTagged {
t.Fatalf("Reference from %q should not support tag", ref)
}
if _, isCanonical := ref.(Canonical); !isCanonical {
t.Fatalf("Reference from %q should not support digest", ref)
}
if expected, actual := "busybox@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa", ref.String(); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
}
func TestInvalidReferenceComponents(t *testing.T) {
if _, err := WithName("-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid name")
}
ref, err := WithName("busybox")
if err != nil {
t.Fatal(err)
}
if _, err := WithTag(ref, "-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid tag")
}
}

View file

@ -0,0 +1,45 @@
package docker
import (
"testing"
"github.com/stretchr/testify/assert"
)
// This is just a smoke test for the common expected header formats,
// by no means comprehensive.
func TestParseValueAndParams(t *testing.T) {
for _, c := range []struct {
input string
scope string
params map[string]string
}{
{
`Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/busybox:pull"`,
"bearer",
map[string]string{
"realm": "https://auth.docker.io/token",
"service": "registry.docker.io",
"scope": "repository:library/busybox:pull",
},
},
{
`Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/busybox:pull,push"`,
"bearer",
map[string]string{
"realm": "https://auth.docker.io/token",
"service": "registry.docker.io",
"scope": "repository:library/busybox:pull,push",
},
},
{
`Bearer realm="http://127.0.0.1:5000/openshift/token"`,
"bearer",
map[string]string{"realm": "http://127.0.0.1:5000/openshift/token"},
},
} {
scope, params := parseValueAndParams(c.input)
assert.Equal(t, c.scope, scope, c.input)
assert.Equal(t, c.params, params, c.input)
}
}

267
vendor/github.com/containers/image/docs/policy.json.md generated vendored Normal file
View file

@ -0,0 +1,267 @@
% POLICY.JSON(5) policy.json Man Page
% Miloslav Trmač
% September 2016
# Signature verification policy file format
Signature verification policy files are used to specify policy, e.g. trusted keys,
applicable when deciding whether to accept an image, or individual signatures of that image, as valid.
The default policy is stored (unless overridden at compile-time) at `/etc/containers/policy.json`;
applications performing verification may allow using a different policy instead.
## Overall structure
The signature verification policy file, usually called `policy.json`,
uses a JSON format. Unlike some other JSON files, its parsing is fairly strict:
unrecognized, duplicated or otherwise invalid fields cause the entire file,
and usually the entire operation, to be rejected.
The purpose of the policy file is to define a set of *policy requirements* for a container image,
usually depending on its location (where it is being pulled from) or otherwise defined identity.
Policy requirements can be defined for:
- An individual *scope* in a *transport*.
The *transport* values are the same as the transport prefixes when pushing/pulling images (e.g. `docker:`, `atomic:`),
and *scope* values are defined by each transport; see below for more details.
Usually, a scope can be defined to match a single image, and various prefixes of
such a most specific scope define namespaces of matching images.
- A default policy for a single transport, expressed using an empty string as a scope
- A global default policy.
If multiple policy requirements match a given image, only the requirements from the most specific match apply,
the more general policy requirements definitions are ignored.
This is expressed in JSON using the top-level syntax
```js
{
"default": [/* policy requirements: global default */]
"transports": {
transport_name: {
"": [/* policy requirements: default for transport $transport_name */],
scope_1: [/* policy requirements: default for $scope_1 in $transport_name */],
scope_2: [/*…*/]
/*…*/
},
transport_name_2: {/*…*/}
/*…*/
}
}
```
The global `default` set of policy requirements is mandatory; all of the other fields
(`transports` itself, any specific transport, the transport-specific default, etc.) are optional.
<!-- NOTE: Keep this in sync with transports/transports.go! -->
## Supported transports and their scopes
### `atomic:`
The `atomic:` transport refers to images in an Atomic Registry.
Supported scopes use the form _hostname_[`:`_port_][`/`_namespace_[`/`_imagestream_ [`:`_tag_]]],
i.e. either specifying a complete name of a tagged image, or prefix denoting
a host/namespace/image stream.
*Note:* The _hostname_ and _port_ refer to the Docker registry host and port (the one used
e.g. for `docker pull`), _not_ to the OpenShift API host and port.
### `dir:`
The `dir:` transport refers to images stored in local directories.
Supported scopes are paths of directories (either containing a single image or
subdirectories possibly containing images).
*Note:* The paths must be absolute and contain no symlinks. Paths violating these requirements may be silently ignored.
The top-level scope `"/"` is forbidden; use the transport default scope `""`,
for consistency with other transports.
### `docker:`
The `docker:` transport refers to images in a registry implementing the "Docker Registry HTTP API V2".
Scopes matching individual images are named Docker references *in the fully expanded form*, either
using a tag or digest. For example, `docker.io/library/busybox:latest` (*not* `busybox:latest`).
More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest),
a repository namespace, or a registry host (by only specifying the host name).
### `oci:`
The `oci:` transport refers to images in directories compliant with "Open Container Image Layout Specification".
Supported scopes use the form _directory_`:`_tag_, and _directory_ referring to
a directory containing one or more tags, or any of the parent directories.
*Note:* See `dir:` above for semantics and restrictions on the directory paths, they apply to `oci:` equivalently.
## Policy Requirements
Using the mechanisms above, a set of policy requirements is looked up. The policy requirements
are represented as a JSON array of individual requirement objects. For an image to be accepted,
*all* of the requirements must be satisfied simulatenously.
The policy requirements can also be used to decide whether an individual signature is accepted (= is signed by a recognized key of a known author);
in that case some requirements may apply only to some signatures, but each signature must be accepted by *at least one* requirement object.
The following requirement objects are supported:
### `insecureAcceptAnything`
A simple requirement with the following syntax
```json
{"type":"insecureAcceptAnything"}
```
This requirement accepts any image (but note that other requirements in the array still apply).
When deciding to accept an individual signature, this requirement does not have any effect; it does *not* cause the signature to be accepted, though.
This is useful primarily for policy scopes where no signature verification is required;
because the array of policy requirements must not be empty, this requirement is used
to represent the lack of requirements explicitly.
### `reject`
A simple requirement with the following syntax:
```json
{"type":"reject"}
```
This requirement rejects every image, and every signature.
### `signedBy`
This requirement requires an image to be signed with an expected identity, or accepts a signature if it is using an expected identity and key.
```js
{
"type": "signedBy",
"keyType": "GPGKeys", /* The only currently supported value */
"keyPath": "/path/to/local/keyring/file",
"keyData": "base64-encoded-keyring-data",
"signedIdentity": identity_requirement
}
```
<!-- Later: other keyType values -->
Exactly one of `keyPath` and `keyData` must be present, containing a GPG keyring of one or more public keys. Only signatures made by these keys are accepted.
The `signedIdentity` field, a JSON object, specifies what image identity the signature claims about the image.
One of the following alternatives are supported:
- The identity in the signature must exactly match the image identity. Note that with this, referencing an image by digest (with a signature claiming a _repository_`:`_tag_ identity) will fail.
```json
{"type":"matchExact"}
```
- If the image identity carries a tag, the identity in the signature must exactly match;
if the image identity uses a digest reference, the identity in the signature must be in the same repository as the image identity (using any tag).
(Note that with images identified using digest references, the digest from the reference is validated even before signature verification starts.)
```json
{"type":"matchRepoDigestOrExact"}
```
- The identity in the signature must be in the same repository as the image identity. This is useful e.g. to pull an image using the `:latest` tag when the image is signed with a tag specifing an exact image version.
```json
{"type":"matchRepository"}
```
- The identity in the signature must exactly match a specified identity.
This is useful e.g. when locally mirroring images signed using their public identity.
```js
{
"type": "exactReference",
"dockerReference": docker_reference_value
}
```
- The identity in the signature must be in the same repository as a specified identity.
This combines the properties of `matchRepository` and `exactReference`.
```js
{
"type": "exactRepository",
"dockerRepository": docker_repository_value
}
```
If the `signedIdentity` field is missing, it is treated as `matchRepoDigestOrExact`.
*Note*: `matchExact`, `matchRepoDigestOrExact` and `matchRepository` can be only used if a Docker-like image identity is
provided by the transport. In particular, the `dir:` and `oci:` transports can be only
used with `exactReference` or `exactRepository`.
<!-- ### `signedBaseLayer` -->
## Examples
It is *strongly* recommended to set the `default` policy to `reject`, and then
selectively allow individual transports and scopes as desired.
### A reasonably locked-down system
(Note that the `/*``*/` comments are not valid in JSON, and must not be used in real policies.)
```js
{
"default": [{"type": "reject"}], /* Reject anything not explicitly allowed */
"transports": {
"docker": {
/* Allow installing images from a specific repository namespace, without cryptographic verification.
This namespace includes images like openshift/hello-openshift and openshift/origin. */
"docker.io/openshift": [{"type": "insecureAcceptAnything"}],
/* Similarly, allow installing the “official” busybox images. Note how the fully expanded
form, with the explicit /library/, must be used. */
"docker.io/library/busybox": [{"type": "insecureAcceptAnything"}]
/* Other docker: images use the global default policy and are rejected */
},
"dir": {
"": [{"type": "insecureAcceptAnything"}] /* Allow any images originating in local directories */
},
"atomic": {
/* The common case: using a known key for a repository or set of repositories */
"hostname:5000/myns/official": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/path/to/official-pubkey.gpg"
}
],
/* A more complex example, for a repository which contains a mirror of a third-party product,
which must be signed-off by local IT */
"hostname:5000/vendor/product": [
{ /* Require the image to be signed by the original vendor, using the vendor's repository location. */
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/path/to/vendor-pubkey.gpg",
"signedIdentity": {
"type": "exactRepository",
"dockerRepository": "vendor-hostname/product/repository"
}
},
{ /* Require the image to _also_ be signed by a local reviewer. */
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/path/to/reviewer-pubkey.gpg"
}
]
}
}
}
```
### Completely disable security, allow all images, do not trust any signatures
```json
{
"default": [{"type": "insecureAcceptAnything"}]
}
```

124
vendor/github.com/containers/image/docs/registries.d.md generated vendored Normal file
View file

@ -0,0 +1,124 @@
% REGISTRIES.D(5) Registries.d Man Page
% Miloslav Trmač
% August 2016
# Registries Configuration Directory
The registries configuration directory contains configuration for various registries
(servers storing remote container images), and for content stored in them,
so that the configuration does not have to be provided in command-line options over and over for every command,
and so that it can be shared by all users of containers/image.
By default (unless overridden at compile-time), the registries configuration directory is `/etc/containers/registries.d`;
applications may allow using a different directory instead.
## Directory Structure
The directory may contain any number of files with the extension `.yaml`,
each using the YAML format. Other than the mandatory extension, names of the files
dont matter.
The contents of these files are merged together; to have a well-defined and easy to understand
behavior, there can be only one configuration section describing a single namespace within a registry
(in particular there can be at most one one `default-docker` section across all files,
and there can be at most one instance of any key under the the `docker` section;
these sections are documented later).
Thus, it is forbidden to have two conflicting configurations for a single registry or scope,
and it is also forbidden to split a configuration for a single registry or scope across
more than one file (even if they are not semantically in conflict).
## Registries, Scopes and Search Order
Each YAML file must contain a “YAML mapping” (key-value pairs). Two top-level keys are defined:
- `default-docker` is the _configuration section_ (as documented below)
for registries implementing "Docker Registry HTTP API V2".
This key is optional.
- `docker` is a mapping, using individual registries implementing "Docker Registry HTTP API V2",
or namespaces and individual images within these registries, as keys;
the value assigned to any such key is a _configuration section_.
This key is optional.
Scopes matching individual images are named Docker references *in the fully expanded form*, either
using a tag or digest. For example, `docker.io/library/busybox:latest` (*not* `busybox:latest`).
More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest),
a repository namespace, or a registry host (and a port if it differs from the default).
Note that if a registry is accessed using a hostname+port configuration, the port-less hostname
is _not_ used as parent scope.
When searching for a configuration to apply for an individual container image, only
the configuration for the most-precisely matching scope is used; configuration using
more general scopes is ignored. For example, if _any_ configuration exists for
`docker.io/library/busybox`, the configuration for `docker.io` is ignored
(even if some element of the configuration is defined for `docker.io` and not for `docker.io/library/busybox`).
## Individual Configuration Sections
A single configuration section is selected for a container image using the process
described above. The configuration section is a YAML mapping, with the following keys:
- `sigstore-staging` defines an URL of of the signature storage, used for editing it (adding or deleting signatures).
This key is optional; if it is missing, `sigstore` below is used.
- `sigstore` defines an URL of the signature storage.
This URL is used for reading existing signatures,
and if `sigstore-staging` does not exist, also for adding or removing them.
This key is optional; if it is missing, no signature storage is defined (no signatures
are download along with images, adding new signatures is possible only if `sigstore-staging` is defined).
## Examples
### Using Containers from Various Origins
The following demonstrates how to to consume and run images from various registries and namespaces:
```yaml
docker:
registry.database-supplier.com:
sigstore: https://sigstore.database-supplier.com
distribution.great-middleware.org:
sigstore: https://security-team.great-middleware.org/sigstore
docker.io/web-framework:
sigstore: https://sigstore.web-framework.io:8080
```
### Developing and Signing Containers, Staging Signatures
For developers in `example.com`:
- Consume most container images using the public servers also used by clients.
- Use a separate sigure storage for an container images in a namespace corresponding to the developers' department, with a staging storage used before publishing signatures.
- Craft an individual exception for a single branch a specific developer is working on locally.
```yaml
docker:
registry.example.com:
sigstore: https://registry-sigstore.example.com
registry.example.com/mydepartment:
sigstore: https://sigstore.mydepartment.example.com
sigstore-staging: file:///mnt/mydepartment/sigstore-staging
registry.example.com/mydepartment/myproject:mybranch:
sigstore: http://localhost:4242/sigstore
sigstore-staging: file:///home/useraccount/webroot/sigstore
```
### A Global Default
If a company publishes its products using a different domain, and different registry hostname for each of them, it is still possible to use a single signature storage server
without listing each domain individually. This is expected to rarely happen, usually only for staging new signatures.
```yaml
default-docker:
sigstore-staging: file:///mnt/company/common-sigstore-staging
```
# AUTHORS
Miloslav Trmač <mitr@redhat.com>

View file

@ -12,6 +12,7 @@ import (
"github.com/containers/image/manifest"
"github.com/containers/image/types"
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
@ -166,6 +167,8 @@ func (m *manifestSchema2) UpdatedImage(options types.ManifestUpdateOptions) (typ
case "": // No conversion, OK
case manifest.DockerV2Schema1SignedMediaType, manifest.DockerV2Schema1MediaType:
return copy.convertToManifestSchema1(options.InformationOnly.Destination)
case imgspecv1.MediaTypeImageManifest:
return copy.convertToManifestOCI1()
default:
return nil, errors.Errorf("Conversion of image manifest from %s to %s is not implemented", manifest.DockerV2Schema2MediaType, options.ManifestMIMEType)
}
@ -173,6 +176,45 @@ func (m *manifestSchema2) UpdatedImage(options types.ManifestUpdateOptions) (typ
return memoryImageFromManifest(&copy), nil
}
func (m *manifestSchema2) convertToManifestOCI1() (types.Image, error) {
configBlob, err := m.ConfigBlob()
if err != nil {
return nil, err
}
// docker v2s2 and OCI v1 are mostly compatible but v2s2 contains more fields
// than OCI v1. This unmarshal, then re-marshal makes sure we drop docker v2s2
// fields that aren't needed in OCI v1.
configOCI := &imgspecv1.Image{}
if err := json.Unmarshal(configBlob, configOCI); err != nil {
return nil, err
}
configOCIBytes, err := json.Marshal(configOCI)
if err != nil {
return nil, err
}
config := descriptor{
MediaType: imgspecv1.MediaTypeImageConfig,
Size: int64(len(configOCIBytes)),
Digest: digest.FromBytes(configOCIBytes),
}
layers := make([]descriptor, len(m.LayersDescriptors))
for idx := range layers {
layers[idx] = m.LayersDescriptors[idx]
if m.LayersDescriptors[idx].MediaType == manifest.DockerV2Schema2ForeignLayerMediaType {
layers[idx].MediaType = imgspecv1.MediaTypeImageLayerNonDistributable
} else {
// we assume layers are gzip'ed because docker v2s2 only deals with
// gzip'ed layers. However, OCI has non-gzip'ed layers as well.
layers[idx].MediaType = imgspecv1.MediaTypeImageLayerGzip
}
}
m1 := manifestOCI1FromComponents(config, m.src, configOCIBytes, layers)
return memoryImageFromManifest(m1), nil
}
// Based on docker/distribution/manifest/schema1/config_builder.go
func (m *manifestSchema2) convertToManifestSchema1(dest types.ImageDestination) (types.Image, error) {
configBytes, err := m.ConfigBlob()

View file

@ -0,0 +1,499 @@
package image
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"path/filepath"
"testing"
"time"
"github.com/pkg/errors"
"github.com/containers/image/docker/reference"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// unusedImageSource is used when we don't expect the ImageSource to be used in our tests.
type unusedImageSource struct{}
func (f unusedImageSource) Reference() types.ImageReference {
panic("Unexpected call to a mock function")
}
func (f unusedImageSource) Close() {
panic("Unexpected call to a mock function")
}
func (f unusedImageSource) GetManifest() ([]byte, string, error) {
panic("Unexpected call to a mock function")
}
func (f unusedImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) {
panic("Unexpected call to a mock function")
}
func (f unusedImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, error) {
panic("Unexpected call to a mock function")
}
func (f unusedImageSource) GetSignatures() ([][]byte, error) {
panic("Unexpected call to a mock function")
}
func manifestSchema2FromFixture(t *testing.T, src types.ImageSource, fixture string) genericManifest {
manifest, err := ioutil.ReadFile(filepath.Join("fixtures", fixture))
require.NoError(t, err)
m, err := manifestSchema2FromManifest(src, manifest)
require.NoError(t, err)
return m
}
func manifestSchema2FromComponentsLikeFixture(configBlob []byte) genericManifest {
return manifestSchema2FromComponents(descriptor{
MediaType: "application/octet-stream",
Size: 5940,
Digest: "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f",
}, nil, configBlob, []descriptor{
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb",
Size: 51354364,
},
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c",
Size: 150,
},
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9",
Size: 11739507,
},
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909",
Size: 8841833,
},
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa",
Size: 291,
},
})
}
func TestManifestSchema2FromManifest(t *testing.T) {
// This just tests that the JSON can be loaded; we test that the parsed
// values are correctly returned in tests for the individual getter methods.
_ = manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json")
_, err := manifestSchema2FromManifest(nil, []byte{})
assert.Error(t, err)
}
func TestManifestSchema2FromComponents(t *testing.T) {
// This just smoke-tests that the manifest can be created; we test that the parsed
// values are correctly returned in tests for the individual getter methods.
_ = manifestSchema2FromComponentsLikeFixture(nil)
}
func TestManifestSchema2Serialize(t *testing.T) {
for _, m := range []genericManifest{
manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"),
manifestSchema2FromComponentsLikeFixture(nil),
} {
serialized, err := m.serialize()
require.NoError(t, err)
var contents map[string]interface{}
err = json.Unmarshal(serialized, &contents)
require.NoError(t, err)
original, err := ioutil.ReadFile("fixtures/schema2.json")
require.NoError(t, err)
var originalContents map[string]interface{}
err = json.Unmarshal(original, &originalContents)
require.NoError(t, err)
// We would ideally like to compare “serialized” with some transformation of
// “original”, but the ordering of fields in JSON maps is undefined, so this is
// easier.
assert.Equal(t, originalContents, contents)
}
}
func TestManifestSchema2ManifestMIMEType(t *testing.T) {
for _, m := range []genericManifest{
manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"),
manifestSchema2FromComponentsLikeFixture(nil),
} {
assert.Equal(t, manifest.DockerV2Schema2MediaType, m.manifestMIMEType())
}
}
func TestManifestSchema2ConfigInfo(t *testing.T) {
for _, m := range []genericManifest{
manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"),
manifestSchema2FromComponentsLikeFixture(nil),
} {
assert.Equal(t, types.BlobInfo{
Size: 5940,
Digest: "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f",
}, m.ConfigInfo())
}
}
// configBlobImageSource allows testing various GetBlob behaviors in .ConfigBlob()
type configBlobImageSource struct {
unusedImageSource // We inherit almost all of the methods, which just panic()
f func(digest digest.Digest) (io.ReadCloser, int64, error)
}
func (f configBlobImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, error) {
if info.Digest.String() != "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" {
panic("Unexpected digest in GetBlob")
}
return f.f(info.Digest)
}
func TestManifestSchema2ConfigBlob(t *testing.T) {
realConfigJSON, err := ioutil.ReadFile("fixtures/schema2-config.json")
require.NoError(t, err)
for _, c := range []struct {
cbISfn func(digest digest.Digest) (io.ReadCloser, int64, error)
blob []byte
}{
// Success
{func(digest digest.Digest) (io.ReadCloser, int64, error) {
return ioutil.NopCloser(bytes.NewReader(realConfigJSON)), int64(len(realConfigJSON)), nil
}, realConfigJSON},
// Various kinds of failures
{nil, nil},
{func(digest digest.Digest) (io.ReadCloser, int64, error) {
return nil, -1, errors.New("Error returned from GetBlob")
}, nil},
{func(digest digest.Digest) (io.ReadCloser, int64, error) {
reader, writer := io.Pipe()
writer.CloseWithError(errors.New("Expected error reading input in ConfigBlob"))
return reader, 1, nil
}, nil},
{func(digest digest.Digest) (io.ReadCloser, int64, error) {
nonmatchingJSON := []byte("This does not match ConfigDescriptor.Digest")
return ioutil.NopCloser(bytes.NewReader(nonmatchingJSON)), int64(len(nonmatchingJSON)), nil
}, nil},
} {
var src types.ImageSource
if c.cbISfn != nil {
src = configBlobImageSource{unusedImageSource{}, c.cbISfn}
} else {
src = nil
}
m := manifestSchema2FromFixture(t, src, "schema2.json")
blob, err := m.ConfigBlob()
if c.blob != nil {
assert.NoError(t, err)
assert.Equal(t, c.blob, blob)
} else {
assert.Error(t, err)
}
}
// Generally conficBlob should match ConfigInfo; we dont quite need it to, and this will
// guarantee that the returned object is returning the original contents instead
// of reading an object from elsewhere.
configBlob := []byte("config blob which does not match ConfigInfo")
// This just tests that the manifest can be created; we test that the parsed
// values are correctly returned in tests for the individual getter methods.
m := manifestSchema2FromComponentsLikeFixture(configBlob)
cb, err := m.ConfigBlob()
require.NoError(t, err)
assert.Equal(t, configBlob, cb)
}
func TestManifestSchema2LayerInfo(t *testing.T) {
for _, m := range []genericManifest{
manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"),
manifestSchema2FromComponentsLikeFixture(nil),
} {
assert.Equal(t, []types.BlobInfo{
{
Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb",
Size: 51354364,
},
{
Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c",
Size: 150,
},
{
Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9",
Size: 11739507,
},
{
Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909",
Size: 8841833,
},
{
Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa",
Size: 291,
},
}, m.LayerInfos())
}
}
func TestManifestSchema2ImageInspectInfo(t *testing.T) {
configJSON, err := ioutil.ReadFile("fixtures/schema2-config.json")
require.NoError(t, err)
m := manifestSchema2FromComponentsLikeFixture(configJSON)
ii, err := m.imageInspectInfo()
require.NoError(t, err)
assert.Equal(t, types.ImageInspectInfo{
Tag: "",
Created: time.Date(2016, 9, 23, 23, 20, 45, 789764590, time.UTC),
DockerVersion: "1.12.1",
Labels: map[string]string{},
Architecture: "amd64",
Os: "linux",
Layers: nil,
}, *ii)
// nil configBlob will trigger an error in m.ConfigBlob()
m = manifestSchema2FromComponentsLikeFixture(nil)
_, err = m.imageInspectInfo()
assert.Error(t, err)
m = manifestSchema2FromComponentsLikeFixture([]byte("invalid JSON"))
_, err = m.imageInspectInfo()
assert.Error(t, err)
}
func TestManifestSchema2UpdatedImageNeedsLayerDiffIDs(t *testing.T) {
for _, m := range []genericManifest{
manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"),
manifestSchema2FromComponentsLikeFixture(nil),
} {
assert.False(t, m.UpdatedImageNeedsLayerDiffIDs(types.ManifestUpdateOptions{
ManifestMIMEType: manifest.DockerV2Schema1SignedMediaType,
}))
}
}
// schema2ImageSource is plausible enough for schema conversions in manifestSchema2.UpdatedImage() to work.
type schema2ImageSource struct {
configBlobImageSource
ref reference.Named
}
func (s2is *schema2ImageSource) Reference() types.ImageReference {
return refImageReferenceMock{s2is.ref}
}
// refImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference.
type refImageReferenceMock struct{ reference.Named }
func (ref refImageReferenceMock) Transport() types.ImageTransport {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) StringWithinTransport() string {
panic("unexpected call to a mock function")
}
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(ctx *types.SystemContext) (types.Image, error) {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) NewImageSource(ctx *types.SystemContext, requestedManifestMIMETypes []string) (types.ImageSource, error) {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) DeleteImage(ctx *types.SystemContext) error {
panic("unexpected call to a mock function")
}
func newSchema2ImageSource(t *testing.T, dockerRef string) *schema2ImageSource {
realConfigJSON, err := ioutil.ReadFile("fixtures/schema2-config.json")
require.NoError(t, err)
ref, err := reference.ParseNamed(dockerRef)
require.NoError(t, err)
return &schema2ImageSource{
configBlobImageSource: configBlobImageSource{
f: func(digest digest.Digest) (io.ReadCloser, int64, error) {
return ioutil.NopCloser(bytes.NewReader(realConfigJSON)), int64(len(realConfigJSON)), nil
},
},
ref: ref,
}
}
type memoryImageDest struct {
ref reference.Named
storedBlobs map[digest.Digest][]byte
}
func (d *memoryImageDest) Reference() types.ImageReference {
return refImageReferenceMock{d.ref}
}
func (d *memoryImageDest) Close() {
panic("Unexpected call to a mock function")
}
func (d *memoryImageDest) SupportedManifestMIMETypes() []string {
panic("Unexpected call to a mock function")
}
func (d *memoryImageDest) SupportsSignatures() error {
panic("Unexpected call to a mock function")
}
func (d *memoryImageDest) ShouldCompressLayers() bool {
panic("Unexpected call to a mock function")
}
func (d *memoryImageDest) AcceptsForeignLayerURLs() bool {
panic("Unexpected call to a mock function")
}
func (d *memoryImageDest) PutBlob(stream io.Reader, inputInfo types.BlobInfo) (types.BlobInfo, error) {
if d.storedBlobs == nil {
d.storedBlobs = make(map[digest.Digest][]byte)
}
if inputInfo.Digest.String() == "" {
panic("inputInfo.Digest unexpectedly empty")
}
contents, err := ioutil.ReadAll(stream)
if err != nil {
return types.BlobInfo{}, err
}
d.storedBlobs[inputInfo.Digest] = contents
return types.BlobInfo{Digest: inputInfo.Digest, Size: int64(len(contents))}, nil
}
func (d *memoryImageDest) HasBlob(inputInfo types.BlobInfo) (bool, int64, error) {
panic("Unexpected call to a mock function")
}
func (d *memoryImageDest) ReapplyBlob(inputInfo types.BlobInfo) (types.BlobInfo, error) {
panic("Unexpected call to a mock function")
}
func (d *memoryImageDest) PutManifest([]byte) error {
panic("Unexpected call to a mock function")
}
func (d *memoryImageDest) PutSignatures(signatures [][]byte) error {
panic("Unexpected call to a mock function")
}
func (d *memoryImageDest) Commit() error {
panic("Unexpected call to a mock function")
}
func TestManifestSchema2UpdatedImage(t *testing.T) {
originalSrc := newSchema2ImageSource(t, "httpd:latest")
original := manifestSchema2FromFixture(t, originalSrc, "schema2.json")
// LayerInfos:
layerInfos := append(original.LayerInfos()[1:], original.LayerInfos()[0])
res, err := original.UpdatedImage(types.ManifestUpdateOptions{
LayerInfos: layerInfos,
})
require.NoError(t, err)
assert.Equal(t, layerInfos, res.LayerInfos())
_, err = original.UpdatedImage(types.ManifestUpdateOptions{
LayerInfos: append(layerInfos, layerInfos[0]),
})
assert.Error(t, err)
// ManifestMIMEType:
// Only smoke-test the valid conversions, detailed tests are below. (This also verifies that “original” is not affected.)
for _, mime := range []string{
manifest.DockerV2Schema1MediaType,
manifest.DockerV2Schema1SignedMediaType,
} {
_, err = original.UpdatedImage(types.ManifestUpdateOptions{
ManifestMIMEType: mime,
InformationOnly: types.ManifestUpdateInformation{
Destination: &memoryImageDest{ref: originalSrc.ref},
},
})
assert.NoError(t, err, mime)
}
for _, mime := range []string{
manifest.DockerV2Schema2MediaType, // This indicates a confused caller, not a no-op
"this is invalid",
} {
_, err = original.UpdatedImage(types.ManifestUpdateOptions{
ManifestMIMEType: mime,
})
assert.Error(t, err, mime)
}
// m hasnt been changed:
m2 := manifestSchema2FromFixture(t, originalSrc, "schema2.json")
typedOriginal, ok := original.(*manifestSchema2)
require.True(t, ok)
typedM2, ok := m2.(*manifestSchema2)
require.True(t, ok)
assert.Equal(t, *typedM2, *typedOriginal)
}
func TestConvertToManifestOCI(t *testing.T) {
originalSrc := newSchema2ImageSource(t, "httpd-copy:latest")
original := manifestSchema2FromFixture(t, originalSrc, "schema2.json")
res, err := original.UpdatedImage(types.ManifestUpdateOptions{
ManifestMIMEType: imgspecv1.MediaTypeImageManifest,
})
require.NoError(t, err)
convertedJSON, mt, err := res.Manifest()
require.NoError(t, err)
assert.Equal(t, imgspecv1.MediaTypeImageManifest, mt)
byHandJSON, err := ioutil.ReadFile("fixtures/schema2-to-oci1.json")
require.NoError(t, err)
var converted, byHand map[string]interface{}
err = json.Unmarshal(byHandJSON, &byHand)
require.NoError(t, err)
err = json.Unmarshal(convertedJSON, &converted)
require.NoError(t, err)
assert.Equal(t, byHand, converted)
}
func TestConvertToManifestSchema1(t *testing.T) {
originalSrc := newSchema2ImageSource(t, "httpd-copy:latest")
original := manifestSchema2FromFixture(t, originalSrc, "schema2.json")
memoryDest := &memoryImageDest{ref: originalSrc.ref}
res, err := original.UpdatedImage(types.ManifestUpdateOptions{
ManifestMIMEType: manifest.DockerV2Schema1SignedMediaType,
InformationOnly: types.ManifestUpdateInformation{
Destination: memoryDest,
},
})
require.NoError(t, err)
convertedJSON, mt, err := res.Manifest()
require.NoError(t, err)
assert.Equal(t, manifest.DockerV2Schema1SignedMediaType, mt)
// byDockerJSON is the result of asking the Docker Hub for a schema1 manifest,
// except that we have replaced "name" to verify that the ref from
// memoryDest, not from originalSrc, is used.
byDockerJSON, err := ioutil.ReadFile("fixtures/schema2-to-schema1-by-docker.json")
require.NoError(t, err)
var converted, byDocker map[string]interface{}
err = json.Unmarshal(byDockerJSON, &byDocker)
require.NoError(t, err)
err = json.Unmarshal(convertedJSON, &converted)
require.NoError(t, err)
delete(byDocker, "signatures")
delete(converted, "signatures")
assert.Equal(t, byDocker, converted)
assert.Equal(t, gzippedEmptyLayer, memoryDest.storedBlobs[gzippedEmptyLayerDigest])
// FIXME? Test also the various failure cases, if only to see that we don't crash?
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,36 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 5940,
"digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 51354364,
"digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 150,
"digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 11739507,
"digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 8841833,
"digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 291,
"digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa"
}
]
}

View file

@ -0,0 +1,35 @@
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 5940,
"digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f"
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 51354364,
"digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 150,
"digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 11739507,
"digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 8841833,
"digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 291,
"digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa"
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,29 @@
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 4651,
"digest": "sha256:a13a0762ab7bed51a1b49adec0a702b1cd99294fd460a025b465bcfb7b152745"
},
"layers": [{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 51354364,
"digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb"
}, {
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 150,
"digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c"
}, {
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 11739507,
"digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9"
}, {
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 8841833,
"digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909"
}, {
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 291,
"digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa"
}]
}

View file

@ -0,0 +1,116 @@
{
"schemaVersion": 1,
"name": "library/httpd-copy",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa"
},
{
"blobSum": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb"
}
],
"history": [
{
"v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"383850eeb47b\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"80/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"HTTPD_PREFIX=/usr/local/apache2\",\"HTTPD_VERSION=2.4.23\",\"HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f\",\"HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\\u0026filename=httpd/httpd-2.4.23.tar.bz2\",\"HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc\"],\"Cmd\":[\"httpd-foreground\"],\"ArgsEscaped\":true,\"Image\":\"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd\",\"Volumes\":null,\"WorkingDir\":\"/usr/local/apache2\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{}},\"container\":\"8825acde1b009729807e4b70a65a89399dd8da8e53be9216b9aaabaff4339f69\",\"container_config\":{\"Hostname\":\"383850eeb47b\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"80/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"HTTPD_PREFIX=/usr/local/apache2\",\"HTTPD_VERSION=2.4.23\",\"HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f\",\"HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\\u0026filename=httpd/httpd-2.4.23.tar.bz2\",\"HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"CMD [\\\"httpd-foreground\\\"]\"],\"ArgsEscaped\":true,\"Image\":\"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd\",\"Volumes\":null,\"WorkingDir\":\"/usr/local/apache2\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{}},\"created\":\"2016-09-23T23:20:45.78976459Z\",\"docker_version\":\"1.12.1\",\"id\":\"dca7323f9c839837493199d63263083d94f5eb1796d7bd04ca8374c4e9d3749a\",\"os\":\"linux\",\"parent\":\"1b750729af47c9a802c8d14b0d327d3ad5ecdce5ae773ac728a0263315b914f4\",\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"1b750729af47c9a802c8d14b0d327d3ad5ecdce5ae773ac728a0263315b914f4\",\"parent\":\"3ef2f186f8b0a2fd2d95f5a1f1cd213f5fb0a6e51b0a8dfbe2ec7003a788ff9a\",\"created\":\"2016-09-23T23:20:45.453934921Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) EXPOSE 80/tcp\"]},\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"3ef2f186f8b0a2fd2d95f5a1f1cd213f5fb0a6e51b0a8dfbe2ec7003a788ff9a\",\"parent\":\"dbbb5c772ba968f675ebdb1968a2fbcf3cf53c0c85ff4e3329619e3735c811e6\",\"created\":\"2016-09-23T23:20:45.127455562Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) COPY file:761e313354b918b6cd7ea99975a4f6b53ff5381ba689bab2984aec4dab597215 in /usr/local/bin/ \"]}}"
},
{
"v1Compatibility": "{\"id\":\"dbbb5c772ba968f675ebdb1968a2fbcf3cf53c0c85ff4e3329619e3735c811e6\",\"parent\":\"d264ded964bb52f78c8905c9e6c5f2b8526ef33f371981f0651f3fb0164ad4a7\",\"created\":\"2016-09-23T23:20:44.585743332Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c set -x \\t\\u0026\\u0026 buildDeps=' \\t\\tbzip2 \\t\\tca-certificates \\t\\tgcc \\t\\tlibpcre++-dev \\t\\tlibssl-dev \\t\\tmake \\t\\twget \\t' \\t\\u0026\\u0026 apt-get update \\t\\u0026\\u0026 apt-get install -y --no-install-recommends $buildDeps \\t\\u0026\\u0026 rm -r /var/lib/apt/lists/* \\t\\t\\u0026\\u0026 wget -O httpd.tar.bz2 \\\"$HTTPD_BZ2_URL\\\" \\t\\u0026\\u0026 echo \\\"$HTTPD_SHA1 *httpd.tar.bz2\\\" | sha1sum -c - \\t\\u0026\\u0026 wget -O httpd.tar.bz2.asc \\\"$HTTPD_ASC_URL\\\" \\t\\u0026\\u0026 export GNUPGHOME=\\\"$(mktemp -d)\\\" \\t\\u0026\\u0026 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \\t\\u0026\\u0026 gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \\t\\u0026\\u0026 rm -r \\\"$GNUPGHOME\\\" httpd.tar.bz2.asc \\t\\t\\u0026\\u0026 mkdir -p src \\t\\u0026\\u0026 tar -xvf httpd.tar.bz2 -C src --strip-components=1 \\t\\u0026\\u0026 rm httpd.tar.bz2 \\t\\u0026\\u0026 cd src \\t\\t\\u0026\\u0026 ./configure \\t\\t--prefix=\\\"$HTTPD_PREFIX\\\" \\t\\t--enable-mods-shared=reallyall \\t\\u0026\\u0026 make -j\\\"$(nproc)\\\" \\t\\u0026\\u0026 make install \\t\\t\\u0026\\u0026 cd .. \\t\\u0026\\u0026 rm -r src \\t\\t\\u0026\\u0026 sed -ri \\t\\t-e 's!^(\\\\s*CustomLog)\\\\s+\\\\S+!\\\\1 /proc/self/fd/1!g' \\t\\t-e 's!^(\\\\s*ErrorLog)\\\\s+\\\\S+!\\\\1 /proc/self/fd/2!g' \\t\\t\\\"$HTTPD_PREFIX/conf/httpd.conf\\\" \\t\\t\\u0026\\u0026 apt-get purge -y --auto-remove $buildDeps\"]}}"
},
{
"v1Compatibility": "{\"id\":\"d264ded964bb52f78c8905c9e6c5f2b8526ef33f371981f0651f3fb0164ad4a7\",\"parent\":\"fd6f8d569a8a6d2a95f797494ab3cee7a47693dde647210b236a141f76b5c5fd\",\"created\":\"2016-09-23T23:19:04.009782822Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc\"]},\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"fd6f8d569a8a6d2a95f797494ab3cee7a47693dde647210b236a141f76b5c5fd\",\"parent\":\"5e2578d171daa47c0eeb55e592b4e3bd28a0946a75baed58e4d4dd315c5d5780\",\"created\":\"2016-09-23T23:19:03.705796801Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\\u0026filename=httpd/httpd-2.4.23.tar.bz2\"]},\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"5e2578d171daa47c0eeb55e592b4e3bd28a0946a75baed58e4d4dd315c5d5780\",\"parent\":\"1912159ee5bea8d7fde49b85012f90c47bceb3f09e4082b112b1f06a3f339c53\",\"created\":\"2016-09-23T19:16:55.629947307Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f\"]},\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"1912159ee5bea8d7fde49b85012f90c47bceb3f09e4082b112b1f06a3f339c53\",\"parent\":\"3bfb089ca9d4bb73a9016e44a2c6f908b701f97704433305c419f75e8559d8a2\",\"created\":\"2016-09-23T19:16:55.321573403Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_VERSION=2.4.23\"]},\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"3bfb089ca9d4bb73a9016e44a2c6f908b701f97704433305c419f75e8559d8a2\",\"parent\":\"ae1ece73de4d0365c8b8ab45ba0bf6b1efa4213c16a4903b89341b704d101c3c\",\"created\":\"2016-09-23T19:16:54.948461741Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c apt-get update \\t\\u0026\\u0026 apt-get install -y --no-install-recommends \\t\\tlibapr1 \\t\\tlibaprutil1 \\t\\tlibaprutil1-ldap \\t\\tlibapr1-dev \\t\\tlibaprutil1-dev \\t\\tlibpcre++0 \\t\\tlibssl1.0.0 \\t\\u0026\\u0026 rm -r /var/lib/apt/lists/*\"]}}"
},
{
"v1Compatibility": "{\"id\":\"ae1ece73de4d0365c8b8ab45ba0bf6b1efa4213c16a4903b89341b704d101c3c\",\"parent\":\"bffbcb416f40e0bd3ebae202403587bfd41829cd1e0d538b66f29adce40c6408\",\"created\":\"2016-09-23T19:16:42.339911155Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) WORKDIR /usr/local/apache2\"]},\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"bffbcb416f40e0bd3ebae202403587bfd41829cd1e0d538b66f29adce40c6408\",\"parent\":\"7b27731a3363efcb6b0520962d544471745aae15664920dffe690b4fdb410d80\",\"created\":\"2016-09-23T19:16:41.990121202Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c mkdir -p \\\"$HTTPD_PREFIX\\\" \\t\\u0026\\u0026 chown www-data:www-data \\\"$HTTPD_PREFIX\\\"\"]}}"
},
{
"v1Compatibility": "{\"id\":\"7b27731a3363efcb6b0520962d544471745aae15664920dffe690b4fdb410d80\",\"parent\":\"57a0a421f1acbc1fe6b88b32d3d1c3c0388ff1958b97f95dd0e3a599b810499b\",\"created\":\"2016-09-23T19:16:41.037788416Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"]},\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"57a0a421f1acbc1fe6b88b32d3d1c3c0388ff1958b97f95dd0e3a599b810499b\",\"parent\":\"faeaf6fdfdcbb18d68c12db9683a02428bab83962a493de88b4c7b1ec941db8f\",\"created\":\"2016-09-23T19:16:40.725768956Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_PREFIX=/usr/local/apache2\"]},\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"faeaf6fdfdcbb18d68c12db9683a02428bab83962a493de88b4c7b1ec941db8f\",\"parent\":\"d0c4f1eb7dc8f4dae2b45fe5c0cf4cfc70e5be85d933f5f5f4deb59f134fb520\",\"created\":\"2016-09-23T18:08:51.133779867Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) CMD [\\\"/bin/bash\\\"]\"]},\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"d0c4f1eb7dc8f4dae2b45fe5c0cf4cfc70e5be85d933f5f5f4deb59f134fb520\",\"created\":\"2016-09-23T18:08:50.537223822Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:c6c23585ab140b0b320d4e99bc1b0eb544c9e96c24d90fec5e069a6d57d335ca in / \"]}}"
}
],
"signatures": [
{
"header": {
"jwk": {
"crv": "P-256",
"kid": "6QVR:5NTY:VIHC:W6IU:XYIN:CTKT:OG5R:XEEG:Z6XJ:2623:YCBP:36MA",
"kty": "EC",
"x": "NAGHj6-IdNonuFoxlqJnNMjcrCCE1CBoq2r_1NDci68",
"y": "Kocqgj_Ey5J-wLXTjkuqLC-HjciAnWxsBEziAOTvSPc"
},
"alg": "ES256"
},
"signature": "2MN5k06i8xkJhD5ay4yxAFK7tsZk58UznAZONxDplvQ5lZwbRS162OeBDjCb0Hk0IDyrLXtAfBDlY2Gzf6jrpw",
"protected": "eyJmb3JtYXRMZW5ndGgiOjEwODk1LCJmb3JtYXRUYWlsIjoiQ24wIiwidGltZSI6IjIwMTYtMTAtMTRUMTY6MTI6MDlaIn0"
}
]
}

View file

@ -0,0 +1,36 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/octet-stream",
"size": 5940,
"digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 51354364,
"digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 150,
"digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 11739507,
"digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 8841833,
"digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 291,
"digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa"
}
]
}

View file

@ -3,11 +3,10 @@ package image
import (
"time"
"github.com/docker/engine-api/types/strslice"
"github.com/opencontainers/go-digest"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
"github.com/docker/docker/api/types/strslice"
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
)

View file

@ -15,7 +15,6 @@ type manifestOCI1 struct {
src types.ImageSource // May be nil if configBlob is not nil
configBlob []byte // If set, corresponds to contents of ConfigDescriptor.
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
ConfigDescriptor descriptor `json:"config"`
LayersDescriptors []descriptor `json:"layers"`
}
@ -29,12 +28,11 @@ func manifestOCI1FromManifest(src types.ImageSource, manifest []byte) (genericMa
}
// manifestOCI1FromComponents builds a new manifestOCI1 from the supplied data:
func manifestOCI1FromComponents(config descriptor, configBlob []byte, layers []descriptor) genericManifest {
func manifestOCI1FromComponents(config descriptor, src types.ImageSource, configBlob []byte, layers []descriptor) genericManifest {
return &manifestOCI1{
src: nil,
src: src,
configBlob: configBlob,
SchemaVersion: 2,
MediaType: imgspecv1.MediaTypeImageManifest,
ConfigDescriptor: config,
LayersDescriptors: layers,
}
@ -45,7 +43,7 @@ func (m *manifestOCI1) serialize() ([]byte, error) {
}
func (m *manifestOCI1) manifestMIMEType() string {
return m.MediaType
return imgspecv1.MediaTypeImageManifest
}
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.

346
vendor/github.com/containers/image/image/oci_test.go generated vendored Normal file
View file

@ -0,0 +1,346 @@
package image
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"path/filepath"
"testing"
"time"
"github.com/pkg/errors"
"github.com/containers/image/docker/reference"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func manifestOCI1FromFixture(t *testing.T, src types.ImageSource, fixture string) genericManifest {
manifest, err := ioutil.ReadFile(filepath.Join("fixtures", fixture))
require.NoError(t, err)
m, err := manifestOCI1FromManifest(src, manifest)
require.NoError(t, err)
return m
}
func manifestOCI1FromComponentsLikeFixture(configBlob []byte) genericManifest {
return manifestOCI1FromComponents(descriptor{
MediaType: imgspecv1.MediaTypeImageConfig,
Size: 5940,
Digest: "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f",
}, nil, configBlob, []descriptor{
{
MediaType: imgspecv1.MediaTypeImageLayerGzip,
Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb",
Size: 51354364,
},
{
MediaType: imgspecv1.MediaTypeImageLayerGzip,
Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c",
Size: 150,
},
{
MediaType: imgspecv1.MediaTypeImageLayerGzip,
Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9",
Size: 11739507,
},
{
MediaType: imgspecv1.MediaTypeImageLayerGzip,
Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909",
Size: 8841833,
},
{
MediaType: imgspecv1.MediaTypeImageLayerGzip,
Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa",
Size: 291,
},
})
}
func TestManifestOCI1FromManifest(t *testing.T) {
// This just tests that the JSON can be loaded; we test that the parsed
// values are correctly returned in tests for the individual getter methods.
_ = manifestOCI1FromFixture(t, unusedImageSource{}, "oci1.json")
_, err := manifestOCI1FromManifest(nil, []byte{})
assert.Error(t, err)
}
func TestManifestOCI1FromComponents(t *testing.T) {
// This just smoke-tests that the manifest can be created; we test that the parsed
// values are correctly returned in tests for the individual getter methods.
_ = manifestOCI1FromComponentsLikeFixture(nil)
}
func TestManifestOCI1Serialize(t *testing.T) {
for _, m := range []genericManifest{
manifestOCI1FromFixture(t, unusedImageSource{}, "oci1.json"),
manifestOCI1FromComponentsLikeFixture(nil),
} {
serialized, err := m.serialize()
require.NoError(t, err)
var contents map[string]interface{}
err = json.Unmarshal(serialized, &contents)
require.NoError(t, err)
original, err := ioutil.ReadFile("fixtures/oci1.json")
require.NoError(t, err)
var originalContents map[string]interface{}
err = json.Unmarshal(original, &originalContents)
require.NoError(t, err)
// We would ideally like to compare “serialized” with some transformation of
// “original”, but the ordering of fields in JSON maps is undefined, so this is
// easier.
assert.Equal(t, originalContents, contents)
}
}
func TestManifestOCI1ManifestMIMEType(t *testing.T) {
for _, m := range []genericManifest{
manifestOCI1FromFixture(t, unusedImageSource{}, "oci1.json"),
manifestOCI1FromComponentsLikeFixture(nil),
} {
assert.Equal(t, imgspecv1.MediaTypeImageManifest, m.manifestMIMEType())
}
}
func TestManifestOCI1ConfigInfo(t *testing.T) {
for _, m := range []genericManifest{
manifestOCI1FromFixture(t, unusedImageSource{}, "oci1.json"),
manifestOCI1FromComponentsLikeFixture(nil),
} {
assert.Equal(t, types.BlobInfo{
Size: 5940,
Digest: "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f",
}, m.ConfigInfo())
}
}
func TestManifestOCI1ConfigBlob(t *testing.T) {
realConfigJSON, err := ioutil.ReadFile("fixtures/oci1-config.json")
require.NoError(t, err)
for _, c := range []struct {
cbISfn func(digest digest.Digest) (io.ReadCloser, int64, error)
blob []byte
}{
// Success
{func(digest digest.Digest) (io.ReadCloser, int64, error) {
return ioutil.NopCloser(bytes.NewReader(realConfigJSON)), int64(len(realConfigJSON)), nil
}, realConfigJSON},
// Various kinds of failures
{nil, nil},
{func(digest digest.Digest) (io.ReadCloser, int64, error) {
return nil, -1, errors.New("Error returned from GetBlob")
}, nil},
{func(digest digest.Digest) (io.ReadCloser, int64, error) {
reader, writer := io.Pipe()
writer.CloseWithError(errors.New("Expected error reading input in ConfigBlob"))
return reader, 1, nil
}, nil},
{func(digest digest.Digest) (io.ReadCloser, int64, error) {
nonmatchingJSON := []byte("This does not match ConfigDescriptor.Digest")
return ioutil.NopCloser(bytes.NewReader(nonmatchingJSON)), int64(len(nonmatchingJSON)), nil
}, nil},
} {
var src types.ImageSource
if c.cbISfn != nil {
src = configBlobImageSource{unusedImageSource{}, c.cbISfn}
} else {
src = nil
}
m := manifestOCI1FromFixture(t, src, "oci1.json")
blob, err := m.ConfigBlob()
if c.blob != nil {
assert.NoError(t, err)
assert.Equal(t, c.blob, blob)
} else {
assert.Error(t, err)
}
}
// Generally conficBlob should match ConfigInfo; we dont quite need it to, and this will
// guarantee that the returned object is returning the original contents instead
// of reading an object from elsewhere.
configBlob := []byte("config blob which does not match ConfigInfo")
// This just tests that the manifest can be created; we test that the parsed
// values are correctly returned in tests for the individual getter methods.
m := manifestOCI1FromComponentsLikeFixture(configBlob)
cb, err := m.ConfigBlob()
require.NoError(t, err)
assert.Equal(t, configBlob, cb)
}
func TestManifestOCI1LayerInfo(t *testing.T) {
for _, m := range []genericManifest{
manifestOCI1FromFixture(t, unusedImageSource{}, "oci1.json"),
manifestOCI1FromComponentsLikeFixture(nil),
} {
assert.Equal(t, []types.BlobInfo{
{
Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb",
Size: 51354364,
},
{
Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c",
Size: 150,
},
{
Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9",
Size: 11739507,
},
{
Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909",
Size: 8841833,
},
{
Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa",
Size: 291,
},
}, m.LayerInfos())
}
}
func TestManifestOCI1ImageInspectInfo(t *testing.T) {
configJSON, err := ioutil.ReadFile("fixtures/oci1-config.json")
require.NoError(t, err)
m := manifestOCI1FromComponentsLikeFixture(configJSON)
ii, err := m.imageInspectInfo()
require.NoError(t, err)
assert.Equal(t, types.ImageInspectInfo{
Tag: "",
Created: time.Date(2016, 9, 23, 23, 20, 45, 789764590, time.UTC),
DockerVersion: "1.12.1",
Labels: map[string]string{},
Architecture: "amd64",
Os: "linux",
Layers: nil,
}, *ii)
// nil configBlob will trigger an error in m.ConfigBlob()
m = manifestOCI1FromComponentsLikeFixture(nil)
_, err = m.imageInspectInfo()
assert.Error(t, err)
m = manifestOCI1FromComponentsLikeFixture([]byte("invalid JSON"))
_, err = m.imageInspectInfo()
assert.Error(t, err)
}
func TestManifestOCI1UpdatedImageNeedsLayerDiffIDs(t *testing.T) {
for _, m := range []genericManifest{
manifestOCI1FromFixture(t, unusedImageSource{}, "oci1.json"),
manifestOCI1FromComponentsLikeFixture(nil),
} {
assert.False(t, m.UpdatedImageNeedsLayerDiffIDs(types.ManifestUpdateOptions{
ManifestMIMEType: manifest.DockerV2Schema2MediaType,
}))
}
}
// oci1ImageSource is plausible enough for schema conversions in manifestOCI1.UpdatedImage() to work.
type oci1ImageSource struct {
configBlobImageSource
ref reference.Named
}
func (OCIis *oci1ImageSource) Reference() types.ImageReference {
return refImageReferenceMock{OCIis.ref}
}
func newOCI1ImageSource(t *testing.T, dockerRef string) *oci1ImageSource {
realConfigJSON, err := ioutil.ReadFile("fixtures/oci1-config.json")
require.NoError(t, err)
ref, err := reference.ParseNamed(dockerRef)
require.NoError(t, err)
return &oci1ImageSource{
configBlobImageSource: configBlobImageSource{
f: func(digest digest.Digest) (io.ReadCloser, int64, error) {
return ioutil.NopCloser(bytes.NewReader(realConfigJSON)), int64(len(realConfigJSON)), nil
},
},
ref: ref,
}
}
func TestManifestOCI1UpdatedImage(t *testing.T) {
originalSrc := newOCI1ImageSource(t, "httpd:latest")
original := manifestOCI1FromFixture(t, originalSrc, "oci1.json")
// LayerInfos:
layerInfos := append(original.LayerInfos()[1:], original.LayerInfos()[0])
res, err := original.UpdatedImage(types.ManifestUpdateOptions{
LayerInfos: layerInfos,
})
require.NoError(t, err)
assert.Equal(t, layerInfos, res.LayerInfos())
_, err = original.UpdatedImage(types.ManifestUpdateOptions{
LayerInfos: append(layerInfos, layerInfos[0]),
})
assert.Error(t, err)
// ManifestMIMEType:
// Only smoke-test the valid conversions, detailed tests are below. (This also verifies that “original” is not affected.)
for _, mime := range []string{
manifest.DockerV2Schema2MediaType,
} {
_, err = original.UpdatedImage(types.ManifestUpdateOptions{
ManifestMIMEType: mime,
InformationOnly: types.ManifestUpdateInformation{
Destination: &memoryImageDest{ref: originalSrc.ref},
},
})
assert.NoError(t, err, mime)
}
for _, mime := range []string{
imgspecv1.MediaTypeImageManifest, // This indicates a confused caller, not a no-op.
"this is invalid",
} {
_, err = original.UpdatedImage(types.ManifestUpdateOptions{
ManifestMIMEType: mime,
})
assert.Error(t, err, mime)
}
// m hasnt been changed:
m2 := manifestOCI1FromFixture(t, originalSrc, "oci1.json")
typedOriginal, ok := original.(*manifestOCI1)
require.True(t, ok)
typedM2, ok := m2.(*manifestOCI1)
require.True(t, ok)
assert.Equal(t, *typedM2, *typedOriginal)
}
func TestConvertToManifestSchema2(t *testing.T) {
originalSrc := newOCI1ImageSource(t, "httpd-copy:latest")
original := manifestOCI1FromFixture(t, originalSrc, "oci1.json")
res, err := original.UpdatedImage(types.ManifestUpdateOptions{
ManifestMIMEType: manifest.DockerV2Schema2MediaType,
})
require.NoError(t, err)
convertedJSON, mt, err := res.Manifest()
require.NoError(t, err)
assert.Equal(t, manifest.DockerV2Schema2MediaType, mt)
byHandJSON, err := ioutil.ReadFile("fixtures/oci1-to-schema2.json")
require.NoError(t, err)
var converted, byHand map[string]interface{}
err = json.Unmarshal(byHandJSON, &byHand)
require.NoError(t, err)
err = json.Unmarshal(convertedJSON, &converted)
require.NoError(t, err)
assert.Equal(t, byHand, converted)
// FIXME? Test also the various failure cases, if only to see that we don't crash?
}

Binary file not shown.

View file

@ -0,0 +1,26 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.serialization.config.v1+json",
"size": 7023,
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
},
"layers": [
{
"mediaType": "application/vnd.oci.image.serialization.rootfs.tar.gzip",
"size": 32654,
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
},
{
"mediaType": "application/vnd.oci.image.serialization.rootfs.tar.gzip",
"size": 16724,
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
},
{
"mediaType": "application/vnd.oci.image.serialization.rootfs.tar.gzip",
"size": 73109,
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
}
]
}

View file

@ -0,0 +1,56 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.list.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 2094,
"digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783",
"platform": {
"architecture": "ppc64le",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 1922,
"digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd",
"platform": {
"architecture": "amd64",
"os": "linux",
"features": [
"sse"
]
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 2084,
"digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2",
"platform": {
"architecture": "s390x",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 2084,
"digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40",
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "armv7"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 2090,
"digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a",
"platform": {
"architecture": "arm64",
"os": "linux",
"variant": "armv8"
}
}
]
}

View file

@ -0,0 +1,5 @@
{
"schemaVersion": 99999,
"name": "mitr/noversion-nonsense",
"tag": "latest"
}

View file

@ -0,0 +1,56 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 2094,
"digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783",
"platform": {
"architecture": "ppc64le",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 1922,
"digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd",
"platform": {
"architecture": "amd64",
"os": "linux",
"features": [
"sse"
]
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 2084,
"digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2",
"platform": {
"architecture": "s390x",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 2084,
"digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40",
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "armv7"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
"size": 2090,
"digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a",
"platform": {
"architecture": "arm64",
"os": "linux",
"variant": "armv8"
}
}
]
}

View file

@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"name": "mitr/buxybox",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
],
"history": [
],
"signatures": 1
}

View file

@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"name": "mitr/buxybox",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
{
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
},
{
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
},
{
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
}
],
"history": [
{
"v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
},
{
"v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
},
{
"v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
}
]
}

View file

@ -0,0 +1,44 @@
{
"schemaVersion": 1,
"name": "mitr/buxybox",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
{
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
},
{
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
},
{
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
}
],
"history": [
{
"v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
},
{
"v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
},
{
"v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
}
],
"signatures": [
{
"header": {
"jwk": {
"crv": "P-256",
"kid": "OZ45:U3IG:TDOI:PMBD:NGP2:LDIW:II2U:PSBI:MMCZ:YZUP:TUUO:XPZT",
"kty": "EC",
"x": "ReC5c0J9tgXSdUL4_xzEt5RsD8kFt2wWSgJcpAcOQx8",
"y": "3sBGEqQ3ZMeqPKwQBAadN2toOUEASha18xa0WwsDF-M"
},
"alg": "ES256"
},
"signature": "dV1paJ3Ck1Ph4FcEhg_frjqxdlGdI6-ywRamk6CvMOcaOEUdCWCpCPQeBQpD2N6tGjkoG1BbstkFNflllfenCw",
"protected": "eyJmb3JtYXRMZW5ndGgiOjU0NzgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNC0xOFQyMDo1NDo0MloifQ"
}
]
}

View file

@ -0,0 +1,26 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32654,
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 16724,
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 73109,
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
}
]
}

View file

@ -0,0 +1,10 @@
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
},
"layers": [
]
}

View file

@ -0,0 +1,12 @@
package manifest
import "github.com/opencontainers/go-digest"
const (
// TestV2S2ManifestDigest is the Docker manifest digest of "v2s2.manifest.json"
TestDockerV2S2ManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55")
// TestV2S1ManifestDigest is the Docker manifest digest of "v2s1.manifest.json"
TestDockerV2S1ManifestDigest = digest.Digest("sha256:077594da70fc17ec2c93cfa4e6ed1fcc26992851fb2c71861338aaf4aa9e41b1")
// TestV2S1UnsignedManifestDigest is the Docker manifest digest of "v2s1unsigned.manifest.json"
TestDockerV2S1UnsignedManifestDigest = digest.Digest("sha256:077594da70fc17ec2c93cfa4e6ed1fcc26992851fb2c71861338aaf4aa9e41b1")
)

View file

@ -0,0 +1,125 @@
package manifest
import (
"io/ioutil"
"path/filepath"
"testing"
"github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
digestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
func TestGuessMIMEType(t *testing.T) {
cases := []struct {
path string
mimeType string
}{
{"ociv1.manifest.json", imgspecv1.MediaTypeImageManifest},
{"ociv1list.manifest.json", imgspecv1.MediaTypeImageManifestList},
{"v2s2.manifest.json", DockerV2Schema2MediaType},
{"v2list.manifest.json", DockerV2ListMediaType},
{"v2s1.manifest.json", DockerV2Schema1SignedMediaType},
{"v2s1-unsigned.manifest.json", DockerV2Schema1MediaType},
{"v2s1-invalid-signatures.manifest.json", DockerV2Schema1SignedMediaType},
{"v2s2nomime.manifest.json", DockerV2Schema2MediaType}, // It is unclear whether this one is legal, but we should guess v2s2 if anything at all.
{"unknown-version.manifest.json", ""},
{"non-json.manifest.json", ""}, // Not a manifest (nor JSON) at all
}
for _, c := range cases {
manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path))
require.NoError(t, err)
mimeType := GuessMIMEType(manifest)
assert.Equal(t, c.mimeType, mimeType, c.path)
}
}
func TestDigest(t *testing.T) {
cases := []struct {
path string
expectedDigest digest.Digest
}{
{"v2s2.manifest.json", TestDockerV2S2ManifestDigest},
{"v2s1.manifest.json", TestDockerV2S1ManifestDigest},
{"v2s1-unsigned.manifest.json", TestDockerV2S1UnsignedManifestDigest},
}
for _, c := range cases {
manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path))
require.NoError(t, err)
actualDigest, err := Digest(manifest)
require.NoError(t, err)
assert.Equal(t, c.expectedDigest, actualDigest)
}
manifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json")
require.NoError(t, err)
actualDigest, err := Digest(manifest)
assert.Error(t, err)
actualDigest, err = Digest([]byte{})
require.NoError(t, err)
assert.Equal(t, digest.Digest(digestSha256EmptyTar), actualDigest)
}
func TestMatchesDigest(t *testing.T) {
cases := []struct {
path string
expectedDigest digest.Digest
result bool
}{
// Success
{"v2s2.manifest.json", TestDockerV2S2ManifestDigest, true},
{"v2s1.manifest.json", TestDockerV2S1ManifestDigest, true},
// No match (switched s1/s2)
{"v2s2.manifest.json", TestDockerV2S1ManifestDigest, false},
{"v2s1.manifest.json", TestDockerV2S2ManifestDigest, false},
// Unrecognized algorithm
{"v2s2.manifest.json", digest.Digest("md5:2872f31c5c1f62a694fbd20c1e85257c"), false},
// Mangled format
{"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String() + "abc"), false},
{"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String()[:20]), false},
{"v2s2.manifest.json", digest.Digest(""), false},
}
for _, c := range cases {
manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path))
require.NoError(t, err)
res, err := MatchesDigest(manifest, c.expectedDigest)
require.NoError(t, err)
assert.Equal(t, c.result, res)
}
manifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json")
require.NoError(t, err)
// Even a correct SHA256 hash is rejected if we can't strip the JSON signature.
res, err := MatchesDigest(manifest, digest.FromBytes(manifest))
assert.False(t, res)
assert.Error(t, err)
res, err = MatchesDigest([]byte{}, digest.Digest(digestSha256EmptyTar))
assert.True(t, res)
assert.NoError(t, err)
}
func TestAddDummyV2S1Signature(t *testing.T) {
manifest, err := ioutil.ReadFile("fixtures/v2s1-unsigned.manifest.json")
require.NoError(t, err)
signedManifest, err := AddDummyV2S1Signature(manifest)
require.NoError(t, err)
sig, err := libtrust.ParsePrettySignature(signedManifest, "signatures")
require.NoError(t, err)
signaturePayload, err := sig.Payload()
require.NoError(t, err)
assert.Equal(t, manifest, signaturePayload)
_, err = AddDummyV2S1Signature([]byte("}this is invalid JSON"))
assert.Error(t, err)
}

View file

@ -37,7 +37,6 @@ func (d *ociImageDestination) Close() {
func (d *ociImageDestination) SupportedManifestMIMETypes() []string {
return []string{
imgspecv1.MediaTypeImageManifest,
manifest.DockerV2Schema2MediaType,
}
}
@ -134,60 +133,16 @@ func (d *ociImageDestination) ReapplyBlob(info types.BlobInfo) (types.BlobInfo,
return info, nil
}
func createManifest(m []byte) ([]byte, string, error) {
om := imgspecv1.Manifest{}
mt := manifest.GuessMIMEType(m)
switch mt {
case manifest.DockerV2Schema1MediaType, manifest.DockerV2Schema1SignedMediaType:
// There a simple reason about not yet implementing this.
// OCI image-spec assure about backward compatibility with docker v2s2 but not v2s1
// generating a v2s2 is a migration docker does when upgrading to 1.10.3
// and I don't think we should bother about this now (I don't want to have migration code here in skopeo)
return nil, "", errors.New("can't create an OCI manifest from Docker V2 schema 1 manifest")
case manifest.DockerV2Schema2MediaType:
if err := json.Unmarshal(m, &om); err != nil {
return nil, "", err
}
om.MediaType = imgspecv1.MediaTypeImageManifest
for i, l := range om.Layers {
if l.MediaType == manifest.DockerV2Schema2ForeignLayerMediaType {
om.Layers[i].MediaType = imgspecv1.MediaTypeImageLayerNonDistributable
} else {
om.Layers[i].MediaType = imgspecv1.MediaTypeImageLayer
}
}
om.Config.MediaType = imgspecv1.MediaTypeImageConfig
b, err := json.Marshal(om)
if err != nil {
return nil, "", err
}
return b, om.MediaType, nil
case manifest.DockerV2ListMediaType:
return nil, "", errors.New("can't create an OCI manifest from Docker V2 schema 2 manifest list")
case imgspecv1.MediaTypeImageManifestList:
return nil, "", errors.New("can't create an OCI manifest from OCI manifest list")
case imgspecv1.MediaTypeImageManifest:
return m, mt, nil
}
return nil, "", errors.Errorf("unrecognized manifest media type %q", mt)
}
func (d *ociImageDestination) PutManifest(m []byte) error {
// TODO(mitr, runcom): this breaks signatures entirely since at this point we're creating a new manifest
// and signatures don't apply anymore. Will fix.
ociMan, mt, err := createManifest(m)
if err != nil {
return err
}
digest, err := manifest.Digest(ociMan)
digest, err := manifest.Digest(m)
if err != nil {
return err
}
desc := imgspecv1.Descriptor{}
desc.Digest = digest.String()
desc.Digest = digest
// TODO(runcom): beaware and add support for OCI manifest list
desc.MediaType = mt
desc.Size = int64(len(ociMan))
desc.MediaType = imgspecv1.MediaTypeImageManifest
desc.Size = int64(len(m))
data, err := json.Marshal(desc)
if err != nil {
return err
@ -197,7 +152,10 @@ func (d *ociImageDestination) PutManifest(m []byte) error {
if err != nil {
return err
}
if err := ioutil.WriteFile(blobPath, ociMan, 0644); err != nil {
if err := ensureParentDirectoryExists(blobPath); err != nil {
return err
}
if err := ioutil.WriteFile(blobPath, m, 0644); err != nil {
return err
}
// TODO(runcom): ugly here?

View file

@ -0,0 +1,61 @@
package layout
import (
"os"
"testing"
"github.com/containers/image/types"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// readerFromFunc allows implementing Reader by any function, e.g. a closure.
type readerFromFunc func([]byte) (int, error)
func (fn readerFromFunc) Read(p []byte) (int, error) {
return fn(p)
}
// TestPutBlobDigestFailure simulates behavior on digest verification failure.
func TestPutBlobDigestFailure(t *testing.T) {
const digestErrorString = "Simulated digest error"
const blobDigest = "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
dirRef, ok := ref.(ociReference)
require.True(t, ok)
blobPath, err := dirRef.blobPath(blobDigest)
assert.NoError(t, err)
firstRead := true
reader := readerFromFunc(func(p []byte) (int, error) {
_, err := os.Lstat(blobPath)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
if firstRead {
if len(p) > 0 {
firstRead = false
}
for i := 0; i < len(p); i++ {
p[i] = 0xAA
}
return len(p), nil
}
return 0, errors.Errorf(digestErrorString)
})
dest, err := ref.NewImageDestination(nil)
require.NoError(t, err)
defer dest.Close()
_, err = dest.PutBlob(reader, types.BlobInfo{Digest: blobDigest, Size: -1})
assert.Error(t, err)
assert.Contains(t, digestErrorString, err.Error())
err = dest.Commit()
assert.NoError(t, err)
_, err = os.Lstat(blobPath)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
}

View file

@ -0,0 +1,272 @@
package layout
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/containers/image/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTransportName(t *testing.T) {
assert.Equal(t, "oci", Transport.Name())
}
func TestTransportParseReference(t *testing.T) {
testParseReference(t, Transport.ParseReference)
}
func TestTransportValidatePolicyConfigurationScope(t *testing.T) {
for _, scope := range []string{
"/etc",
"/etc:notlatest",
"/this/does/not/exist",
"/this/does/not/exist:notlatest",
"/:strangecornercase",
} {
err := Transport.ValidatePolicyConfigurationScope(scope)
assert.NoError(t, err, scope)
}
for _, scope := range []string{
"relative/path",
"/",
"/double//slashes",
"/has/./dot",
"/has/dot/../dot",
"/trailing/slash/",
"/etc:invalid'tag!value@",
"/path:with/colons",
"/path:with/colons/and:tag",
} {
err := Transport.ValidatePolicyConfigurationScope(scope)
assert.Error(t, err, scope)
}
}
func TestParseReference(t *testing.T) {
testParseReference(t, ParseReference)
}
// testParseReference is a test shared for Transport.ParseReference and ParseReference.
func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) {
tmpDir, err := ioutil.TempDir("", "oci-transport-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
for _, path := range []string{
"/",
"/etc",
tmpDir,
"relativepath",
tmpDir + "/thisdoesnotexist",
} {
for _, tag := range []struct{ suffix, tag string }{
{":notlatest", "notlatest"},
{"", "latest"},
} {
input := path + tag.suffix
ref, err := fn(input)
require.NoError(t, err, input)
ociRef, ok := ref.(ociReference)
require.True(t, ok)
assert.Equal(t, path, ociRef.dir, input)
assert.Equal(t, tag.tag, ociRef.tag, input)
}
}
_, err = fn(tmpDir + "/with:multiple:colons:and:tag")
assert.Error(t, err)
_, err = fn(tmpDir + ":invalid'tag!value@")
assert.Error(t, err)
}
func TestNewReference(t *testing.T) {
const tagValue = "tagValue"
tmpDir, err := ioutil.TempDir("", "oci-transport-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
ref, err := NewReference(tmpDir, tagValue)
require.NoError(t, err)
ociRef, ok := ref.(ociReference)
require.True(t, ok)
assert.Equal(t, tmpDir, ociRef.dir)
assert.Equal(t, tagValue, ociRef.tag)
_, err = NewReference(tmpDir+"/thisparentdoesnotexist/something", tagValue)
assert.Error(t, err)
_, err = NewReference(tmpDir+"/has:colon", tagValue)
assert.Error(t, err)
_, err = NewReference(tmpDir, "invalid'tag!value@")
assert.Error(t, err)
}
// refToTempOCI creates a temporary directory and returns an reference to it.
// The caller should
// defer os.RemoveAll(tmpDir)
func refToTempOCI(t *testing.T) (ref types.ImageReference, tmpDir string) {
tmpDir, err := ioutil.TempDir("", "oci-transport-test")
require.NoError(t, err)
ref, err = NewReference(tmpDir, "tagValue")
require.NoError(t, err)
return ref, tmpDir
}
func TestReferenceTransport(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
assert.Equal(t, Transport, ref.Transport())
}
func TestReferenceStringWithinTransport(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "oci-transport-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
for _, c := range []struct{ input, result string }{
{"/dir1:notlatest", "/dir1:notlatest"}, // Explicit tag
{"/dir2", "/dir2:latest"}, // Default tag
} {
ref, err := ParseReference(tmpDir + c.input)
require.NoError(t, err, c.input)
stringRef := ref.StringWithinTransport()
assert.Equal(t, tmpDir+c.result, stringRef, c.input)
// Do one more round to verify that the output can be parsed, to an equal value.
ref2, err := Transport.ParseReference(stringRef)
require.NoError(t, err, c.input)
stringRef2 := ref2.StringWithinTransport()
assert.Equal(t, stringRef, stringRef2, c.input)
}
}
func TestReferenceDockerReference(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
assert.Nil(t, ref.DockerReference())
}
func TestReferencePolicyConfigurationIdentity(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
assert.Equal(t, tmpDir+":tagValue", ref.PolicyConfigurationIdentity())
// A non-canonical path. Test just one, the various other cases are
// tested in explicitfilepath.ResolvePathToFullyExplicit.
ref, err := NewReference(tmpDir+"/.", "tag2")
require.NoError(t, err)
assert.Equal(t, tmpDir+":tag2", ref.PolicyConfigurationIdentity())
// "/" as a corner case.
ref, err = NewReference("/", "tag3")
require.NoError(t, err)
assert.Equal(t, "/:tag3", ref.PolicyConfigurationIdentity())
}
func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
// We don't really know enough to make a full equality test here.
ns := ref.PolicyConfigurationNamespaces()
require.NotNil(t, ns)
assert.True(t, len(ns) >= 2)
assert.Equal(t, tmpDir, ns[0])
assert.Equal(t, filepath.Dir(tmpDir), ns[1])
// Test with a known path which should exist. Test just one non-canonical
// path, the various other cases are tested in explicitfilepath.ResolvePathToFullyExplicit.
//
// It would be nice to test a deeper hierarchy, but it is not obvious what
// deeper path is always available in the various distros, AND is not likely
// to contains a symbolic link.
for _, path := range []string{"/etc/skel", "/etc/skel/./."} {
_, err := os.Lstat(path)
require.NoError(t, err)
ref, err := NewReference(path, "sometag")
require.NoError(t, err)
ns := ref.PolicyConfigurationNamespaces()
require.NotNil(t, ns)
assert.Equal(t, []string{"/etc/skel", "/etc"}, ns)
}
// "/" as a corner case.
ref, err := NewReference("/", "tag3")
require.NoError(t, err)
assert.Equal(t, []string{}, ref.PolicyConfigurationNamespaces())
}
func TestReferenceNewImage(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
_, err := ref.NewImage(nil)
assert.Error(t, err)
}
func TestReferenceNewImageSource(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
_, err := ref.NewImageSource(nil, nil)
assert.NoError(t, err)
}
func TestReferenceNewImageDestination(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
dest, err := ref.NewImageDestination(nil)
assert.NoError(t, err)
defer dest.Close()
}
func TestReferenceDeleteImage(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
err := ref.DeleteImage(nil)
assert.Error(t, err)
}
func TestReferenceOCILayoutPath(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
ociRef, ok := ref.(ociReference)
require.True(t, ok)
assert.Equal(t, tmpDir+"/oci-layout", ociRef.ociLayoutPath())
}
func TestReferenceBlobPath(t *testing.T) {
const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
ociRef, ok := ref.(ociReference)
require.True(t, ok)
bp, err := ociRef.blobPath("sha256:" + hex)
assert.NoError(t, err)
assert.Equal(t, tmpDir+"/blobs/sha256/"+hex, bp)
}
func TestReferenceBlobPathInvalid(t *testing.T) {
const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
ociRef, ok := ref.(ociReference)
require.True(t, ok)
_, err := ociRef.blobPath(hex)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unexpected digest reference "+hex)
}
func TestReferenceDescriptorPath(t *testing.T) {
ref, tmpDir := refToTempOCI(t)
defer os.RemoveAll(tmpDir)
ociRef, ok := ref.(ociReference)
require.True(t, ok)
assert.Equal(t, tmpDir+"/refs/notlatest", ociRef.descriptorPath("notlatest"))
}

1
vendor/github.com/containers/image/oci/oci.go generated vendored Normal file
View file

@ -0,0 +1 @@
package oci

View file

@ -13,14 +13,14 @@ import (
"path"
"path/filepath"
"reflect"
"strings"
"time"
"github.com/ghodss/yaml"
"github.com/imdario/mergo"
"github.com/pkg/errors"
utilerrors "k8s.io/kubernetes/pkg/util/errors"
"k8s.io/kubernetes/pkg/util/homedir"
utilnet "k8s.io/kubernetes/pkg/util/net"
"golang.org/x/net/http2"
"k8s.io/client-go/util/homedir"
)
// restTLSClientConfig is a modified copy of k8s.io/kubernets/pkg/client/restclient.TLSClientConfig.
@ -450,6 +450,55 @@ func (config *directClientConfig) getCluster() clientcmdCluster {
return mergedClusterInfo
}
// aggregateErr is a modified copy of k8s.io/apimachinery/pkg/util/errors.aggregate.
// This helper implements the error and Errors interfaces. Keeping it private
// prevents people from making an aggregate of 0 errors, which is not
// an error, but does satisfy the error interface.
type aggregateErr []error
// newAggregate is a modified copy of k8s.io/apimachinery/pkg/util/errors.NewAggregate.
// NewAggregate converts a slice of errors into an Aggregate interface, which
// is itself an implementation of the error interface. If the slice is empty,
// this returns nil.
// It will check if any of the element of input error list is nil, to avoid
// nil pointer panic when call Error().
func newAggregate(errlist []error) error {
if len(errlist) == 0 {
return nil
}
// In case of input error list contains nil
var errs []error
for _, e := range errlist {
if e != nil {
errs = append(errs, e)
}
}
if len(errs) == 0 {
return nil
}
return aggregateErr(errs)
}
// Error is a modified copy of k8s.io/apimachinery/pkg/util/errors.aggregate.Error.
// Error is part of the error interface.
func (agg aggregateErr) Error() string {
if len(agg) == 0 {
// This should never happen, really.
return ""
}
if len(agg) == 1 {
return agg[0].Error()
}
result := fmt.Sprintf("[%s", agg[0].Error())
for i := 1; i < len(agg); i++ {
result += fmt.Sprintf(", %s", agg[i].Error())
}
result += "]"
return result
}
// REMOVED: aggregateErr.Errors
// errConfigurationInvalid is a modified? copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.errConfigurationInvalid.
// errConfigurationInvalid is a set of errors indicating the configuration is invalid.
type errConfigurationInvalid []error
@ -470,7 +519,7 @@ func newErrConfigurationInvalid(errs []error) error {
// Error implements the error interface
func (e errConfigurationInvalid) Error() string {
return fmt.Sprintf("invalid configuration: %v", utilerrors.NewAggregate(e).Error())
return fmt.Sprintf("invalid configuration: %v", newAggregate(e).Error())
}
// clientConfigLoadingRules is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.ClientConfigLoadingRules
@ -550,7 +599,7 @@ func (rules *clientConfigLoadingRules) Load() (*clientcmdConfig, error) {
errlist = append(errlist, err)
}
return config, utilerrors.NewAggregate(errlist)
return config, newAggregate(errlist)
}
// loadFromFile is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.LoadFromFile
@ -799,6 +848,52 @@ func transportNew(config *restConfig) (http.RoundTripper, error) {
return rt, nil
}
// newProxierWithNoProxyCIDR is a modified copy of k8s.io/apimachinery/pkg/util/net.NewProxierWithNoProxyCIDR.
// NewProxierWithNoProxyCIDR constructs a Proxier function that respects CIDRs in NO_PROXY and delegates if
// no matching CIDRs are found
func newProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error)) func(req *http.Request) (*url.URL, error) {
// we wrap the default method, so we only need to perform our check if the NO_PROXY envvar has a CIDR in it
noProxyEnv := os.Getenv("NO_PROXY")
noProxyRules := strings.Split(noProxyEnv, ",")
cidrs := []*net.IPNet{}
for _, noProxyRule := range noProxyRules {
_, cidr, _ := net.ParseCIDR(noProxyRule)
if cidr != nil {
cidrs = append(cidrs, cidr)
}
}
if len(cidrs) == 0 {
return delegate
}
return func(req *http.Request) (*url.URL, error) {
host := req.URL.Host
// for some urls, the Host is already the host, not the host:port
if net.ParseIP(host) == nil {
var err error
host, _, err = net.SplitHostPort(req.URL.Host)
if err != nil {
return delegate(req)
}
}
ip := net.ParseIP(host)
if ip == nil {
return delegate(req)
}
for _, cidr := range cidrs {
if cidr.Contains(ip) {
return nil, nil
}
}
return delegate(req)
}
}
// tlsCacheGet is a modified copy of k8s.io/kubernetes/pkg/client/transport.tlsTransportCache.get.
func tlsCacheGet(config *restConfig) (http.RoundTripper, error) {
// REMOVED: any actual caching
@ -813,15 +908,23 @@ func tlsCacheGet(config *restConfig) (http.RoundTripper, error) {
return http.DefaultTransport, nil
}
return utilnet.SetTransportDefaults(&http.Transport{ // FIXME??
Proxy: http.ProxyFromEnvironment,
// REMOVED: Call to k8s.io/apimachinery/pkg/util/net.SetTransportDefaults; instead of the generic machinery and conditionals, hard-coded the result here.
t := &http.Transport{
// http.ProxyFromEnvironment doesn't respect CIDRs and that makes it impossible to exclude things like pod and service IPs from proxy settings
// ProxierWithNoProxyCIDR allows CIDR rules in NO_PROXY
Proxy: newProxierWithNoProxyCIDR(http.ProxyFromEnvironment),
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: tlsConfig,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
}), nil
}
// Allow clients to disable http2 if needed.
if s := os.Getenv("DISABLE_HTTP2"); len(s) == 0 {
_ = http2.ConfigureTransport(t)
}
return t, nil
}
// tlsConfigFor is a modified copy of k8s.io/kubernetes/pkg/client/transport.TLSConfigFor.

View file

@ -0,0 +1,125 @@
package openshift
import (
"testing"
"github.com/containers/image/docker/reference"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
sha256digest = "@sha256:" + sha256digestHex
)
func TestTransportName(t *testing.T) {
assert.Equal(t, "atomic", Transport.Name())
}
func TestTransportValidatePolicyConfigurationScope(t *testing.T) {
for _, scope := range []string{
"registry.example.com/ns/stream" + sha256digest,
"registry.example.com/ns/stream:notlatest",
"registry.example.com/ns/stream",
"registry.example.com/ns",
"registry.example.com",
} {
err := Transport.ValidatePolicyConfigurationScope(scope)
assert.NoError(t, err, scope)
}
for _, scope := range []string{
"registry.example.com/too/deep/hierarchy",
"registry.example.com/ns/stream:tag1:tag2",
} {
err := Transport.ValidatePolicyConfigurationScope(scope)
assert.Error(t, err, scope)
}
}
func TestNewReference(t *testing.T) {
// too many ns
r, err := reference.ParseNamed("registry.example.com/ns1/ns2/ns3/stream:tag")
require.NoError(t, err)
tagged, ok := r.(reference.NamedTagged)
require.True(t, ok)
_, err = NewReference(tagged)
assert.Error(t, err)
r, err = reference.ParseNamed("registry.example.com/ns/stream:tag")
require.NoError(t, err)
tagged, ok = r.(reference.NamedTagged)
require.True(t, ok)
_, err = NewReference(tagged)
assert.NoError(t, err)
}
func TestParseReference(t *testing.T) {
// Success
ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest")
require.NoError(t, err)
osRef, ok := ref.(openshiftReference)
require.True(t, ok)
assert.Equal(t, "ns", osRef.namespace)
assert.Equal(t, "stream", osRef.stream)
assert.Equal(t, "notlatest", osRef.dockerReference.Tag())
assert.Equal(t, "registry.example.com:8443", osRef.dockerReference.Hostname())
// Components creating an invalid Docker Reference name
_, err = ParseReference("registry.example.com/ns/UPPERCASEISINVALID:notlatest")
assert.Error(t, err)
_, err = ParseReference("registry.example.com/ns/stream:invalid!tag@value=")
assert.Error(t, err)
}
func TestReferenceDockerReference(t *testing.T) {
ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest")
require.NoError(t, err)
dockerRef := ref.DockerReference()
require.NotNil(t, dockerRef)
assert.Equal(t, "registry.example.com:8443/ns/stream:notlatest", dockerRef.String())
}
func TestReferenceTransport(t *testing.T) {
ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest")
require.NoError(t, err)
assert.Equal(t, Transport, ref.Transport())
}
func TestReferenceStringWithinTransport(t *testing.T) {
ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest")
require.NoError(t, err)
assert.Equal(t, "registry.example.com:8443/ns/stream:notlatest", ref.StringWithinTransport())
// We should do one more round to verify that the output can be parsed, to an equal value,
// 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 := ParseReference("registry.example.com:8443/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 := ParseReference("registry.example.com:8443/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())
}
// openshiftReference.NewImage, openshiftReference.NewImageSource, openshiftReference.NewImageDestination untested because they depend
// on per-user configuration when initializing httpClient.
func TestReferenceDeleteImage(t *testing.T) {
ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest")
require.NoError(t, err)
err = ref.DeleteImage(nil)
assert.Error(t, err)
}

View file

@ -5,6 +5,7 @@ package signature
import (
"fmt"
"github.com/containers/image/docker/reference"
"github.com/containers/image/manifest"
"github.com/opencontainers/go-digest"
)
@ -16,12 +17,7 @@ func SignDockerManifest(m []byte, dockerReference string, mech SigningMechanism,
if err != nil {
return nil, err
}
sig := privateSignature{
Signature{
DockerManifestDigest: manifestDigest,
DockerReference: dockerReference,
},
}
sig := newUntrustedSignature(manifestDigest, dockerReference)
return sig.sign(mech, keyIdentity)
}
@ -29,6 +25,10 @@ func SignDockerManifest(m []byte, dockerReference string, mech SigningMechanism,
// using mech.
func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byte,
expectedDockerReference string, mech SigningMechanism, expectedKeyIdentity string) (*Signature, error) {
expectedRef, err := reference.ParseNamed(expectedDockerReference)
if err != nil {
return nil, err
}
sig, err := verifyAndExtractSignature(mech, unverifiedSignature, signatureAcceptanceRules{
validateKeyIdentity: func(keyIdentity string) error {
if keyIdentity != expectedKeyIdentity {
@ -37,7 +37,11 @@ func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byt
return nil
},
validateSignedDockerReference: func(signedDockerReference string) error {
if signedDockerReference != expectedDockerReference {
signedRef, err := reference.ParseNamed(signedDockerReference)
if err != nil {
return InvalidSignatureError{msg: fmt.Sprintf("Invalid docker reference %s in signature", signedDockerReference)}
}
if signedRef.String() != expectedRef.String() {
return InvalidSignatureError{msg: fmt.Sprintf("Docker reference %s does not match %s",
signedDockerReference, expectedDockerReference)}
}

View file

@ -0,0 +1,101 @@
package signature
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSignDockerManifest(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
manifest, err := ioutil.ReadFile("fixtures/image.manifest.json")
require.NoError(t, err)
// Successful signing
signature, err := SignDockerManifest(manifest, TestImageSignatureReference, mech, TestKeyFingerprint)
require.NoError(t, err)
verified, err := VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint)
assert.NoError(t, err)
assert.Equal(t, TestImageSignatureReference, verified.DockerReference)
assert.Equal(t, TestImageManifestDigest, verified.DockerManifestDigest)
// Error computing Docker manifest
invalidManifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json")
require.NoError(t, err)
_, err = SignDockerManifest(invalidManifest, TestImageSignatureReference, mech, TestKeyFingerprint)
assert.Error(t, err)
// Error creating blob to sign
_, err = SignDockerManifest(manifest, "", mech, TestKeyFingerprint)
assert.Error(t, err)
// Error signing
_, err = SignDockerManifest(manifest, TestImageSignatureReference, mech, "this fingerprint doesn't exist")
assert.Error(t, err)
}
func TestVerifyDockerManifestSignature(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
manifest, err := ioutil.ReadFile("fixtures/image.manifest.json")
require.NoError(t, err)
signature, err := ioutil.ReadFile("fixtures/image.signature")
require.NoError(t, err)
// Successful verification
sig, err := VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint)
require.NoError(t, err)
assert.Equal(t, TestImageSignatureReference, sig.DockerReference)
assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest)
// Verification using a different canonicalization of TestImageSignatureReference
sig, err = VerifyDockerManifestSignature(signature, manifest, "docker.io/"+TestImageSignatureReference, mech, TestKeyFingerprint)
require.NoError(t, err)
assert.Equal(t, TestImageSignatureReference, sig.DockerReference)
assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest)
// For extra paranoia, test that we return nil data on error.
// Invalid docker reference on input
sig, err = VerifyDockerManifestSignature(signature, manifest, "UPPERCASEISINVALID", mech, TestKeyFingerprint)
assert.Error(t, err)
assert.Nil(t, sig)
// Error computing Docker manifest
invalidManifest, err := ioutil.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json")
require.NoError(t, err)
sig, err = VerifyDockerManifestSignature(signature, invalidManifest, TestImageSignatureReference, mech, TestKeyFingerprint)
assert.Error(t, err)
assert.Nil(t, sig)
// Error verifying signature
corruptSignature, err := ioutil.ReadFile("fixtures/corrupt.signature")
sig, err = VerifyDockerManifestSignature(corruptSignature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint)
assert.Error(t, err)
assert.Nil(t, sig)
// Key fingerprint mismatch
sig, err = VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, "unexpected fingerprint")
assert.Error(t, err)
assert.Nil(t, sig)
// Invalid reference in the signature
invalidReferenceSignature, err := ioutil.ReadFile("fixtures/invalid-reference.signature")
sig, err = VerifyDockerManifestSignature(invalidReferenceSignature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint)
assert.Error(t, err)
assert.Nil(t, sig)
// Docker reference mismatch
sig, err = VerifyDockerManifestSignature(signature, manifest, "example.com/doesnt/match", mech, TestKeyFingerprint)
assert.Error(t, err)
assert.Nil(t, sig)
// Docker manifest digest mismatch
sig, err = VerifyDockerManifestSignature(signature, []byte("unexpected manifest"), TestImageSignatureReference, mech, TestKeyFingerprint)
assert.Error(t, err)
assert.Nil(t, sig)
}

View file

@ -0,0 +1,4 @@
/*.gpg~
/.gpg-v21-migrated
/private-keys-v1.d
/random_seed

Binary file not shown.

View file

@ -0,0 +1,27 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32654,
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 16724,
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 73109,
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
}
],
"extra": "this manifest has been modified"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,26 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32654,
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 16724,
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 73109,
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,96 @@
{
"default": [
{
"type": "reject"
}
],
"transports": {
"dir": {
"": [
{
"type": "insecureAcceptAnything"
}
]
},
"docker": {
"example.com/playground": [
{
"type": "insecureAcceptAnything"
}
],
"example.com/production": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/keys/employee-gpg-keyring"
}
],
"example.com/hardened": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/keys/employee-gpg-keyring",
"signedIdentity": {
"type": "matchRepository"
}
},
{
"type": "signedBy",
"keyType": "signedByGPGKeys",
"keyPath": "/keys/public-key-signing-gpg-keyring",
"signedIdentity": {
"type": "matchExact"
}
},
{
"type": "signedBaseLayer",
"baseLayerIdentity": {
"type": "exactRepository",
"dockerRepository": "registry.access.redhat.com/rhel7/rhel"
}
}
],
"example.com/hardened-x509": [
{
"type": "signedBy",
"keyType": "X509Certificates",
"keyPath": "/keys/employee-cert-file",
"signedIdentity": {
"type": "matchRepository"
}
},
{
"type": "signedBy",
"keyType": "signedByX509CAs",
"keyPath": "/keys/public-key-signing-ca-file"
}
],
"registry.access.redhat.com": [
{
"type": "signedBy",
"keyType": "signedByGPGKeys",
"keyPath": "/keys/RH-key-signing-key-gpg-keyring",
"signedIdentity": {
"type": "matchRepoDigestOrExact"
}
}
],
"bogus/key-data-example": [
{
"type": "signedBy",
"keyType": "signedByGPGKeys",
"keyData": "bm9uc2Vuc2U="
}
],
"bogus/signed-identity-example": [
{
"type": "signedBaseLayer",
"baseLayerIdentity": {
"type": "exactReference",
"dockerReference": "registry.access.redhat.com/rhel7/rhel:latest"
}
}
]
}
}
}

View file

@ -0,0 +1,19 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mI0EVurzqQEEAL3qkFq4K2URtSWVDYnQUNA9HdM9sqS2eAWfqUFMrkD5f+oN+LBL
tPyaE5GNLA0vXY7nHAM2TeM8ijZ/eMP17Raj64JL8GhCymL3wn2jNvb9XaF0R0s6
H0IaRPPu45A3SnxLwm4Orc/9Z7/UxtYjKSg9xOaTiVPzJgaf5Vm4J4ApABEBAAG0
EnNrb3BlbyB0ZXN0aW5nIGtleYi4BBMBAgAiBQJW6vOpAhsDBgsJCAcDAgYVCAIJ
CgsEFgIDAQIeAQIXgAAKCRDbcvIYi7RsyBbOBACgJFiKDlQ1UyvsNmGqJ7D0OpbS
1OppJlradKgZXyfahFswhFI+7ZREvELLHbinq3dBy5cLXRWzQKdJZNHknSN5Tjf2
0ipVBQuqpcBo+dnKiG4zH6fhTri7yeTZksIDfsqlI6FXDOdKLUSnahagEBn4yU+x
jHPvZk5SuuZv56A45biNBFbq86kBBADIC/9CsAlOmRALuYUmkhcqEjuFwn3wKz2d
IBjzgvro7zcVNNCgxQfMEjcUsvEh5cx13G3QQHcwOKy3M6Bv6VMhfZjd+1P1el4P
0fJS8GFmhWRBknMN8jFsgyohQeouQ798RFFv94KszfStNnr/ae8oao5URmoUXSCa
/MdUxn0YKwARAQABiJ8EGAECAAkFAlbq86kCGwwACgkQ23LyGIu0bMjUywQAq0dn
lUpDNSoLTcpNWuVvHQ7c/qmnE4TyiSLiRiAywdEWA6gMiyhUUucuGsEhMFP1WX1k
UNwArZ6UG7BDOUsvngP7jKGNqyUOQrq1s/r8D+0MrJGOWErGLlfttO2WeoijECkI
5qm8cXzAra3Xf/Z3VjxYTKSnNu37LtZkakdTdYE=
=tJAt
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"name": "mitr/buxybox",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
],
"history": [
],
"signatures": 1
}

View file

@ -0,0 +1,14 @@
package signature
import "github.com/opencontainers/go-digest"
const (
// TestImageManifestDigest is the Docker manifest digest of "image.manifest.json"
TestImageManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55")
// TestImageSignatureReference is the Docker image reference signed in "image.signature"
TestImageSignatureReference = "testing/manifest"
// TestKeyFingerprint is the fingerprint of the private key in this directory.
TestKeyFingerprint = "1D8230F6CDB6A06716E414C1DB72F2188BB46CC8"
// TestKeyShortID is the short ID of the private key in this directory.
TestKeyShortID = "DB72F2188BB46CC8"
)

View file

@ -29,6 +29,23 @@ func validateExactMapKeys(m map[string]interface{}, expectedKeys ...string) erro
return nil
}
// int64Field returns a member fieldName of m, if it is an int64, or an error.
func int64Field(m map[string]interface{}, fieldName string) (int64, error) {
untyped, ok := m[fieldName]
if !ok {
return -1, jsonFormatError(fmt.Sprintf("Field %s missing", fieldName))
}
f, ok := untyped.(float64)
if !ok {
return -1, jsonFormatError(fmt.Sprintf("Field %s is not a number", fieldName))
}
v := int64(f)
if float64(v) != f {
return -1, jsonFormatError(fmt.Sprintf("Field %s is not an integer", fieldName))
}
return v, nil
}
// mapField returns a member fieldName of m, if it is a JSON map, or an error.
func mapField(m map[string]interface{}, fieldName string) (map[string]interface{}, error) {
untyped, ok := m[fieldName]
@ -50,7 +67,7 @@ func stringField(m map[string]interface{}, fieldName string) (string, error) {
}
v, ok := untyped.(string)
if !ok {
return "", jsonFormatError(fmt.Sprintf("Field %s is not a JSON object", fieldName))
return "", jsonFormatError(fmt.Sprintf("Field %s is not a string", fieldName))
}
return v, nil
}

View file

@ -0,0 +1,182 @@
package signature
import (
"encoding/json"
"fmt"
"math"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mSI map[string]interface{} // To minimize typing the long name
// A short-hand way to get a JSON object field value or panic. No error handling done, we know
// what we are working with, a panic in a test is good enough, and fitting test cases on a single line
// is a priority.
func x(m mSI, fields ...string) mSI {
for _, field := range fields {
// Not .(mSI) because type assertion of an unnamed type to a named type always fails (the types
// are not "identical"), but the assignment is fine because they are "assignable".
m = m[field].(map[string]interface{})
}
return m
}
func TestValidateExactMapKeys(t *testing.T) {
// Empty map and keys
err := validateExactMapKeys(mSI{})
assert.NoError(t, err)
// Success
err = validateExactMapKeys(mSI{"a": nil, "b": 1}, "b", "a")
assert.NoError(t, err)
// Extra map keys
err = validateExactMapKeys(mSI{"a": nil, "b": 1}, "a")
assert.Error(t, err)
// Extra expected keys
err = validateExactMapKeys(mSI{"a": 1}, "b", "a")
assert.Error(t, err)
// Unexpected key values
err = validateExactMapKeys(mSI{"a": 1}, "b")
assert.Error(t, err)
}
func TestInt64Field(t *testing.T) {
// Field not found
_, err := int64Field(mSI{"a": "x"}, "b")
assert.Error(t, err)
// Field has a wrong type
_, err = int64Field(mSI{"a": "string"}, "a")
assert.Error(t, err)
for _, value := range []float64{
0.5, // Fractional input
math.Inf(1), // Infinity
math.NaN(), // NaN
} {
_, err = int64Field(mSI{"a": value}, "a")
assert.Error(t, err, fmt.Sprintf("%f", value))
}
// Success
// The float64 type has 53 bits of effective precision, so ±1FFFFFFFFFFFFF is the
// range of integer values which can all be represented exactly (beyond that,
// some are representable if they are divisible by a high enough power of 2,
// but most are not).
for _, value := range []int64{0, 1, -1, 0x1FFFFFFFFFFFFF, -0x1FFFFFFFFFFFFF} {
testName := fmt.Sprintf("%d", value)
v, err := int64Field(mSI{"a": float64(value), "b": nil}, "a")
require.NoError(t, err, testName)
assert.Equal(t, value, v, testName)
}
}
func TestMapField(t *testing.T) {
// Field not found
_, err := mapField(mSI{"a": mSI{}}, "b")
assert.Error(t, err)
// Field has a wrong type
_, err = mapField(mSI{"a": 1}, "a")
assert.Error(t, err)
// Success
// FIXME? We can't use mSI as the type of child, that type apparently can't be converted to the raw map type.
child := map[string]interface{}{"b": mSI{}}
m, err := mapField(mSI{"a": child, "b": nil}, "a")
require.NoError(t, err)
assert.Equal(t, child, m)
}
func TestStringField(t *testing.T) {
// Field not found
_, err := stringField(mSI{"a": "x"}, "b")
assert.Error(t, err)
// Field has a wrong type
_, err = stringField(mSI{"a": 1}, "a")
assert.Error(t, err)
// Success
s, err := stringField(mSI{"a": "x", "b": nil}, "a")
require.NoError(t, err)
assert.Equal(t, "x", s)
}
// implementsUnmarshalJSON is a minimalistic type used to detect that
// paranoidUnmarshalJSONObject uses the json.Unmarshaler interface of resolved
// pointers.
type implementsUnmarshalJSON bool
// Compile-time check that Policy implements json.Unmarshaler.
var _ json.Unmarshaler = (*implementsUnmarshalJSON)(nil)
func (dest *implementsUnmarshalJSON) UnmarshalJSON(data []byte) error {
_ = data // We don't care, not really.
*dest = true // Mark handler as called
return nil
}
func TestParanoidUnmarshalJSONObject(t *testing.T) {
type testStruct struct {
A string
B int
}
ts := testStruct{}
var unmarshalJSONCalled implementsUnmarshalJSON
tsResolver := func(key string) interface{} {
switch key {
case "a":
return &ts.A
case "b":
return &ts.B
case "implementsUnmarshalJSON":
return &unmarshalJSONCalled
default:
return nil
}
}
// Empty object
ts = testStruct{}
err := paranoidUnmarshalJSONObject([]byte(`{}`), tsResolver)
require.NoError(t, err)
assert.Equal(t, testStruct{}, ts)
// Success
ts = testStruct{}
err = paranoidUnmarshalJSONObject([]byte(`{"a":"x", "b":2}`), tsResolver)
require.NoError(t, err)
assert.Equal(t, testStruct{A: "x", B: 2}, ts)
// json.Unamarshaler is used for decoding values
ts = testStruct{}
unmarshalJSONCalled = implementsUnmarshalJSON(false)
err = paranoidUnmarshalJSONObject([]byte(`{"implementsUnmarshalJSON":true}`), tsResolver)
require.NoError(t, err)
assert.Equal(t, unmarshalJSONCalled, implementsUnmarshalJSON(true))
// Various kinds of invalid input
for _, input := range []string{
``, // Empty input
`&`, // Entirely invalid JSON
`1`, // Not an object
`{&}`, // Invalid key JSON
`{1:1}`, // Key not a string
`{"b":1, "b":1}`, // Duplicate key
`{"thisdoesnotexist":1}`, // Key rejected by resolver
`{"a":&}`, // Invalid value JSON
`{"a":1}`, // Type mismatch
`{"a":"value"}{}`, // Extra data after object
} {
ts = testStruct{}
err := paranoidUnmarshalJSONObject([]byte(input), tsResolver)
assert.Error(t, err, input)
}
}

View file

@ -4,9 +4,13 @@ package signature
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"strings"
"github.com/mtrmac/gpgme"
"golang.org/x/crypto/openpgp"
)
// SigningMechanism abstracts a way to sign binary blobs and verify their signatures.
@ -21,6 +25,12 @@ type SigningMechanism interface {
Sign(input []byte, keyIdentity string) ([]byte, error)
// Verify parses unverifiedSignature and returns the content and the signer's identity
Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error)
// UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION,
// along with a short identifier of the key used for signing.
// WARNING: The short key identifier (which correponds to "Key ID" for OpenPGP keys)
// is NOT the same as a "key identity" used in other calls ot this interface, and
// the values may have no recognizable relationship if the public key is not available.
UntrustedSignatureContents(untrustedSignature []byte) (untrustedContents []byte, shortKeyIdentifier string, err error)
}
// A GPG/OpenPGP signing mechanism.
@ -119,3 +129,31 @@ func (m gpgSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte
}
return signedBuffer.Bytes(), sig.Fingerprint, nil
}
// UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION,
// along with a short identifier of the key used for signing.
// WARNING: The short key identifier (which correponds to "Key ID" for OpenPGP keys)
// is NOT the same as a "key identity" used in other calls ot this interface, and
// the values may have no recognizable relationship if the public key is not available.
func (m gpgSigningMechanism) UntrustedSignatureContents(untrustedSignature []byte) (untrustedContents []byte, shortKeyIdentifier string, err error) {
// This uses the Golang-native OpenPGP implementation instead of gpgme because we are not doing any cryptography.
md, err := openpgp.ReadMessage(bytes.NewReader(untrustedSignature), openpgp.EntityList{}, nil, nil)
if err != nil {
return nil, "", err
}
if !md.IsSigned {
return nil, "", errors.New("The input is not a signature")
}
content, err := ioutil.ReadAll(md.UnverifiedBody)
if err != nil {
// Coverage: An error during reading the body can happen only if
// 1) the message is encrypted, which is not our case (and we dont give ReadMessage the key
// to decrypt the contents anyway), or
// 2) the message is signed AND we give ReadMessage a correspnding public key, which we dont.
return nil, "", err
}
// Uppercase the key ID for minimal consistency with the gpgme-returned fingerprints
// (but note that key ID is a suffix of the fingerprint only for V4 keys, not V3)!
return content, strings.ToUpper(fmt.Sprintf("%016X", md.SignedByKeyId)), nil
}

View file

@ -0,0 +1,205 @@
package signature
import (
"bytes"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testGPGHomeDirectory = "./fixtures"
)
func TestNewGPGSigningMechanism(t *testing.T) {
// A dumb test just for code coverage. We test more with newGPGSigningMechanismInDirectory().
_, err := NewGPGSigningMechanism()
assert.NoError(t, err)
}
func TestNewGPGSigningMechanismInDirectory(t *testing.T) {
// A dumb test just for code coverage.
_, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
assert.NoError(t, err)
// The various GPG failure cases are not obviously easy to reach.
}
func TestGPGSigningMechanismImportKeysFromBytes(t *testing.T) {
testDir, err := ioutil.TempDir("", "gpg-import-keys")
require.NoError(t, err)
defer os.RemoveAll(testDir)
mech, err := newGPGSigningMechanismInDirectory(testDir)
require.NoError(t, err)
// Try validating a signature when the key is unknown.
signature, err := ioutil.ReadFile("./fixtures/invalid-blob.signature")
require.NoError(t, err)
content, signingFingerprint, err := mech.Verify(signature)
require.Error(t, err)
// Successful import
keyBlob, err := ioutil.ReadFile("./fixtures/public-key.gpg")
require.NoError(t, err)
keyIdentities, err := mech.ImportKeysFromBytes(keyBlob)
require.NoError(t, err)
assert.Equal(t, []string{TestKeyFingerprint}, keyIdentities)
// After import, the signature should validate.
content, signingFingerprint, err = mech.Verify(signature)
require.NoError(t, err)
assert.Equal(t, []byte("This is not JSON\n"), content)
assert.Equal(t, TestKeyFingerprint, signingFingerprint)
// Two keys: just concatenate the valid input twice.
keyIdentities, err = mech.ImportKeysFromBytes(bytes.Join([][]byte{keyBlob, keyBlob}, nil))
require.NoError(t, err)
assert.Equal(t, []string{TestKeyFingerprint, TestKeyFingerprint}, keyIdentities)
// Invalid input: This is accepted anyway by GPG, just returns no keys.
keyIdentities, err = mech.ImportKeysFromBytes([]byte("This is invalid"))
require.NoError(t, err)
assert.Equal(t, []string{}, keyIdentities)
// The various GPG/GPGME failures cases are not obviously easy to reach.
}
func TestGPGSigningMechanismSign(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
// Successful signing
content := []byte("content")
signature, err := mech.Sign(content, TestKeyFingerprint)
require.NoError(t, err)
signedContent, signingFingerprint, err := mech.Verify(signature)
require.NoError(t, err)
assert.EqualValues(t, content, signedContent)
assert.Equal(t, TestKeyFingerprint, signingFingerprint)
// Error signing
_, err = mech.Sign(content, "this fingerprint doesn't exist")
assert.Error(t, err)
// The various GPG/GPGME failures cases are not obviously easy to reach.
}
func assertSigningError(t *testing.T, content []byte, fingerprint string, err error) {
assert.Error(t, err)
assert.Nil(t, content)
assert.Empty(t, fingerprint)
}
func TestGPGSigningMechanismVerify(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
// Successful verification
signature, err := ioutil.ReadFile("./fixtures/invalid-blob.signature")
require.NoError(t, err)
content, signingFingerprint, err := mech.Verify(signature)
require.NoError(t, err)
assert.Equal(t, []byte("This is not JSON\n"), content)
assert.Equal(t, TestKeyFingerprint, signingFingerprint)
// For extra paranoia, test that we return nil data on error.
// Completely invalid signature.
content, signingFingerprint, err = mech.Verify([]byte{})
assertSigningError(t, content, signingFingerprint, err)
content, signingFingerprint, err = mech.Verify([]byte("invalid signature"))
assertSigningError(t, content, signingFingerprint, err)
// Literal packet, not a signature
signature, err = ioutil.ReadFile("./fixtures/unsigned-literal.signature")
require.NoError(t, err)
content, signingFingerprint, err = mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err)
// Encrypted data, not a signature.
signature, err = ioutil.ReadFile("./fixtures/unsigned-encrypted.signature")
require.NoError(t, err)
content, signingFingerprint, err = mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err)
// FIXME? Is there a way to create a multi-signature so that gpgme_op_verify returns multiple signatures?
// Expired signature
signature, err = ioutil.ReadFile("./fixtures/expired.signature")
require.NoError(t, err)
content, signingFingerprint, err = mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err)
// Corrupt signature
signature, err = ioutil.ReadFile("./fixtures/corrupt.signature")
require.NoError(t, err)
content, signingFingerprint, err = mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err)
// Valid signature with an unknown key
signature, err = ioutil.ReadFile("./fixtures/unknown-key.signature")
require.NoError(t, err)
content, signingFingerprint, err = mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err)
// The various GPG/GPGME failures cases are not obviously easy to reach.
}
func TestGPGSigningMechanismUntrustedSignatureContents(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
// A valid signature
signature, err := ioutil.ReadFile("./fixtures/invalid-blob.signature")
require.NoError(t, err)
content, shortKeyID, err := mech.UntrustedSignatureContents(signature)
require.NoError(t, err)
assert.Equal(t, []byte("This is not JSON\n"), content)
assert.Equal(t, TestKeyShortID, shortKeyID)
// Completely invalid signature.
_, _, err = mech.UntrustedSignatureContents([]byte{})
assert.Error(t, err)
_, _, err = mech.UntrustedSignatureContents([]byte("invalid signature"))
assert.Error(t, err)
// Literal packet, not a signature
signature, err = ioutil.ReadFile("./fixtures/unsigned-literal.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
assert.Error(t, err)
// Encrypted data, not a signature.
signature, err = ioutil.ReadFile("./fixtures/unsigned-encrypted.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
assert.Error(t, err)
// Expired signature
signature, err = ioutil.ReadFile("./fixtures/expired.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
require.NoError(t, err)
assert.Equal(t, []byte("This signature is expired.\n"), content)
assert.Equal(t, TestKeyShortID, shortKeyID)
// Corrupt signature
signature, err = ioutil.ReadFile("./fixtures/corrupt.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
require.NoError(t, err)
assert.Equal(t, []byte(`{"critical":{"identity":{"docker-reference":"testing/manifest"},"image":{"docker-manifest-digest":"sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"},"type":"atomic container signature"},"optional":{"creator":"atomic ","timestamp":1458239713}}`), content)
assert.Equal(t, TestKeyShortID, shortKeyID)
// Valid signature with an unknown key
signature, err = ioutil.ReadFile("./fixtures/unknown-key.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
require.NoError(t, err)
assert.Equal(t, []byte(`{"critical":{"identity":{"docker-reference":"testing/manifest"},"image":{"docker-manifest-digest":"sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"},"type":"atomic container signature"},"optional":{"creator":"atomic 0.1.13-dev","timestamp":1464633474}}`), content)
assert.Equal(t, "E5476D1110D07803", shortKeyID)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
package signature
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestPRSignedBaseLayerIsSignatureAuthorAccepted(t *testing.T) {
pr, err := NewPRSignedBaseLayer(NewPRMMatchRepository())
require.NoError(t, err)
// Pass nil pointers to, kind of, test that the return value does not depend on the parameters.
sar, parsedSig, err := pr.isSignatureAuthorAccepted(nil, nil)
assertSARUnknown(t, sar, parsedSig, err)
}
func TestPRSignedBaseLayerIsRunningImageAllowed(t *testing.T) {
// This will obviously need to change after signedBaseLayer is implemented.
pr, err := NewPRSignedBaseLayer(NewPRMMatchRepository())
require.NoError(t, err)
// Pass a nil pointer to, kind of, test that the return value does not depend on the image.
res, err := pr.isRunningImageAllowed(nil)
assertRunningRejectedPolicyRequirement(t, res, err)
}

View file

@ -0,0 +1,264 @@
package signature
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/containers/image/directory"
"github.com/containers/image/docker/reference"
"github.com/containers/image/image"
"github.com/containers/image/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// dirImageMock returns a types.UnparsedImage for a directory, claiming a specified dockerReference.
// The caller must call .Close() on the returned UnparsedImage.
func dirImageMock(t *testing.T, dir, dockerReference string) types.UnparsedImage {
ref, err := reference.ParseNamed(dockerReference)
require.NoError(t, err)
return dirImageMockWithRef(t, dir, refImageReferenceMock{ref})
}
// dirImageMockWithRef returns a types.UnparsedImage for a directory, claiming a specified ref.
// The caller must call .Close() on the returned UnparsedImage.
func dirImageMockWithRef(t *testing.T, dir string, ref types.ImageReference) types.UnparsedImage {
srcRef, err := directory.NewReference(dir)
require.NoError(t, err)
src, err := srcRef.NewImageSource(nil, nil)
require.NoError(t, err)
return image.UnparsedFromSource(&dirImageSourceMock{
ImageSource: src,
ref: ref,
})
}
// dirImageSourceMock inherits dirImageSource, but overrides its Reference method.
type dirImageSourceMock struct {
types.ImageSource
ref types.ImageReference
}
func (d *dirImageSourceMock) Reference() types.ImageReference {
return d.ref
}
func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) {
ktGPG := SBKeyTypeGPGKeys
prm := NewPRMMatchExact()
testImage := dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest")
defer testImage.Close()
testImageSig, err := ioutil.ReadFile("fixtures/dir-img-valid/signature-1")
require.NoError(t, err)
// Successful validation, with KeyData and KeyPath
pr, err := NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
sar, parsedSig, err := pr.isSignatureAuthorAccepted(testImage, testImageSig)
assertSARAccepted(t, sar, parsedSig, err, Signature{
DockerManifestDigest: TestImageManifestDigest,
DockerReference: "testing/manifest:latest",
})
keyData, err := ioutil.ReadFile("fixtures/public-key.gpg")
require.NoError(t, err)
pr, err = NewPRSignedByKeyData(ktGPG, keyData, prm)
require.NoError(t, err)
sar, parsedSig, err = pr.isSignatureAuthorAccepted(testImage, testImageSig)
assertSARAccepted(t, sar, parsedSig, err, Signature{
DockerManifestDigest: TestImageManifestDigest,
DockerReference: "testing/manifest:latest",
})
// Unimplemented and invalid KeyType values
for _, keyType := range []sbKeyType{SBKeyTypeSignedByGPGKeys,
SBKeyTypeX509Certificates,
SBKeyTypeSignedByX509CAs,
sbKeyType("This is invalid"),
} {
// Do not use NewPRSignedByKeyData, because it would reject invalid values.
pr := &prSignedBy{
KeyType: keyType,
KeyData: []byte("abc"),
SignedIdentity: prm,
}
// Pass nil pointers to, kind of, test that the return value does not depend on the parameters.
sar, parsedSig, err := pr.isSignatureAuthorAccepted(nil, nil)
assertSARRejected(t, sar, parsedSig, err)
}
// Both KeyPath and KeyData set. Do not use NewPRSignedBy*, because it would reject this.
prSB := &prSignedBy{
KeyType: ktGPG,
KeyPath: "/foo/bar",
KeyData: []byte("abc"),
SignedIdentity: prm,
}
// Pass nil pointers to, kind of, test that the return value does not depend on the parameters.
sar, parsedSig, err = prSB.isSignatureAuthorAccepted(nil, nil)
assertSARRejected(t, sar, parsedSig, err)
// Invalid KeyPath
pr, err = NewPRSignedByKeyPath(ktGPG, "/this/does/not/exist", prm)
require.NoError(t, err)
// Pass nil pointers to, kind of, test that the return value does not depend on the parameters.
sar, parsedSig, err = pr.isSignatureAuthorAccepted(nil, nil)
assertSARRejected(t, sar, parsedSig, err)
// Errors initializing the temporary GPG directory and mechanism are not obviously easy to reach.
// KeyData has no public keys.
pr, err = NewPRSignedByKeyData(ktGPG, []byte{}, prm)
require.NoError(t, err)
// Pass nil pointers to, kind of, test that the return value does not depend on the parameters.
sar, parsedSig, err = pr.isSignatureAuthorAccepted(nil, nil)
assertSARRejectedPolicyRequirement(t, sar, parsedSig, err)
// A signature which does not GPG verify
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
// Pass a nil pointer to, kind of, test that the return value does not depend on the image parmater..
sar, parsedSig, err = pr.isSignatureAuthorAccepted(nil, []byte("invalid signature"))
assertSARRejected(t, sar, parsedSig, err)
// A valid signature using an unknown key.
// (This is (currently?) rejected through the "mech.Verify fails" path, not the "!identityFound" path,
// because we use a temporary directory and only import the trusted keys.)
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
sig, err := ioutil.ReadFile("fixtures/unknown-key.signature")
require.NoError(t, err)
// Pass a nil pointer to, kind of, test that the return value does not depend on the image parmater..
sar, parsedSig, err = pr.isSignatureAuthorAccepted(nil, sig)
assertSARRejected(t, sar, parsedSig, err)
// A valid signature of an invalid JSON.
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
sig, err = ioutil.ReadFile("fixtures/invalid-blob.signature")
require.NoError(t, err)
// Pass a nil pointer to, kind of, test that the return value does not depend on the image parmater..
sar, parsedSig, err = pr.isSignatureAuthorAccepted(nil, sig)
assertSARRejected(t, sar, parsedSig, err)
assert.IsType(t, InvalidSignatureError{}, err)
// A valid signature with a rejected identity.
nonmatchingPRM, err := NewPRMExactReference("this/doesnt:match")
require.NoError(t, err)
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", nonmatchingPRM)
require.NoError(t, err)
sar, parsedSig, err = pr.isSignatureAuthorAccepted(testImage, testImageSig)
assertSARRejectedPolicyRequirement(t, sar, parsedSig, err)
// Error reading image manifest
image := dirImageMock(t, "fixtures/dir-img-no-manifest", "testing/manifest:latest")
defer image.Close()
sig, err = ioutil.ReadFile("fixtures/dir-img-no-manifest/signature-1")
require.NoError(t, err)
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
sar, parsedSig, err = pr.isSignatureAuthorAccepted(image, sig)
assertSARRejected(t, sar, parsedSig, err)
// Error computing manifest digest
image = dirImageMock(t, "fixtures/dir-img-manifest-digest-error", "testing/manifest:latest")
defer image.Close()
sig, err = ioutil.ReadFile("fixtures/dir-img-manifest-digest-error/signature-1")
require.NoError(t, err)
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
sar, parsedSig, err = pr.isSignatureAuthorAccepted(image, sig)
assertSARRejected(t, sar, parsedSig, err)
// A valid signature with a non-matching manifest
image = dirImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest")
defer image.Close()
sig, err = ioutil.ReadFile("fixtures/dir-img-modified-manifest/signature-1")
require.NoError(t, err)
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
sar, parsedSig, err = pr.isSignatureAuthorAccepted(image, sig)
assertSARRejectedPolicyRequirement(t, sar, parsedSig, err)
}
// createInvalidSigDir creates a directory suitable for dirImageMock, in which image.Signatures()
// fails.
// The caller should eventually call os.RemoveAll on the returned path.
func createInvalidSigDir(t *testing.T) string {
dir, err := ioutil.TempDir("", "skopeo-test-unreadable-signature")
require.NoError(t, err)
err = ioutil.WriteFile(path.Join(dir, "manifest.json"), []byte("{}"), 0644)
require.NoError(t, err)
// Creating a 000-permissions file would work for unprivileged accounts, but root (in particular,
// in the Docker container we use for testing) would still have access. So, create a symlink
// pointing to itself, to cause an ELOOP. (Note that a symlink pointing to a nonexistent file would be treated
// just like a nonexistent signature file, and not an error.)
err = os.Symlink("signature-1", path.Join(dir, "signature-1"))
require.NoError(t, err)
return dir
}
func TestPRSignedByIsRunningImageAllowed(t *testing.T) {
ktGPG := SBKeyTypeGPGKeys
prm := NewPRMMatchExact()
// A simple success case: single valid signature.
image := dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest")
defer image.Close()
pr, err := NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
allowed, err := pr.isRunningImageAllowed(image)
assertRunningAllowed(t, allowed, err)
// Error reading signatures
invalidSigDir := createInvalidSigDir(t)
defer os.RemoveAll(invalidSigDir)
image = dirImageMock(t, invalidSigDir, "testing/manifest:latest")
defer image.Close()
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
allowed, err = pr.isRunningImageAllowed(image)
assertRunningRejected(t, allowed, err)
// No signatures
image = dirImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest")
defer image.Close()
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
allowed, err = pr.isRunningImageAllowed(image)
assertRunningRejectedPolicyRequirement(t, allowed, err)
// 1 invalid signature: use dir-img-valid, but a non-matching Docker reference
image = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:notlatest")
defer image.Close()
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
allowed, err = pr.isRunningImageAllowed(image)
assertRunningRejectedPolicyRequirement(t, allowed, err)
// 2 valid signatures
image = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest")
defer image.Close()
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
allowed, err = pr.isRunningImageAllowed(image)
assertRunningAllowed(t, allowed, err)
// One invalid, one valid signature (in this order)
image = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest")
defer image.Close()
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
allowed, err = pr.isRunningImageAllowed(image)
assertRunningAllowed(t, allowed, err)
// 2 invalid signatures: use dir-img-valid-2, but a non-matching Docker reference
image = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:notlatest")
defer image.Close()
pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
require.NoError(t, err)
allowed, err = pr.isRunningImageAllowed(image)
assertRunningRejectedPolicyRequirement(t, allowed, err)
}

View file

@ -0,0 +1,74 @@
package signature
import (
"testing"
"github.com/containers/image/docker/reference"
"github.com/containers/image/types"
)
// nameOnlyImageMock is a mock of types.UnparsedImage which only allows transports.ImageName to work
type nameOnlyImageMock struct {
forbiddenImageMock
}
func (nameOnlyImageMock) Reference() types.ImageReference {
return nameOnlyImageReferenceMock("== StringWithinTransport mock")
}
// nameOnlyImageReferenceMock is a mock of types.ImageReference which only allows transports.ImageName to work, returning self.
type nameOnlyImageReferenceMock string
func (ref nameOnlyImageReferenceMock) Transport() types.ImageTransport {
return nameImageTransportMock("== Transport mock")
}
func (ref nameOnlyImageReferenceMock) StringWithinTransport() string {
return string(ref)
}
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(ctx *types.SystemContext) (types.Image, error) {
panic("unexpected call to a mock function")
}
func (ref nameOnlyImageReferenceMock) NewImageSource(ctx *types.SystemContext, requestedManifestMIMETypes []string) (types.ImageSource, error) {
panic("unexpected call to a mock function")
}
func (ref nameOnlyImageReferenceMock) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
panic("unexpected call to a mock function")
}
func (ref nameOnlyImageReferenceMock) DeleteImage(ctx *types.SystemContext) error {
panic("unexpected call to a mock function")
}
func TestPRInsecureAcceptAnythingIsSignatureAuthorAccepted(t *testing.T) {
pr := NewPRInsecureAcceptAnything()
// Pass nil signature to, kind of, test that the return value does not depend on it.
sar, parsedSig, err := pr.isSignatureAuthorAccepted(nameOnlyImageMock{}, nil)
assertSARUnknown(t, sar, parsedSig, err)
}
func TestPRInsecureAcceptAnythingIsRunningImageAllowed(t *testing.T) {
pr := NewPRInsecureAcceptAnything()
res, err := pr.isRunningImageAllowed(nameOnlyImageMock{})
assertRunningAllowed(t, res, err)
}
func TestPRRejectIsSignatureAuthorAccepted(t *testing.T) {
pr := NewPRReject()
// Pass nil signature to, kind of, test that the return value does not depend on it.
sar, parsedSig, err := pr.isSignatureAuthorAccepted(nameOnlyImageMock{}, nil)
assertSARRejectedPolicyRequirement(t, sar, parsedSig, err)
}
func TestPRRejectIsRunningImageAllowed(t *testing.T) {
pr := NewPRReject()
res, err := pr.isRunningImageAllowed(nameOnlyImageMock{})
assertRunningRejectedPolicyRequirement(t, res, err)
}

View file

@ -0,0 +1,488 @@
package signature
import (
"fmt"
"os"
"testing"
"github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/docker/reference"
"github.com/containers/image/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPolicyRequirementError(t *testing.T) {
// A stupid test just to keep code coverage
s := "test"
err := PolicyRequirementError(s)
assert.Equal(t, s, err.Error())
}
func TestPolicyContextChangeState(t *testing.T) {
pc, err := NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}})
require.NoError(t, err)
defer pc.Destroy()
require.Equal(t, pcReady, pc.state)
err = pc.changeState(pcReady, pcInUse)
require.NoError(t, err)
err = pc.changeState(pcReady, pcInUse)
require.Error(t, err)
// Return state to pcReady to allow pc.Destroy to clean up.
err = pc.changeState(pcInUse, pcReady)
require.NoError(t, err)
}
func TestPolicyContextNewDestroy(t *testing.T) {
pc, err := NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}})
require.NoError(t, err)
assert.Equal(t, pcReady, pc.state)
err = pc.Destroy()
require.NoError(t, err)
assert.Equal(t, pcDestroyed, pc.state)
// Trying to destroy when not pcReady
pc, err = NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}})
require.NoError(t, err)
err = pc.changeState(pcReady, pcInUse)
require.NoError(t, err)
err = pc.Destroy()
require.Error(t, err)
assert.Equal(t, pcInUse, pc.state) // The state, and hopefully nothing else, has changed.
err = pc.changeState(pcInUse, pcReady)
require.NoError(t, err)
err = pc.Destroy()
assert.NoError(t, err)
}
// pcImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference
// and handles PolicyConfigurationIdentity and PolicyConfigurationReference consistently.
type pcImageReferenceMock struct {
transportName string
ref reference.Named
}
func (ref pcImageReferenceMock) Transport() types.ImageTransport {
return nameImageTransportMock(ref.transportName)
}
func (ref pcImageReferenceMock) StringWithinTransport() string {
// We use this in error messages, so sadly we must return something.
return "== StringWithinTransport mock"
}
func (ref pcImageReferenceMock) DockerReference() reference.Named {
return ref.ref
}
func (ref pcImageReferenceMock) PolicyConfigurationIdentity() string {
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(ctx *types.SystemContext) (types.Image, error) {
panic("unexpected call to a mock function")
}
func (ref pcImageReferenceMock) NewImageSource(ctx *types.SystemContext, requestedManifestMIMETypes []string) (types.ImageSource, error) {
panic("unexpected call to a mock function")
}
func (ref pcImageReferenceMock) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
panic("unexpected call to a mock function")
}
func (ref pcImageReferenceMock) DeleteImage(ctx *types.SystemContext) error {
panic("unexpected call to a mock function")
}
func TestPolicyContextRequirementsForImageRef(t *testing.T) {
ktGPG := SBKeyTypeGPGKeys
prm := NewPRMMatchRepoDigestOrExact()
policy := &Policy{
Default: PolicyRequirements{NewPRReject()},
Transports: map[string]PolicyTransportScopes{},
}
// Just put _something_ into the PolicyTransportScopes map for the keys we care about, and make it pairwise
// distinct so that we can compare the values and show them when debugging the tests.
for _, t := range []struct{ transport, scope string }{
{"docker", ""},
{"docker", "unmatched"},
{"docker", "deep.com"},
{"docker", "deep.com/n1"},
{"docker", "deep.com/n1/n2"},
{"docker", "deep.com/n1/n2/n3"},
{"docker", "deep.com/n1/n2/n3/repo"},
{"docker", "deep.com/n1/n2/n3/repo:tag2"},
{"atomic", "unmatched"},
} {
if _, ok := policy.Transports[t.transport]; !ok {
policy.Transports[t.transport] = PolicyTransportScopes{}
}
policy.Transports[t.transport][t.scope] = PolicyRequirements{xNewPRSignedByKeyData(ktGPG, []byte(t.transport+t.scope), prm)}
}
pc, err := NewPolicyContext(policy)
require.NoError(t, err)
for _, c := range []struct{ inputTransport, input, matchedTransport, matched string }{
// Full match
{"docker", "deep.com/n1/n2/n3/repo:tag2", "docker", "deep.com/n1/n2/n3/repo:tag2"},
// Namespace matches
{"docker", "deep.com/n1/n2/n3/repo:nottag2", "docker", "deep.com/n1/n2/n3/repo"},
{"docker", "deep.com/n1/n2/n3/notrepo:tag2", "docker", "deep.com/n1/n2/n3"},
{"docker", "deep.com/n1/n2/notn3/repo:tag2", "docker", "deep.com/n1/n2"},
{"docker", "deep.com/n1/notn2/n3/repo:tag2", "docker", "deep.com/n1"},
// Host name match
{"docker", "deep.com/notn1/n2/n3/repo:tag2", "docker", "deep.com"},
// Default
{"docker", "this.doesnt/match:anything", "docker", ""},
// No match within a matched transport which doesn't have a "" scope
{"atomic", "this.doesnt/match:anything", "", ""},
// No configuration available for this transport at all
{"dir", "what/ever", "", ""}, // "what/ever" is not a valid scope for the real "dir" transport, but we only need it to be a valid reference.Named.
} {
var expected PolicyRequirements
if c.matchedTransport != "" {
e, ok := policy.Transports[c.matchedTransport][c.matched]
require.True(t, ok, fmt.Sprintf("case %s:%s: expected reqs not found", c.inputTransport, c.input))
expected = e
} else {
expected = policy.Default
}
ref, err := reference.ParseNamed(c.input)
require.NoError(t, err)
reqs := pc.requirementsForImageRef(pcImageReferenceMock{c.inputTransport, ref})
comment := fmt.Sprintf("case %s:%s: %#v", c.inputTransport, c.input, reqs[0])
// 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)
assert.True(t, len(reqs) == len(expected), comment)
}
}
// pcImageMock returns a types.UnparsedImage for a directory, claiming a specified dockerReference and implementing PolicyConfigurationIdentity/PolicyConfigurationNamespaces.
// The caller must call .Close() on the returned Image.
func pcImageMock(t *testing.T, dir, dockerReference string) types.UnparsedImage {
ref, err := reference.ParseNamed(dockerReference)
require.NoError(t, err)
return dirImageMockWithRef(t, dir, pcImageReferenceMock{"docker", ref})
}
func TestPolicyContextGetSignaturesWithAcceptedAuthor(t *testing.T) {
expectedSig := &Signature{
DockerManifestDigest: TestImageManifestDigest,
DockerReference: "testing/manifest:latest",
}
pc, err := NewPolicyContext(&Policy{
Default: PolicyRequirements{NewPRReject()},
Transports: map[string]PolicyTransportScopes{
"docker": {
"docker.io/testing/manifest:latest": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchExact()),
},
"docker.io/testing/manifest:twoAccepts": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
},
"docker.io/testing/manifest:acceptReject": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
NewPRReject(),
},
"docker.io/testing/manifest:acceptUnknown": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
xNewPRSignedBaseLayer(NewPRMMatchRepository()),
},
"docker.io/testing/manifest:rejectUnknown": {
NewPRReject(),
xNewPRSignedBaseLayer(NewPRMMatchRepository()),
},
"docker.io/testing/manifest:unknown": {
xNewPRSignedBaseLayer(NewPRMMatchRepository()),
},
"docker.io/testing/manifest:unknown2": {
NewPRInsecureAcceptAnything(),
},
"docker.io/testing/manifest:invalidEmptyRequirements": {},
},
},
})
require.NoError(t, err)
defer pc.Destroy()
// Success
img := pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest")
defer img.Close()
sigs, err := pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig}, sigs)
// Two signatures
// FIXME? Use really different signatures for this?
img = pcImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig, expectedSig}, sigs)
// No signatures
img = pcImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Empty(t, sigs)
// Only invalid signatures
img = pcImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Empty(t, sigs)
// 1 invalid, 1 valid signature (in this order)
img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig}, sigs)
// Two sarAccepted results for one signature
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:twoAccepts")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig}, sigs)
// sarAccepted+sarRejected for a signature
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:acceptReject")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Empty(t, sigs)
// sarAccepted+sarUnknown for a signature
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:acceptUnknown")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig}, sigs)
// sarRejected+sarUnknown for a signature
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:rejectUnknown")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Empty(t, sigs)
// sarUnknown only
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:unknown")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Empty(t, sigs)
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:unknown2")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Empty(t, sigs)
// Empty list of requirements (invalid)
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
require.NoError(t, err)
assert.Empty(t, sigs)
// Failures: Make sure we return nil sigs.
// Unexpected state (context already destroyed)
destroyedPC, err := NewPolicyContext(pc.Policy)
require.NoError(t, err)
err = destroyedPC.Destroy()
require.NoError(t, err)
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest")
defer img.Close()
sigs, err = destroyedPC.GetSignaturesWithAcceptedAuthor(img)
assert.Error(t, err)
assert.Nil(t, sigs)
// Not testing the pcInUse->pcReady transition, that would require custom PolicyRequirement
// implementations meddling with the state, or threads. This is for catching trivial programmer
// mistakes only, anyway.
// Error reading signatures.
invalidSigDir := createInvalidSigDir(t)
defer os.RemoveAll(invalidSigDir)
img = pcImageMock(t, invalidSigDir, "testing/manifest:latest")
defer img.Close()
sigs, err = pc.GetSignaturesWithAcceptedAuthor(img)
assert.Error(t, err)
assert.Nil(t, sigs)
}
func TestPolicyContextIsRunningImageAllowed(t *testing.T) {
pc, err := NewPolicyContext(&Policy{
Default: PolicyRequirements{NewPRReject()},
Transports: map[string]PolicyTransportScopes{
"docker": {
"docker.io/testing/manifest:latest": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchExact()),
},
"docker.io/testing/manifest:twoAllows": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
},
"docker.io/testing/manifest:allowDeny": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
NewPRReject(),
},
"docker.io/testing/manifest:reject": {
NewPRReject(),
},
"docker.io/testing/manifest:acceptAnything": {
NewPRInsecureAcceptAnything(),
},
"docker.io/testing/manifest:invalidEmptyRequirements": {},
},
},
})
require.NoError(t, err)
defer pc.Destroy()
// Success
img := pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest")
defer img.Close()
res, err := pc.IsRunningImageAllowed(img)
assertRunningAllowed(t, res, err)
// Two signatures
// FIXME? Use really different signatures for this?
img = pcImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest")
defer img.Close()
res, err = pc.IsRunningImageAllowed(img)
assertRunningAllowed(t, res, err)
// No signatures
img = pcImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest")
defer img.Close()
res, err = pc.IsRunningImageAllowed(img)
assertRunningRejectedPolicyRequirement(t, res, err)
// Only invalid signatures
img = pcImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest")
defer img.Close()
res, err = pc.IsRunningImageAllowed(img)
assertRunningRejectedPolicyRequirement(t, res, err)
// 1 invalid, 1 valid signature (in this order)
img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest")
defer img.Close()
res, err = pc.IsRunningImageAllowed(img)
assertRunningAllowed(t, res, err)
// Two allowed results
img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:twoAllows")
defer img.Close()
res, err = pc.IsRunningImageAllowed(img)
assertRunningAllowed(t, res, err)
// Allow + deny results
img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:allowDeny")
defer img.Close()
res, err = pc.IsRunningImageAllowed(img)
assertRunningRejectedPolicyRequirement(t, res, err)
// prReject works
img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:reject")
defer img.Close()
res, err = pc.IsRunningImageAllowed(img)
assertRunningRejectedPolicyRequirement(t, res, err)
// prInsecureAcceptAnything works
img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:acceptAnything")
defer img.Close()
res, err = pc.IsRunningImageAllowed(img)
assertRunningAllowed(t, res, err)
// Empty list of requirements (invalid)
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements")
defer img.Close()
res, err = pc.IsRunningImageAllowed(img)
assertRunningRejectedPolicyRequirement(t, res, err)
// Unexpected state (context already destroyed)
destroyedPC, err := NewPolicyContext(pc.Policy)
require.NoError(t, err)
err = destroyedPC.Destroy()
require.NoError(t, err)
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest")
defer img.Close()
res, err = destroyedPC.IsRunningImageAllowed(img)
assertRunningRejected(t, res, err)
// Not testing the pcInUse->pcReady transition, that would require custom PolicyRequirement
// implementations meddling with the state, or threads. This is for catching trivial programmer
// mistakes only, anyway.
}
// Helpers for validating PolicyRequirement.isSignatureAuthorAccepted results:
// assertSARRejected verifies that isSignatureAuthorAccepted returns a consistent sarRejected result
// with the expected signature.
func assertSARAccepted(t *testing.T, sar signatureAcceptanceResult, parsedSig *Signature, err error, expectedSig Signature) {
assert.Equal(t, sarAccepted, sar)
assert.Equal(t, &expectedSig, parsedSig)
assert.NoError(t, err)
}
// assertSARRejected verifies that isSignatureAuthorAccepted returns a consistent sarRejected result.
func assertSARRejected(t *testing.T, sar signatureAcceptanceResult, parsedSig *Signature, err error) {
assert.Equal(t, sarRejected, sar)
assert.Nil(t, parsedSig)
assert.Error(t, err)
}
// assertSARRejectedPolicyRequiremnt verifies that isSignatureAuthorAccepted returns a consistent sarRejected resul,
// and that the returned error is a PolicyRequirementError..
func assertSARRejectedPolicyRequirement(t *testing.T, sar signatureAcceptanceResult, parsedSig *Signature, err error) {
assertSARRejected(t, sar, parsedSig, err)
assert.IsType(t, PolicyRequirementError(""), err)
}
// assertSARRejected verifies that isSignatureAuthorAccepted returns a consistent sarUnknown result.
func assertSARUnknown(t *testing.T, sar signatureAcceptanceResult, parsedSig *Signature, err error) {
assert.Equal(t, sarUnknown, sar)
assert.Nil(t, parsedSig)
assert.NoError(t, err)
}
// Helpers for validating PolicyRequirement.isRunningImageAllowed results:
// assertRunningAllowed verifies that isRunningImageAllowed returns a consistent true result
func assertRunningAllowed(t *testing.T, allowed bool, err error) {
assert.Equal(t, true, allowed)
assert.NoError(t, err)
}
// assertRunningRejected verifies that isRunningImageAllowed returns a consistent false result
func assertRunningRejected(t *testing.T, allowed bool, err error) {
assert.Equal(t, false, allowed)
assert.Error(t, err)
}
// assertRunningRejectedPolicyRequirement verifies that isRunningImageAllowed returns a consistent false result
// and that the returned error is a PolicyRequirementError.
func assertRunningRejectedPolicyRequirement(t *testing.T, allowed bool, err error) {
assertRunningRejected(t, allowed, err)
assert.IsType(t, PolicyRequirementError(""), err)
}

View file

@ -0,0 +1,370 @@
package signature
import (
"fmt"
"testing"
"github.com/containers/image/docker/reference"
"github.com/containers/image/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
fullRHELRef = "registry.access.redhat.com/rhel7/rhel:7.2.3"
untaggedRHELRef = "registry.access.redhat.com/rhel7/rhel"
digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
digestSuffixOther = "@sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
)
func TestParseImageAndDockerReference(t *testing.T) {
const (
ok1 = "busybox"
ok2 = fullRHELRef
bad1 = "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES"
bad2 = ""
)
// Success
ref, err := reference.ParseNamed(ok1)
require.NoError(t, err)
r1, r2, err := parseImageAndDockerReference(refImageMock{ref}, ok2)
require.NoError(t, err)
assert.Equal(t, ok1, r1.String())
assert.Equal(t, ok2, r2.String())
// Unidentified images are rejected.
_, _, err = parseImageAndDockerReference(refImageMock{nil}, ok2)
require.Error(t, err)
assert.IsType(t, PolicyRequirementError(""), err)
// Failures
for _, refs := range [][]string{
{bad1, ok2},
{ok1, bad2},
{bad1, bad2},
} {
ref, err := reference.ParseNamed(refs[0])
if err == nil {
_, _, err := parseImageAndDockerReference(refImageMock{ref}, refs[1])
assert.Error(t, err)
}
}
}
// refImageMock is a mock of types.UnparsedImage which returns itself in Reference().DockerReference.
type refImageMock struct{ reference.Named }
func (ref refImageMock) Reference() types.ImageReference {
return refImageReferenceMock{ref.Named}
}
func (ref refImageMock) Close() {
panic("unexpected call to a mock function")
}
func (ref refImageMock) Manifest() ([]byte, string, error) {
panic("unexpected call to a mock function")
}
func (ref refImageMock) Signatures() ([][]byte, error) {
panic("unexpected call to a mock function")
}
// refImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference.
type refImageReferenceMock struct{ reference.Named }
func (ref refImageReferenceMock) Transport() types.ImageTransport {
// We use this in error messages, so sadly we must return something. But right now we do so only when DockerReference is nil, so restrict to that.
if ref.Named == nil {
return nameImageTransportMock("== Transport mock")
}
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) StringWithinTransport() string {
// We use this in error messages, so sadly we must return something. But right now we do so only when DockerReference is nil, so restrict to that.
if ref.Named == nil {
return "== StringWithinTransport for an image with no Docker support"
}
panic("unexpected call to a mock function")
}
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(ctx *types.SystemContext) (types.Image, error) {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) NewImageSource(ctx *types.SystemContext, requestedManifestMIMETypes []string) (types.ImageSource, error) {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) DeleteImage(ctx *types.SystemContext) error {
panic("unexpected call to a mock function")
}
// nameImageTransportMock is a mock of types.ImageTransport which returns itself in Name.
type nameImageTransportMock string
func (name nameImageTransportMock) Name() string {
return string(name)
}
func (name nameImageTransportMock) ParseReference(reference string) (types.ImageReference, error) {
panic("unexpected call to a mock function")
}
func (name nameImageTransportMock) ValidatePolicyConfigurationScope(scope string) error {
panic("unexpected call to a mock function")
}
type prmSymmetricTableTest struct {
refA, refB string
result bool
}
// Test cases for exact reference match. The behavior is supposed to be symmetric.
var prmExactMatchTestTable = []prmSymmetricTableTest{
// Success, simple matches
{"busybox:latest", "busybox:latest", true},
{fullRHELRef, fullRHELRef, true},
{"busybox" + digestSuffix, "busybox" + digestSuffix, true}, // NOTE: This is not documented; signing digests is not recommended at this time.
// Non-canonical reference format is canonicalized
{"library/busybox:latest", "busybox:latest", true},
{"docker.io/library/busybox:latest", "busybox:latest", true},
{"library/busybox" + digestSuffix, "busybox" + digestSuffix, true},
// Mismatch
{"busybox:latest", "busybox:notlatest", false},
{"busybox:latest", "notbusybox:latest", false},
{"busybox:latest", "hostname/library/busybox:notlatest", false},
{"hostname/library/busybox:latest", "busybox:notlatest", false},
{"busybox:latest", fullRHELRef, false},
{"busybox" + digestSuffix, "notbusybox" + digestSuffix, false},
{"busybox:latest", "busybox" + digestSuffix, false},
{"busybox" + digestSuffix, "busybox" + digestSuffixOther, false},
// NameOnly references
{"busybox", "busybox:latest", false},
{"busybox", "busybox" + digestSuffix, false},
{"busybox", "busybox", false},
// References with both tags and digests: `reference.WithName` essentially drops the tag.
// This is not _particularly_ desirable but it is the semantics used throughout containers/image; at least, with the digest it is clear which image the reference means,
// even if the tag may reflect a different user intent.
// NOTE: Again, this is not documented behavior; the recommendation is to sign tags, not digests, and then tag-and-digest references wont match the signed identity.
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false},
{"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, true}, // Ugly. Do not rely on this.
{"busybox:latest" + digestSuffix, "busybox" + digestSuffix, true}, // Ugly. Do not rely on this.
{"busybox:latest" + digestSuffix, "busybox:latest", false},
// Invalid format
{"UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", "busybox:latest", false},
{"", "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", false},
// Even if they are exactly equal, invalid values are rejected.
{"INVALID", "INVALID", false},
}
// Test cases for repository-only reference match. The behavior is supposed to be symmetric.
var prmRepositoryMatchTestTable = []prmSymmetricTableTest{
// Success, simple matches
{"busybox:latest", "busybox:latest", true},
{fullRHELRef, fullRHELRef, true},
{"busybox" + digestSuffix, "busybox" + digestSuffix, true}, // NOTE: This is not documented; signing digests is not recommended at this time.
// Non-canonical reference format is canonicalized
{"library/busybox:latest", "busybox:latest", true},
{"docker.io/library/busybox:latest", "busybox:latest", true},
{"library/busybox" + digestSuffix, "busybox" + digestSuffix, true},
// The same as above, but with mismatching tags
{"busybox:latest", "busybox:notlatest", true},
{fullRHELRef + "tagsuffix", fullRHELRef, true},
{"library/busybox:latest", "busybox:notlatest", true},
{"busybox:latest", "library/busybox:notlatest", true},
{"docker.io/library/busybox:notlatest", "busybox:latest", true},
{"busybox:notlatest", "docker.io/library/busybox:latest", true},
{"busybox:latest", "busybox" + digestSuffix, true},
{"busybox" + digestSuffix, "busybox" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.)
// The same as above, but with defaulted tags (should not actually happen)
{"busybox", "busybox:notlatest", true},
{fullRHELRef, untaggedRHELRef, true},
{"busybox", "busybox" + digestSuffix, true},
{"library/busybox", "busybox", true},
{"docker.io/library/busybox", "busybox", true},
// Mismatch
{"busybox:latest", "notbusybox:latest", false},
{"hostname/library/busybox:latest", "busybox:notlatest", false},
{"busybox:latest", fullRHELRef, false},
{"busybox" + digestSuffix, "notbusybox" + digestSuffix, false},
// References with both tags and digests: `reference.WithName` essentially drops the tag, and we ignore both anyway.
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, true},
{"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox:latest", true},
// Invalid format
{"UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", "busybox:latest", false},
{"", "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", false},
// Even if they are exactly equal, invalid values are rejected.
{"INVALID", "INVALID", false},
}
func testImageAndSig(t *testing.T, prm PolicyReferenceMatch, imageRef, sigRef string, result bool) {
// This assumes that all ways to obtain a reference.Named perform equivalent validation,
// and therefore values refused by reference.ParseNamed can not happen in practice.
parsedImageRef, err := reference.ParseNamed(imageRef)
if err != nil {
return
}
res := prm.matchesDockerReference(refImageMock{parsedImageRef}, sigRef)
assert.Equal(t, result, res, fmt.Sprintf("%s vs. %s", imageRef, sigRef))
}
func TestPRMMatchExactMatchesDockerReference(t *testing.T) {
prm := NewPRMMatchExact()
for _, test := range prmExactMatchTestTable {
testImageAndSig(t, prm, test.refA, test.refB, test.result)
testImageAndSig(t, prm, test.refB, test.refA, test.result)
}
// Even if they are signed with an empty string as a reference, unidentified images are rejected.
res := prm.matchesDockerReference(refImageMock{nil}, "")
assert.False(t, res, `unidentified vs. ""`)
}
func TestPMMMatchRepoDigestOrExactMatchesDockerReference(t *testing.T) {
prm := NewPRMMatchRepoDigestOrExact()
// prmMatchRepoDigestOrExact is a middle ground between prmMatchExact and prmMatchRepository:
// It accepts anything prmMatchExact accepts,…
for _, test := range prmExactMatchTestTable {
if test.result == true {
testImageAndSig(t, prm, test.refA, test.refB, test.result)
testImageAndSig(t, prm, test.refB, test.refA, test.result)
}
}
// … and it rejects everything prmMatchRepository rejects.
for _, test := range prmRepositoryMatchTestTable {
if test.result == false {
testImageAndSig(t, prm, test.refA, test.refB, test.result)
testImageAndSig(t, prm, test.refB, test.refA, test.result)
}
}
// The other cases, possibly assymetrical:
for _, test := range []struct {
imageRef, sigRef string
result bool
}{
// Tag mismatch
{"busybox:latest", "busybox:notlatest", false},
{fullRHELRef + "tagsuffix", fullRHELRef, false},
{"library/busybox:latest", "busybox:notlatest", false},
{"busybox:latest", "library/busybox:notlatest", false},
{"docker.io/library/busybox:notlatest", "busybox:latest", false},
{"busybox:notlatest", "docker.io/library/busybox:latest", false},
// NameOnly references
{"busybox", "busybox:latest", false},
{"busybox:latest", "busybox", false},
{"busybox", "busybox" + digestSuffix, false},
{"busybox" + digestSuffix, "busybox", false},
{fullRHELRef, untaggedRHELRef, false},
{"busybox", "busybox", false},
// Tag references only accept signatures with matching tags.
{"busybox:latest", "busybox" + digestSuffix, false},
// Digest references accept any signature with matching repository.
{"busybox" + digestSuffix, "busybox:latest", true},
{"busybox" + digestSuffix, "busybox" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.)
// References with both tags and digests: `reference.WithName` essentially drops the tag.
// This is not _particularly_ desirable but it is the semantics used throughout containers/image; at least, with the digest it is clear which image the reference means,
// even if the tag may reflect a different user intent.
{"busybox:latest" + digestSuffix, "busybox:latest", true},
{"busybox:latest" + digestSuffix, "busybox:notlatest", true},
{"busybox:latest", "busybox:latest" + digestSuffix, false},
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.)
{"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffixOther, true}, // Ugly. Do not rely on this.
} {
testImageAndSig(t, prm, test.imageRef, test.sigRef, test.result)
}
}
func TestPRMMatchRepositoryMatchesDockerReference(t *testing.T) {
prm := NewPRMMatchRepository()
for _, test := range prmRepositoryMatchTestTable {
testImageAndSig(t, prm, test.refA, test.refB, test.result)
testImageAndSig(t, prm, test.refB, test.refA, test.result)
}
// Even if they are signed with an empty string as a reference, unidentified images are rejected.
res := prm.matchesDockerReference(refImageMock{nil}, "")
assert.False(t, res, `unidentified vs. ""`)
}
func TestParseDockerReferences(t *testing.T) {
const (
ok1 = "busybox"
ok2 = fullRHELRef
bad1 = "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES"
bad2 = ""
)
// Success
r1, r2, err := parseDockerReferences(ok1, ok2)
require.NoError(t, err)
assert.Equal(t, ok1, r1.String())
assert.Equal(t, ok2, r2.String())
// Failures
for _, refs := range [][]string{
{bad1, ok2},
{ok1, bad2},
{bad1, bad2},
} {
_, _, err := parseDockerReferences(refs[0], refs[1])
assert.Error(t, err)
}
}
// forbiddenImageMock is a mock of types.UnparsedImage which ensures Reference is not called
type forbiddenImageMock struct{}
func (ref forbiddenImageMock) Reference() types.ImageReference {
panic("unexpected call to a mock function")
}
func (ref forbiddenImageMock) Close() {
panic("unexpected call to a mock function")
}
func (ref forbiddenImageMock) Manifest() ([]byte, string, error) {
panic("unexpected call to a mock function")
}
func (ref forbiddenImageMock) Signatures() ([][]byte, error) {
panic("unexpected call to a mock function")
}
func testExactPRMAndSig(t *testing.T, prmFactory func(string) PolicyReferenceMatch, imageRef, sigRef string, result bool) {
prm := prmFactory(imageRef)
res := prm.matchesDockerReference(forbiddenImageMock{}, sigRef)
assert.Equal(t, result, res, fmt.Sprintf("%s vs. %s", imageRef, sigRef))
}
func prmExactReferenceFactory(ref string) PolicyReferenceMatch {
// Do not use NewPRMExactReference, we want to also test the case with an invalid DockerReference,
// even though NewPRMExactReference should never let it happen.
return &prmExactReference{DockerReference: ref}
}
func TestPRMExactReferenceMatchesDockerReference(t *testing.T) {
for _, test := range prmExactMatchTestTable {
testExactPRMAndSig(t, prmExactReferenceFactory, test.refA, test.refB, test.result)
testExactPRMAndSig(t, prmExactReferenceFactory, test.refB, test.refA, test.result)
}
}
func prmExactRepositoryFactory(ref string) PolicyReferenceMatch {
// Do not use NewPRMExactRepository, we want to also test the case with an invalid DockerReference,
// even though NewPRMExactRepository should never let it happen.
return &prmExactRepository{DockerRepository: ref}
}
func TestPRMExactRepositoryMatchesDockerReference(t *testing.T) {
for _, test := range prmRepositoryMatchTestTable {
testExactPRMAndSig(t, prmExactRepositoryFactory, test.refA, test.refB, test.result)
testExactPRMAndSig(t, prmExactRepositoryFactory, test.refB, test.refA, test.result)
}
}

View file

@ -27,37 +27,74 @@ func (err InvalidSignatureError) Error() string {
}
// Signature is a parsed content of a signature.
// The only way to get this structure from a blob should be as a return value from a successful call to verifyAndExtractSignature below.
type Signature struct {
DockerManifestDigest digest.Digest
DockerReference string // FIXME: more precise type?
}
// Wrap signature to add to it some methods which we don't want to make public.
type privateSignature struct {
Signature
// untrustedSignature is a parsed content of a signature.
type untrustedSignature struct {
UntrustedDockerManifestDigest digest.Digest
UntrustedDockerReference string // FIXME: more precise type?
UntrustedCreatorID *string
// This is intentionally an int64; the native JSON float64 type would allow to represent _some_ sub-second precision,
// but not nearly enough (with current timestamp values, a single unit in the last place is on the order of hundreds of nanoseconds).
// So, this is explicitly an int64, and we reject fractional values. If we did need more precise timestamps eventually,
// we would add another field, UntrustedTimestampNS int64.
UntrustedTimestamp *int64
}
// Compile-time check that privateSignature implements json.Marshaler
var _ json.Marshaler = (*privateSignature)(nil)
// UntrustedSignatureInformation is information available in an untrusted signature.
// This may be useful when debugging signature verification failures,
// or when managing a set of signatures on a single image.
//
// WARNING: Do not use the contents of this for ANY security decisions,
// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable.
// There is NO REASON to expect the values to be correct, or not intentionally misleading
// (including things like “✅ Verified by $authority”)
type UntrustedSignatureInformation struct {
UntrustedDockerManifestDigest digest.Digest
UntrustedDockerReference string // FIXME: more precise type?
UntrustedCreatorID *string
UntrustedTimestamp *time.Time
UntrustedShortKeyIdentifier string
}
// newUntrustedSignature returns an untrustedSignature object with
// the specified primary contents and appropriate metadata.
func newUntrustedSignature(dockerManifestDigest digest.Digest, dockerReference string) untrustedSignature {
// Use intermediate variables for these values so that we can take their addresses.
// Golang guarantees that they will have a new address on every execution.
creatorID := "atomic " + version.Version
timestamp := time.Now().Unix()
return untrustedSignature{
UntrustedDockerManifestDigest: dockerManifestDigest,
UntrustedDockerReference: dockerReference,
UntrustedCreatorID: &creatorID,
UntrustedTimestamp: &timestamp,
}
}
// Compile-time check that untrustedSignature implements json.Marshaler
var _ json.Marshaler = (*untrustedSignature)(nil)
// MarshalJSON implements the json.Marshaler interface.
func (s privateSignature) MarshalJSON() ([]byte, error) {
return s.marshalJSONWithVariables(time.Now().UTC().Unix(), "atomic "+version.Version)
}
// Implementation of MarshalJSON, with a caller-chosen values of the variable items to help testing.
func (s privateSignature) marshalJSONWithVariables(timestamp int64, creatorID string) ([]byte, error) {
if s.DockerManifestDigest == "" || s.DockerReference == "" {
func (s untrustedSignature) MarshalJSON() ([]byte, error) {
if s.UntrustedDockerManifestDigest == "" || s.UntrustedDockerReference == "" {
return nil, errors.New("Unexpected empty signature content")
}
critical := map[string]interface{}{
"type": signatureType,
"image": map[string]string{"docker-manifest-digest": s.DockerManifestDigest.String()},
"identity": map[string]string{"docker-reference": s.DockerReference},
"image": map[string]string{"docker-manifest-digest": s.UntrustedDockerManifestDigest.String()},
"identity": map[string]string{"docker-reference": s.UntrustedDockerReference},
}
optional := map[string]interface{}{
"creator": creatorID,
"timestamp": timestamp,
optional := map[string]interface{}{}
if s.UntrustedCreatorID != nil {
optional["creator"] = *s.UntrustedCreatorID
}
if s.UntrustedTimestamp != nil {
optional["timestamp"] = *s.UntrustedTimestamp
}
signature := map[string]interface{}{
"critical": critical,
@ -66,11 +103,11 @@ func (s privateSignature) marshalJSONWithVariables(timestamp int64, creatorID st
return json.Marshal(signature)
}
// Compile-time check that privateSignature implements json.Unmarshaler
var _ json.Unmarshaler = (*privateSignature)(nil)
// Compile-time check that untrustedSignature implements json.Unmarshaler
var _ json.Unmarshaler = (*untrustedSignature)(nil)
// UnmarshalJSON implements the json.Unmarshaler interface
func (s *privateSignature) UnmarshalJSON(data []byte) error {
func (s *untrustedSignature) UnmarshalJSON(data []byte) error {
err := s.strictUnmarshalJSON(data)
if err != nil {
if _, ok := err.(jsonFormatError); ok {
@ -82,7 +119,7 @@ func (s *privateSignature) UnmarshalJSON(data []byte) error {
// strictUnmarshalJSON is UnmarshalJSON, except that it may return the internal jsonFormatError error type.
// Splitting it into a separate function allows us to do the jsonFormatError → InvalidSignatureError in a single place, the caller.
func (s *privateSignature) strictUnmarshalJSON(data []byte) error {
func (s *untrustedSignature) strictUnmarshalJSON(data []byte) error {
var untyped interface{}
if err := json.Unmarshal(data, &untyped); err != nil {
return err
@ -107,7 +144,20 @@ func (s *privateSignature) strictUnmarshalJSON(data []byte) error {
if err != nil {
return err
}
_ = optional // We don't use anything from here for now.
if _, ok := optional["creator"]; ok {
creatorID, err := stringField(optional, "creator")
if err != nil {
return err
}
s.UntrustedCreatorID = &creatorID
}
if _, ok := optional["timestamp"]; ok {
timestamp, err := int64Field(optional, "timestamp")
if err != nil {
return err
}
s.UntrustedTimestamp = &timestamp
}
t, err := stringField(c, "type")
if err != nil {
@ -128,7 +178,7 @@ func (s *privateSignature) strictUnmarshalJSON(data []byte) error {
if err != nil {
return err
}
s.DockerManifestDigest = digest.Digest(digestString)
s.UntrustedDockerManifestDigest = digest.Digest(digestString)
identity, err := mapField(c, "identity")
if err != nil {
@ -141,13 +191,18 @@ func (s *privateSignature) strictUnmarshalJSON(data []byte) error {
if err != nil {
return err
}
s.DockerReference = reference
s.UntrustedDockerReference = reference
return nil
}
// Sign formats the signature and returns a blob signed using mech and keyIdentity
func (s privateSignature) sign(mech SigningMechanism, keyIdentity string) ([]byte, error) {
// (If it seems surprising that this is a method on untrustedSignature, note that there
// isnt a good reason to think that a key used by the user is trusted by any component
// of the system just because it is a private key — actually the presence of a private key
// on the system increases the likelihood of an a successful attack on that private key
// on that particular system.)
func (s untrustedSignature) sign(mech SigningMechanism, keyIdentity string) ([]byte, error) {
json, err := json.Marshal(s)
if err != nil {
return nil, err
@ -159,7 +214,7 @@ func (s privateSignature) sign(mech SigningMechanism, keyIdentity string) ([]byt
// signatureAcceptanceRules specifies how to decide whether an untrusted signature is acceptable.
// We centralize the actual parsing and data extraction in verifyAndExtractSignature; this supplies
// the policy. We use an object instead of supplying func parameters to verifyAndExtractSignature
// because all of the functions have the same type, so there is a risk of exchanging the functions;
// because the functions have the same or similar types, so there is a risk of exchanging the functions;
// named members of this struct are more explicit.
type signatureAcceptanceRules struct {
validateKeyIdentity func(string) error
@ -178,16 +233,58 @@ func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte
return nil, err
}
var unmatchedSignature privateSignature
var unmatchedSignature untrustedSignature
if err := json.Unmarshal(signed, &unmatchedSignature); err != nil {
return nil, InvalidSignatureError{msg: err.Error()}
}
if err := rules.validateSignedDockerManifestDigest(unmatchedSignature.DockerManifestDigest); err != nil {
if err := rules.validateSignedDockerManifestDigest(unmatchedSignature.UntrustedDockerManifestDigest); err != nil {
return nil, err
}
if err := rules.validateSignedDockerReference(unmatchedSignature.DockerReference); err != nil {
if err := rules.validateSignedDockerReference(unmatchedSignature.UntrustedDockerReference); err != nil {
return nil, err
}
signature := unmatchedSignature.Signature // Policy OK.
return &signature, nil
// signatureAcceptanceRules have accepted this value.
return &Signature{
DockerManifestDigest: unmatchedSignature.UntrustedDockerManifestDigest,
DockerReference: unmatchedSignature.UntrustedDockerReference,
}, nil
}
// GetUntrustedSignatureInformationWithoutVerifying extracts information available in an untrusted signature,
// WITHOUT doing any cryptographic verification.
// This may be useful when debugging signature verification failures,
// or when managing a set of signatures on a single image.
//
// WARNING: Do not use the contents of this for ANY security decisions,
// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable.
// There is NO REASON to expect the values to be correct, or not intentionally misleading
// (including things like “✅ Verified by $authority”)
func GetUntrustedSignatureInformationWithoutVerifying(untrustedSignatureBytes []byte) (*UntrustedSignatureInformation, error) {
// NOTE: This should eventualy do format autodetection.
mech, err := NewGPGSigningMechanism()
if err != nil {
return nil, err
}
untrustedContents, shortKeyIdentifier, err := mech.UntrustedSignatureContents(untrustedSignatureBytes)
if err != nil {
return nil, err
}
var untrustedDecodedContents untrustedSignature
if err := json.Unmarshal(untrustedContents, &untrustedDecodedContents); err != nil {
return nil, InvalidSignatureError{msg: err.Error()}
}
var timestamp *time.Time // = nil
if untrustedDecodedContents.UntrustedTimestamp != nil {
ts := time.Unix(*untrustedDecodedContents.UntrustedTimestamp, 0)
timestamp = &ts
}
return &UntrustedSignatureInformation{
UntrustedDockerManifestDigest: untrustedDecodedContents.UntrustedDockerManifestDigest,
UntrustedDockerReference: untrustedDecodedContents.UntrustedDockerReference,
UntrustedCreatorID: untrustedDecodedContents.UntrustedCreatorID,
UntrustedTimestamp: timestamp,
UntrustedShortKeyIdentifier: shortKeyIdentifier,
}, nil
}

Some files were not shown because too many files have changed in this diff Show more