Fix manifest lists to always use correct size

Stores complete OCI descriptor instead of digest and platform
fields. This includes the size which was getting lost by not
storing the original manifest bytes.

Attempt to support existing cached files, if not output
the filename with the incorrect content.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
This commit is contained in:
Derek McGowan 2018-06-28 17:41:47 -07:00
parent ea65e9043c
commit 1fd2d66df8
9 changed files with 161 additions and 82 deletions

View File

@ -6,6 +6,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/store" "github.com/docker/cli/cli/manifest/store"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -64,20 +65,23 @@ func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
} }
// Update the mf // Update the mf
if imageManifest.Descriptor.Platform == nil {
imageManifest.Descriptor.Platform = new(ocispec.Platform)
}
if opts.os != "" { if opts.os != "" {
imageManifest.Platform.OS = opts.os imageManifest.Descriptor.Platform.OS = opts.os
} }
if opts.arch != "" { if opts.arch != "" {
imageManifest.Platform.Architecture = opts.arch imageManifest.Descriptor.Platform.Architecture = opts.arch
} }
for _, osFeature := range opts.osFeatures { for _, osFeature := range opts.osFeatures {
imageManifest.Platform.OSFeatures = appendIfUnique(imageManifest.Platform.OSFeatures, osFeature) imageManifest.Descriptor.Platform.OSFeatures = appendIfUnique(imageManifest.Descriptor.Platform.OSFeatures, osFeature)
} }
if opts.variant != "" { if opts.variant != "" {
imageManifest.Platform.Variant = opts.variant imageManifest.Descriptor.Platform.Variant = opts.variant
} }
if !isValidOSArch(imageManifest.Platform.OS, imageManifest.Platform.Architecture) { if !isValidOSArch(imageManifest.Descriptor.Platform.OS, imageManifest.Descriptor.Platform.Architecture) {
return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch) return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
} }
return manifestStore.Save(targetRef, imgRef, imageManifest) return manifestStore.Save(targetRef, imgRef, imageManifest)

View File

@ -12,6 +12,7 @@ import (
"github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/registry" "github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -123,7 +124,7 @@ func printManifestList(dockerCli command.Cli, namedRef reference.Named, list []t
for _, img := range list { for _, img := range list {
mfd, err := buildManifestDescriptor(targetRepo, img) mfd, err := buildManifestDescriptor(targetRepo, img)
if err != nil { if err != nil {
return fmt.Errorf("error assembling ManifestDescriptor") return errors.Wrap(err, "failed to assemble ManifestDescriptor")
} }
manifests = append(manifests, mfd) manifests = append(manifests, mfd)
} }

View File

@ -14,6 +14,7 @@ import (
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
digest "github.com/opencontainers/go-digest" digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
@ -50,8 +51,22 @@ func fullImageManifest(t *testing.T, ref reference.Named) types.ImageManifest {
}, },
}) })
assert.NilError(t, err) assert.NilError(t, err)
// TODO: include image data for verbose inspect // TODO: include image data for verbose inspect
return types.NewImageManifest(ref, digest.Digest("sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd"), types.Image{OS: "linux", Architecture: "amd64"}, man) mt, raw, err := man.Payload()
assert.NilError(t, err)
desc := ocispec.Descriptor{
Digest: digest.FromBytes(raw),
Size: int64(len(raw)),
MediaType: mt,
Platform: &ocispec.Platform{
Architecture: "amd64",
OS: "linux",
},
}
return types.NewImageManifest(ref, desc, man)
} }
func TestInspectCommandLocalManifestNotFound(t *testing.T) { func TestInspectCommandLocalManifestNotFound(t *testing.T) {

View File

@ -10,6 +10,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/types" "github.com/docker/cli/cli/manifest/types"
registryclient "github.com/docker/cli/cli/registry/client" registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@ -141,7 +142,9 @@ func buildManifestList(manifests []types.ImageManifest, targetRef reference.Name
descriptors := []manifestlist.ManifestDescriptor{} descriptors := []manifestlist.ManifestDescriptor{}
for _, imageManifest := range manifests { for _, imageManifest := range manifests {
if imageManifest.Platform.Architecture == "" || imageManifest.Platform.OS == "" { if imageManifest.Descriptor.Platform == nil ||
imageManifest.Descriptor.Platform.Architecture == "" ||
imageManifest.Descriptor.Platform.OS == "" {
return nil, errors.Errorf( return nil, errors.Errorf(
"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref) "manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
} }
@ -167,17 +170,18 @@ func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest
return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname) return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
} }
mediaType, raw, err := imageManifest.Payload() manifest := manifestlist.ManifestDescriptor{
if err != nil { Descriptor: distribution.Descriptor{
return manifestlist.ManifestDescriptor{}, err Digest: imageManifest.Descriptor.Digest,
Size: imageManifest.Descriptor.Size,
MediaType: imageManifest.Descriptor.MediaType,
},
} }
manifest := manifestlist.ManifestDescriptor{ platform := types.PlatformSpecFromOCI(imageManifest.Descriptor.Platform)
Platform: imageManifest.Platform, if platform != nil {
manifest.Platform = *platform
} }
manifest.Descriptor.Digest = imageManifest.Digest
manifest.Size = int64(len(raw))
manifest.MediaType = mediaType
if err = manifest.Descriptor.Digest.Validate(); err != nil { if err = manifest.Descriptor.Digest.Validate(); err != nil {
return manifestlist.ManifestDescriptor{}, errors.Wrapf(err, return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
@ -195,7 +199,11 @@ func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.
if err != nil { if err != nil {
return nil, err return nil, err
} }
blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: imageManifest.Platform.OS}) var os string
if imageManifest.Descriptor.Platform != nil {
os = imageManifest.Descriptor.Platform.OS
}
blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: os})
} }
return blobReqs, nil return blobReqs, nil
} }
@ -206,7 +214,7 @@ func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef refere
if err != nil { if err != nil {
return mountRequest{}, err return mountRequest{}, err
} }
mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Digest) mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Descriptor.Digest)
if err != nil { if err != nil {
return mountRequest{}, err return mountRequest{}, err
} }

