Skip to content
This repository was archived by the owner on Feb 6, 2024. It is now read-only.

docker/schema2: support manifest lists #4

Merged
merged 6 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion v2/clair/clair.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package clair

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -142,7 +143,11 @@ func getClairVulnerabilities(manifest distribution.Manifest, config Config, toke

switch {
case s2.IsManifest(manifest):
m := s2.ToManifest(manifest)
client := oauth2.NewClient(context.TODO(), tokenSrc)
m, mfErr := s2.ToManifest(client, image, manifest)
if mfErr != nil {
return nil, fmt.Errorf("fetching manifest: %w", mfErr)
}

vulns, err = getSchema2Layers(m, config, tokenSrc, image, parent)
case s1.IsManifest(manifest):
Expand Down
4 changes: 3 additions & 1 deletion v2/docker/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
Expand All @@ -17,11 +18,12 @@ import (
func RequestManifest(client *http.Client, ref reference.Canonical) (distribution.Manifest, error) {
var manifest distribution.Manifest

request, err := http.NewRequest(http.MethodGet, uri.GetManifestURI(ref), nil)
request, err := http.NewRequest(http.MethodGet, uri.GetDigestManifestURI(ref), nil)
if nil != err {
return nil, err
}

request.Header.Add("Accept", manifestlist.MediaTypeManifestList)
request.Header.Add("Accept", schema2.MediaTypeManifest)
request.Header.Add("Accept", schema1.MediaTypeManifest)
request.Header.Add("Accept", schema1.MediaTypeSignedManifest)
Expand Down
22 changes: 21 additions & 1 deletion v2/docker/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ func TestRequestManifest(t *testing.T) {
manifest, err := RequestManifest(client, ref)
require.NoError(t, err)

schema2Manifest := schema2.ToManifest(manifest)
schema2Manifest, err := schema2.ToManifest(client, ref, manifest)
require.NoError(t, err)

assert.Equal(
t,
Expand Down Expand Up @@ -55,3 +56,22 @@ func TestRateLimitedBadManifest(t *testing.T) {
err,
)
}

func TestRequestManifestList(t *testing.T) {
ref := vtesting.NewTestManifestListReference(t)

client, server := vtesting.PrepareDockerTest(t, ref)
defer server.Close()

manifest, err := RequestManifest(client, ref)
require.NoError(t, err)

schema2Manifest, err := schema2.ToManifest(client, ref, manifest)
require.NoError(t, err)

assert.Equal(
t,
vtesting.NewTestManifest().Manifest,
schema2Manifest,
)
}
6 changes: 5 additions & 1 deletion v2/docker/schema2/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package schema2
import (
"encoding/json"
"errors"
"fmt"
"net/http"

"github.com/docker/distribution"
Expand All @@ -23,7 +24,10 @@ func RequestConfig(client *http.Client, ref reference.Canonical, manifest distri
return nil, errors.New("cannot request schema2 config for non-schema2 manifest")
}

v2Manifest := ToManifest(manifest)
v2Manifest, err := ToManifest(client, ref, manifest)
if err != nil {
return nil, fmt.Errorf("fetching manifest: %w", err)
}

var wrapper v2Blob

Expand Down
74 changes: 67 additions & 7 deletions v2/docker/schema2/manifest.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,83 @@
package schema2

import (
"encoding/json"
"fmt"
"net/http"
"os"

"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema2"
v2 "github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
"github.com/grafeas/voucher/v2/docker/uri"
)

// IsManifest returns true if the passed manifest is a schema2 manifest.
func IsManifest(m distribution.Manifest) bool {
_, ok := m.(*v2.DeserializedManifest)
return ok
switch m.(type) {
case *v2.DeserializedManifest, *manifestlist.DeserializedManifestList:
return true
default:
return false
}
}

// ToManifest casts a distribution.Manifest to a schema2.Manifest. It panics
// if it passed anything other than a schema2.DeserialzedManifest.
func ToManifest(manifest distribution.Manifest) v2.Manifest {
schema2Manifest, ok := manifest.(*v2.DeserializedManifest)
if !ok {
panic("schema2.ToManifest was passed a non-schema2.DeserializedManifest")
func ToManifest(client *http.Client, ref reference.Named, manifest distribution.Manifest) (v2.Manifest, error) {
switch m := manifest.(type) {
case *v2.DeserializedManifest:
return m.Manifest, nil
case *manifestlist.DeserializedManifestList:
return resolveManifestFromList(client, ref, m)
default:
return v2.Manifest{}, fmt.Errorf("schema2.ToManifest was passed a %T", manifest)
}
}

// Ugly method to override the target os/arch without wiring the voucher config to this context
var targetOS, targetArch string

func init() {
targetOS = os.Getenv("VOUCHER_TARGET_OS")
if targetOS == "" {
targetOS = "linux"
}
targetArch = os.Getenv("VOUCHER_TARGET_ARCH")
if targetArch == "" {
targetArch = "amd64"
}
}

func resolveManifestFromList(client *http.Client, ref reference.Named, mfs *manifestlist.DeserializedManifestList) (v2.Manifest, error) {
for _, mf := range mfs.Manifests {
if mf.Platform.Architecture != targetArch || mf.Platform.OS != targetOS {
continue
}

return schema2Manifest.Manifest
manifestURI := uri.GetManifestURI(ref, string(mf.Digest))
req, err := http.NewRequest("GET", manifestURI, nil)
if err != nil {
return v2.Manifest{}, fmt.Errorf("preparing request to fetch manifest from list: %w", err)
}
req.Header.Add("Accept", schema2.MediaTypeManifest)

resp, err := client.Do(req)
if err != nil {
return v2.Manifest{}, fmt.Errorf("fetching manifest from list: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return v2.Manifest{}, fmt.Errorf("could not load manifest %q - %s", manifestURI, resp.Status)
}

var archManifest v2.DeserializedManifest
if err := json.NewDecoder(resp.Body).Decode(&archManifest); err != nil {
return v2.Manifest{}, fmt.Errorf("decoding fetched manifest from list: %w", err)
}
return archManifest.Manifest, nil
}
return v2.Manifest{}, fmt.Errorf("no manifest matching %s/%s found", targetOS, targetArch)
}
4 changes: 3 additions & 1 deletion v2/docker/schema2/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

vtesting "github.com/grafeas/voucher/v2/testing"
)

func TestToManifest(t *testing.T) {
newManifest := vtesting.NewTestManifest()

manifest := ToManifest(newManifest)
manifest, err := ToManifest(nil, nil, newManifest)
require.NoError(t, err)
assert.NotNil(t, manifest)
}
15 changes: 6 additions & 9 deletions v2/docker/uri/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,22 @@ func GetBlobURI(ref reference.Named, digest digest.Digest) string {
return u.String()
}

// GetManifestURI gets a manifest URI based on the passed repository and
// digest.
func GetManifestURI(ref reference.Canonical) string {
u := createURL(ref, reference.Path(ref), "manifests", string(ref.Digest()))
// GetManifestURI gets a manifest URI based on the passed repository and label (tag or digest).
func GetManifestURI(ref reference.Named, label string) string {
u := createURL(ref, reference.Path(ref), "manifests", label)
return u.String()
}

// GetTagManifestURI gets a manifest URI based on the passed repository and
// tag.
func GetTagManifestURI(ref reference.NamedTagged) string {
u := createURL(ref, reference.Path(ref), "manifests", ref.Tag())
return u.String()
return GetManifestURI(ref, ref.Tag())
}

// GetDigestManifestURI gets a manifest URI based on the passed repository and
// tag.
// digest.
func GetDigestManifestURI(ref reference.Canonical) string {
u := createURL(ref, reference.Path(ref), "manifests", string(ref.Digest()))
return u.String()
return GetManifestURI(ref, string(ref.Digest()))
}

func createURL(ref reference.Named, pathSegments ...string) url.URL {
Expand Down
2 changes: 1 addition & 1 deletion v2/docker/uri/uri_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ func TestGetBaseURI(t *testing.T) {
assert.Equal(t, hostname, "gcr.io")
assert.Equal(t, path, testProject)
assert.Equal(t, testBlobURL, GetBlobURI(canonicalRef, canonicalRef.Digest()))
assert.Equal(t, testManifestURL, GetManifestURI(canonicalRef))
assert.Equal(t, testManifestURL, GetDigestManifestURI(canonicalRef))
}
4 changes: 4 additions & 0 deletions v2/testing/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http/httptest"
"testing"

"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
dockerTypes "github.com/docker/docker/api/types"
Expand Down Expand Up @@ -57,6 +58,9 @@ func (mock *dockerAPIMock) ServeHTTP(writer http.ResponseWriter, req *http.Reque
case "/v2/path/to/image/blobs/sha256:b5b2b2c507a0944348e0303114d8d93bbbb081732b86451d9bce1f432a537bc7":
jsonRespond(writer, schema2.MediaTypeImageConfig, NewTestRootImageConfig())
return
case "/v2/path/to/image/manifests/sha256:fefafefa52ba402ed7dd98d73f5a41836ece508d1f4704b274562ac0c9b3b7da":
jsonRespond(writer, manifestlist.MediaTypeManifestList, NewTestManifestList())
return
}

http.Error(writer, fmt.Sprintf("failed to handle request: %s", req.URL.Path), 500)
Expand Down
36 changes: 36 additions & 0 deletions v2/testing/manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package vtesting

import (
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/libtrust"
Expand Down Expand Up @@ -45,6 +47,40 @@ func NewTestManifest() *schema2.DeserializedManifest {
return newManifest
}

func NewTestManifestList() *manifestlist.ManifestList {
return &manifestlist.ManifestList{
Versioned: manifest.Versioned{
MediaType: manifestlist.MediaTypeManifestList,
},
Manifests: []manifestlist.ManifestDescriptor{
// Wrong arch
{
Platform: manifestlist.PlatformSpec{
OS: "linux",
Architecture: "arm64",
},
},
// Wrong OS
{
Platform: manifestlist.PlatformSpec{
OS: "windows",
Architecture: "amd64",
},
},
// Matched manifest
{
Platform: manifestlist.PlatformSpec{
OS: "linux",
Architecture: "amd64",
},
Descriptor: distribution.Descriptor{
Digest: "sha256:b148c8af52ba402ed7dd98d73f5a41836ece508d1f4704b274562ac0c9b3b7da",
},
},
},
}
}

// NewTestRootManifest creates a test schema2 manifest for our mock Docker API, which points to an image whose user is configured to be root
func NewTestRootManifest() *schema2.DeserializedManifest {
manifest := schema2.Manifest{
Expand Down
8 changes: 8 additions & 0 deletions v2/testing/reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ func NewTestReference(t *testing.T) reference.Canonical {
return parseReference(t, "localhost/path/to/image@sha256:b148c8af52ba402ed7dd98d73f5a41836ece508d1f4704b274562ac0c9b3b7da")
}

// NewTestManifestListReference creates a new reference to be used throughout the docker tests.
// The returned reference is assumed to be a manifest list, with images for multiple platforms.
func NewTestManifestListReference(t *testing.T) reference.Canonical {
t.Helper()

return parseReference(t, "localhost/path/to/image@sha256:fefafefa52ba402ed7dd98d73f5a41836ece508d1f4704b274562ac0c9b3b7da")
}

// NewBadTestReference creates a new reference to be used throughout the docker tests.
// The returned reference is assumed to not, and does not have valid configuration
// or layers.
Expand Down