View File

@ -1,6 +1,18 @@
{ {
"Ref": "example.com/alpine:3.0", "Ref": "example.com/alpine:3.0",
"Digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd", "Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe",
"size": 528,
"platform": {
"architecture": "arm",
"os": "freebsd",
"os.features": [
"feature1"
],
"variant": "v7"
}
},
"SchemaV2Manifest": { "SchemaV2Manifest": {
"schemaVersion": 2, "schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json", "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
@ -16,13 +28,5 @@
"digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926" "digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926"
} }
] ]
},
"Platform": {
"architecture": "arm",
"os": "freebsd",
"os.features": [
"feature1"
],
"variant": "v7"
} }
} }

View File

@ -4,8 +4,8 @@
"manifests": [ "manifests": [
{ {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json", "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 428, "size": 528,
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd", "digest": "sha256:1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe",
"platform": { "platform": {
"architecture": "amd64", "architecture": "amd64",
"os": "linux" "os": "linux"
@ -13,8 +13,8 @@
}, },
{ {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json", "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 428, "size": 528,
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd", "digest": "sha256:1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe",
"platform": { "platform": {
"architecture": "amd64", "architecture": "amd64",
"os": "linux" "os": "linux"

View File

@ -9,7 +9,11 @@ import (
"strings" "strings"
"github.com/docker/cli/cli/manifest/types" "github.com/docker/cli/cli/manifest/types"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
) )
// Store manages local storage of image distribution manifests // Store manages local storage of image distribution manifests
@ -50,8 +54,37 @@ func (s *fsStore) getFromFilename(ref reference.Reference, filename string) (typ
case err != nil: case err != nil:
return types.ImageManifest{}, err return types.ImageManifest{}, err
} }
var manifestInfo types.ImageManifest var manifestInfo struct {
return manifestInfo, json.Unmarshal(bytes, &manifestInfo) types.ImageManifest
// Deprecated Fields, replaced by Descriptor
Digest digest.Digest
Platform *manifestlist.PlatformSpec
}
if err := json.Unmarshal(bytes, &manifestInfo); err != nil {
return types.ImageManifest{}, err
}
// Compatibility with image manifests created before
// descriptor, newer versions omit Digest and Platform
if manifestInfo.Digest != "" {
mediaType, raw, err := manifestInfo.Payload()
if err != nil {
return types.ImageManifest{}, err
}
if dgst := digest.FromBytes(raw); dgst != manifestInfo.Digest {
return types.ImageManifest{}, errors.Errorf("invalid manifest file %v: image manifest digest mismatch (%v != %v)", filename, manifestInfo.Digest, dgst)
}
manifestInfo.ImageManifest.Descriptor = ocispec.Descriptor{
Digest: manifestInfo.Digest,
Size: int64(len(raw)),
MediaType: mediaType,
Platform: types.OCIPlatform(manifestInfo.Platform),
}
}
return manifestInfo.ImageManifest, nil
} }
// GetList returns all the local manifests for a transaction // GetList returns all the local manifests for a transaction

View File

@ -8,15 +8,46 @@ import (
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// ImageManifest contains info to output for a manifest object. // ImageManifest contains info to output for a manifest object.
type ImageManifest struct { type ImageManifest struct {
Ref *SerializableNamed Ref *SerializableNamed
Digest digest.Digest Descriptor ocispec.Descriptor
// SchemaV2Manifest is used for inspection
// TODO: Deprecate this and store manifest blobs
SchemaV2Manifest *schema2.DeserializedManifest `json:",omitempty"` SchemaV2Manifest *schema2.DeserializedManifest `json:",omitempty"`
Platform manifestlist.PlatformSpec }
// OCIPlatform creates an OCI platform from a manifest list platform spec
func OCIPlatform(ps *manifestlist.PlatformSpec) *ocispec.Platform {
if ps == nil {
return nil
}
return &ocispec.Platform{
Architecture: ps.Architecture,
OS: ps.OS,
OSVersion: ps.OSVersion,
OSFeatures: ps.OSFeatures,
Variant: ps.Variant,
}
}
// PlatformSpecFromOCI creates a platform spec from OCI platform
func PlatformSpecFromOCI(p *ocispec.Platform) *manifestlist.PlatformSpec {
if p == nil {
return nil
}
return &manifestlist.PlatformSpec{
Architecture: p.Architecture,
OS: p.OS,
OSVersion: p.OSVersion,
OSFeatures: p.OSFeatures,
Variant: p.Variant,
}
} }
// Blobs returns the digests for all the blobs referenced by this manifest // Blobs returns the digests for all the blobs referenced by this manifest
@ -30,6 +61,7 @@ func (i ImageManifest) Blobs() []digest.Digest {
// Payload returns the media type and bytes for the manifest // Payload returns the media type and bytes for the manifest
func (i ImageManifest) Payload() (string, []byte, error) { func (i ImageManifest) Payload() (string, []byte, error) {
// TODO: If available, read content from a content store by digest
switch { switch {
case i.SchemaV2Manifest != nil: case i.SchemaV2Manifest != nil:
return i.SchemaV2Manifest.Payload() return i.SchemaV2Manifest.Payload()
@ -51,18 +83,11 @@ func (i ImageManifest) References() []distribution.Descriptor {
// NewImageManifest returns a new ImageManifest object. The values for Platform // NewImageManifest returns a new ImageManifest object. The values for Platform
// are initialized from those in the image // are initialized from those in the image
func NewImageManifest(ref reference.Named, digest digest.Digest, img Image, manifest *schema2.DeserializedManifest) ImageManifest { func NewImageManifest(ref reference.Named, desc ocispec.Descriptor, manifest *schema2.DeserializedManifest) ImageManifest {
platform := manifestlist.PlatformSpec{
OS: img.OS,
Architecture: img.Architecture,
OSVersion: img.OSVersion,
OSFeatures: img.OSFeatures,
}
return ImageManifest{ return ImageManifest{
Ref: &SerializableNamed{Named: ref}, Ref: &SerializableNamed{Named: ref},
Digest: digest, Descriptor: desc,
SchemaV2Manifest: manifest, SchemaV2Manifest: manifest,
Platform: platform,
} }
} }
@ -87,21 +112,3 @@ func (s *SerializableNamed) UnmarshalJSON(b []byte) error {
func (s *SerializableNamed) MarshalJSON() ([]byte, error) { func (s *SerializableNamed) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String()) return json.Marshal(s.String())
} }
// Image is the minimal set of fields required to set default platform settings
// on a manifest.
type Image struct {
Architecture string `json:"architecture,omitempty"`
OS string `json:"os,omitempty"`
OSVersion string `json:"os.version,omitempty"`
OSFeatures []string `json:"os.features,omitempty"`
}
// NewImageFromJSON creates an Image configuration from json.
func NewImageFromJSON(src []byte) (*Image, error) {
img := &Image{}
if err := json.Unmarshal(src, img); err != nil {
return nil, err
}
return img, nil
}

View File

@ -2,6 +2,7 @@ package client
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"github.com/docker/cli/cli/manifest/types" "github.com/docker/cli/cli/manifest/types"
@ -14,6 +15,7 @@ import (
distclient "github.com/docker/distribution/registry/client" distclient "github.com/docker/distribution/registry/client"
"github.com/docker/docker/registry" "github.com/docker/docker/registry"
digest "github.com/opencontainers/go-digest" digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -72,7 +74,7 @@ func getManifest(ctx context.Context, repo distribution.Repository, ref referenc
} }
func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst schema2.DeserializedManifest) (types.ImageManifest, error) { func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst schema2.DeserializedManifest) (types.ImageManifest, error) {
manifestDigest, err := validateManifestDigest(ref, mfst) manifestDesc, err := validateManifestDigest(ref, mfst)
if err != nil { if err != nil {
return types.ImageManifest{}, err return types.ImageManifest{}, err
} }
@ -81,11 +83,16 @@ func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distrib
return types.ImageManifest{}, err return types.ImageManifest{}, err
} }
img, err := types.NewImageFromJSON(configJSON) if manifestDesc.Platform == nil {
if err != nil { manifestDesc.Platform = &ocispec.Platform{}
}
// Fill in os and architecture fields from config JSON
if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil {
return types.ImageManifest{}, err return types.ImageManifest{}, err
} }
return types.NewImageManifest(ref, manifestDigest, *img, &mfst), nil
return types.NewImageManifest(ref, manifestDesc, &mfst), nil
} }
func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) { func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) {
@ -110,29 +117,26 @@ func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, re
// validateManifestDigest computes the manifest digest, and, if pulling by // validateManifestDigest computes the manifest digest, and, if pulling by
// digest, ensures that it matches the requested digest. // digest, ensures that it matches the requested digest.
func validateManifestDigest(ref reference.Named, mfst distribution.Manifest) (digest.Digest, error) { func validateManifestDigest(ref reference.Named, mfst distribution.Manifest) (ocispec.Descriptor, error) {
_, canonical, err := mfst.Payload() mediaType, canonical, err := mfst.Payload()
if err != nil { if err != nil {
return "", err return ocispec.Descriptor{}, err
}
desc := ocispec.Descriptor{
Digest: digest.FromBytes(canonical),
Size: int64(len(canonical)),
MediaType: mediaType,
} }
// If pull by digest, then verify the manifest digest. // If pull by digest, then verify the manifest digest.
if digested, isDigested := ref.(reference.Canonical); isDigested { if digested, isDigested := ref.(reference.Canonical); isDigested {
verifier := digested.Digest().Verifier() if digested.Digest() != desc.Digest {
if err != nil {
return "", err
}
if _, err := verifier.Write(canonical); err != nil {
return "", err
}
if !verifier.Verified() {
err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest()) err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest())
return "", err return ocispec.Descriptor{}, err
} }
return digested.Digest(), nil
} }
return digest.FromBytes(canonical), nil return desc, nil
} }
// pullManifestList handles "manifest lists" which point to various // pullManifestList handles "manifest lists" which point to various
@ -166,7 +170,10 @@ func pullManifestList(ctx context.Context, ref reference.Named, repo distributio
if err != nil { if err != nil {
return nil, err return nil, err
} }
imageManifest.Platform = manifestDescriptor.Platform
// Replace platform from config
imageManifest.Descriptor.Platform = types.OCIPlatform(&manifestDescriptor.Platform)
infos = append(infos, imageManifest) infos = append(infos, imageManifest)
} }
return infos, nil return infos, nil