diff --git a/pkg/dockerregistry/server/manifesthandler.go b/pkg/dockerregistry/server/manifesthandler.go index bf298e24b355..8f3dd7df3e07 100644 --- a/pkg/dockerregistry/server/manifesthandler.go +++ b/pkg/dockerregistry/server/manifesthandler.go @@ -1,6 +1,7 @@ package server import ( + "errors" "fmt" "github.com/docker/distribution" @@ -9,9 +10,13 @@ import ( "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" + imageapi "github.com/openshift/origin/pkg/image/apis/image" imageapiv1 "github.com/openshift/origin/pkg/image/apis/image/v1" ) +// ErrNotImplemented is returned by an interface instance that does not implement the method in question. +var ErrNotImplemented = errors.New("not implemented") + // A ManifestHandler defines a common set of operations on all versions of manifest schema. type ManifestHandler interface { // Config returns a blob with image configuration associated with the manifest. This applies only to @@ -21,6 +26,12 @@ type ManifestHandler interface { // Digest returns manifest's digest. Digest() (manifestDigest digest.Digest, err error) + // Layers returns a list of image layers. + Layers(ctx context.Context) ([]imageapiv1.ImageLayer, error) + + // Metadata returns image configuration in internal representation. + Metadata(ctx context.Context) (*imageapi.DockerImage, error) + // Manifest returns a deserialized manifest object. Manifest() distribution.Manifest @@ -52,7 +63,7 @@ func NewManifestHandlerFromImage(repo *repository, image *imageapiv1.Image) (Man ) switch image.DockerImageManifestMediaType { - case "", schema1.MediaTypeManifest: + case "", schema1.MediaTypeManifest, schema1.MediaTypeSignedManifest: manifest, err = unmarshalManifestSchema1([]byte(image.DockerImageManifest), image.DockerImageSignatures) case schema2.MediaTypeManifest: manifest, err = unmarshalManifestSchema2([]byte(image.DockerImageManifest)) diff --git a/pkg/dockerregistry/server/manifestschema1handler.go b/pkg/dockerregistry/server/manifestschema1handler.go index c821a4871f5b..ddc43f14cc18 100644 --- a/pkg/dockerregistry/server/manifestschema1handler.go +++ b/pkg/dockerregistry/server/manifestschema1handler.go @@ -2,6 +2,7 @@ package server import ( "encoding/json" + "errors" "fmt" "path" @@ -11,8 +12,16 @@ import ( "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/reference" "github.com/docker/libtrust" + + "k8s.io/apimachinery/pkg/util/sets" + + imageapi "github.com/openshift/origin/pkg/image/apis/image" + imageapiv1 "github.com/openshift/origin/pkg/image/apis/image/v1" ) +// ErrNoManifestMetadata is an error informing about invalid manifest that lacks metadata. +var ErrNoManifestMetadata = errors.New("no manifest metadata found") + func unmarshalManifestSchema1(content []byte, signatures [][]byte) (distribution.Manifest, error) { // prefer signatures from the manifest if _, err := libtrust.ParsePrettySignature(content, "signatures"); err == nil { @@ -41,8 +50,9 @@ func unmarshalManifestSchema1(content []byte, signatures [][]byte) (distribution } type manifestSchema1Handler struct { - repo *repository - manifest *schema1.SignedManifest + repo *repository + manifest *schema1.SignedManifest + cachedLayers []imageapiv1.ImageLayer } var _ ManifestHandler = &manifestSchema1Handler{} @@ -55,10 +65,86 @@ func (h *manifestSchema1Handler) Digest() (digest.Digest, error) { return digest.FromBytes(h.manifest.Canonical), nil } +func (h *manifestSchema1Handler) Layers(ctx context.Context) ([]imageapiv1.ImageLayer, error) { + if h.cachedLayers == nil { + var sizeContainer = imageapi.DockerV1CompatibilityImageSize{} + + layers := make([]imageapiv1.ImageLayer, len(h.manifest.FSLayers)) + for hi, li := 0, len(h.manifest.FSLayers)-1; hi < len(h.manifest.FSLayers) && li >= 0; hi, li = hi+1, li-1 { + layer := &layers[li] + sizeContainer.Size = 0 + if hi < len(h.manifest.History) { + if err := json.Unmarshal([]byte(h.manifest.History[hi].V1Compatibility), &sizeContainer); err != nil { + sizeContainer.Size = 0 + } + } + if err := h.updateLayerMetadata(ctx, layer, &h.manifest.FSLayers[hi], sizeContainer.Size); err != nil { + return nil, err + } + } + + h.cachedLayers = layers + } + + layers := make([]imageapiv1.ImageLayer, len(h.cachedLayers)) + for i, l := range h.cachedLayers { + layers[i] = l + } + + return layers, nil +} + func (h *manifestSchema1Handler) Manifest() distribution.Manifest { return h.manifest } +func (h *manifestSchema1Handler) Metadata(ctx context.Context) (*imageapi.DockerImage, error) { + if len(h.manifest.History) == 0 { + // should never have an empty history, but just in case... + return nil, ErrNoManifestMetadata + } + + v1Metadata := imageapi.DockerV1CompatibilityImage{} + if err := json.Unmarshal([]byte(h.manifest.History[0].V1Compatibility), &v1Metadata); err != nil { + return nil, err + } + + var ( + dockerImageSize int64 + layerSet = sets.NewString() + ) + + layers, err := h.Layers(ctx) + if err != nil { + return nil, err + } + for _, layer := range layers { + if !layerSet.Has(layer.Name) { + dockerImageSize += layer.LayerSize + layerSet.Insert(layer.Name) + } + } + + meta := &imageapi.DockerImage{} + meta.ID = v1Metadata.ID + meta.Parent = v1Metadata.Parent + meta.Comment = v1Metadata.Comment + meta.Created = v1Metadata.Created + meta.Container = v1Metadata.Container + meta.ContainerConfig = v1Metadata.ContainerConfig + meta.DockerVersion = v1Metadata.DockerVersion + meta.Author = v1Metadata.Author + meta.Config = v1Metadata.Config + meta.Architecture = v1Metadata.Architecture + meta.Size = dockerImageSize + + return meta, nil +} + +func (h *manifestSchema1Handler) Signatures(ctx context.Context) ([][]byte, error) { + return h.manifest.Signatures() +} + func (h *manifestSchema1Handler) Payload() (mediaType string, payload []byte, canonical []byte, err error) { mt, payload, err := h.manifest.Payload() return mt, payload, h.manifest.Canonical, err @@ -133,3 +219,25 @@ func (h *manifestSchema1Handler) Verify(ctx context.Context, skipDependencyVerif } return nil } + +func (h *manifestSchema1Handler) updateLayerMetadata( + ctx context.Context, + layerMetadata *imageapiv1.ImageLayer, + manifestLayer *schema1.FSLayer, + size int64, +) error { + layerMetadata.Name = manifestLayer.BlobSum.String() + layerMetadata.MediaType = schema1.MediaTypeManifestLayer + if size > 0 { + layerMetadata.LayerSize = size + return nil + } + + desc, err := h.repo.Blobs(ctx).Stat(ctx, digest.Digest(layerMetadata.Name)) + if err != nil { + context.GetLogger(ctx).Errorf("failed to stat blob %s", layerMetadata.Name) + return err + } + layerMetadata.LayerSize = desc.Size + return nil +} diff --git a/pkg/dockerregistry/server/manifestschema1handler_test.go b/pkg/dockerregistry/server/manifestschema1handler_test.go index f578b17ef7b9..17bb951b6eda 100644 --- a/pkg/dockerregistry/server/manifestschema1handler_test.go +++ b/pkg/dockerregistry/server/manifestschema1handler_test.go @@ -1,6 +1,7 @@ package server import ( + "encoding/json" "reflect" "strings" "testing" @@ -8,8 +9,15 @@ import ( "k8s.io/apimachinery/pkg/util/diff" "github.com/docker/distribution" + "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest/schema1" + "github.com/docker/libtrust" + + registryclient "github.com/openshift/origin/pkg/dockerregistry/server/client" + "github.com/openshift/origin/pkg/dockerregistry/testutil" + imageapi "github.com/openshift/origin/pkg/image/apis/image" + imageapiv1 "github.com/openshift/origin/pkg/image/apis/image/v1" ) func TestUnmarshalManifestSchema1(t *testing.T) { @@ -166,6 +174,358 @@ func TestUnmarshalManifestSchema1(t *testing.T) { } } +func TestManifestSchema1Handler_Layers(t *testing.T) { + for _, tc := range []struct { + name string + manifestString string + blobDescriptors blobDescriptors + expectedLayers []imageapiv1.ImageLayer + expectedError error + }{ + { + name: "valid manifest with sizes", + manifestString: manifestSchema1, + expectedLayers: []imageapiv1.ImageLayer{ + { + MediaType: schema1.MediaTypeManifestLayer, + Name: manifestSchema1Layers[1], + LayerSize: 1095501, + }, + { + MediaType: schema1.MediaTypeManifestLayer, + Name: digestSHA256GzippedEmptyTar.String(), + LayerSize: 0, + }, + }, + }, + + { + name: "valid manifest with missing sizes", + manifestString: manifestSchema1WithoutSize, + blobDescriptors: blobDescriptors{ + digest.Digest(manifestSchema1Layers[1]): distribution.Descriptor{ + Digest: digest.Digest(manifestSchema1Layers[1]), + Size: 100, + }, + }, + expectedLayers: []imageapiv1.ImageLayer{ + { + MediaType: schema1.MediaTypeManifestLayer, + Name: manifestSchema1Layers[1], + LayerSize: 100, + }, + { + MediaType: schema1.MediaTypeManifestLayer, + Name: digestSHA256GzippedEmptyTar.String(), + LayerSize: 0, + }, + }, + }, + + { + name: "manifest sizes take precedence", + manifestString: manifestSchema1, + blobDescriptors: blobDescriptors{ + digest.Digest(manifestSchema1Layers[1]): distribution.Descriptor{ + Digest: digest.Digest(manifestSchema1Layers[1]), + Size: 5, + }, + }, + expectedLayers: []imageapiv1.ImageLayer{ + { + MediaType: schema1.MediaTypeManifestLayer, + Name: manifestSchema1Layers[1], + LayerSize: 1095501, + }, + { + MediaType: schema1.MediaTypeManifestLayer, + Name: digestSHA256GzippedEmptyTar.String(), + LayerSize: 0, + }, + }, + }, + + { + name: "shorter history", + manifestString: manifestSchema1ShortHistory, + blobDescriptors: blobDescriptors{ + digest.Digest(manifestSchema1Layers[1]): distribution.Descriptor{ + Digest: digest.Digest(manifestSchema1Layers[1]), + Size: 100, + }, + }, + expectedLayers: []imageapiv1.ImageLayer{ + { + MediaType: schema1.MediaTypeManifestLayer, + Name: manifestSchema1Layers[1], + LayerSize: 100, + }, + { + MediaType: schema1.MediaTypeManifestLayer, + Name: digestSHA256GzippedEmptyTar.String(), + LayerSize: 0, + }, + }, + }, + + { + name: "shorter fs layers", + manifestString: manifestSchema1ShortFSLayers, + blobDescriptors: blobDescriptors{ + digest.Digest(manifestSchema1Layers[0]): distribution.Descriptor{ + Digest: digest.Digest(manifestSchema1Layers[0]), + Size: 100, + }, + }, + expectedLayers: []imageapiv1.ImageLayer{ + { + MediaType: schema1.MediaTypeManifestLayer, + Name: digestSHA256GzippedEmptyTar.String(), + LayerSize: 5, + }, + }, + }, + + { + name: "manifest with no layers", + manifestString: manifestSchema1NoLayers, + expectedLayers: []imageapiv1.ImageLayer{}, + }, + + { + name: "blob unknown", + manifestString: manifestSchema1WithoutSize, + expectedError: distribution.ErrBlobUnknown, + }, + } { + + t.Run(tc.name, func(t *testing.T) { + manifest := tryUnmarshalManifestSchema1OrGenerateSignatures(t, tc.manifestString) + + bds := blobDescriptors{ + digestSHA256GzippedEmptyTar: distribution.Descriptor{ + Digest: digestSHA256GzippedEmptyTar, + Size: 0, + }, + } + for d, desc := range tc.blobDescriptors { + bds[d] = desc + } + + bs := newTestBlobStore(bds, nil) + _, imageClient := testutil.NewFakeOpenShiftWithClient() + repo := newTestRepository(t, "nm", "repo", testRepositoryOptions{ + client: registryclient.NewFakeRegistryAPIClient(nil, imageClient), + blobs: bs, + }) + h, err := NewManifestHandler(repo, manifest) + if err != nil { + t.Fatal(err) + } + + ctx := withAuthPerformed(context.Background()) + layers, err := h.Layers(ctx) + if !assertErrorAndContinue(t, err, tc.expectedError) { + return + } + + if !reflect.DeepEqual(layers, tc.expectedLayers) { + t.Fatalf("got unexpected docker image layers: %s", diff.ObjectGoPrintDiff(layers, tc.expectedLayers)) + } + }) + } +} + +func TestManifestSchema1Handler_Metadata(t *testing.T) { + for _, tc := range []struct { + name string + manifestString string + metadataString string + blobDescriptors blobDescriptors + expectedImageSize int64 + expectedError error + }{ + { + name: "sizes in manifest", + manifestString: manifestSchema1, + metadataString: manifestSchema1Metadata, + expectedImageSize: 1095501, + }, + + { + name: "manifest without layer sizes", + manifestString: manifestSchema1WithoutSize, + metadataString: manifestSchema1Metadata, + blobDescriptors: blobDescriptors{ + digest.Digest(manifestSchema1Layers[1]): distribution.Descriptor{ + Digest: digest.Digest(manifestSchema1Layers[1]), + Size: 100, + }, + }, + expectedImageSize: 100, + }, + + { + name: "manifest sizes take precedence", + manifestString: manifestSchema1, + metadataString: manifestSchema1Metadata, + blobDescriptors: blobDescriptors{ + digest.Digest(manifestSchema1Layers[1]): distribution.Descriptor{ + Digest: digest.Digest(manifestSchema1Layers[1]), + Size: 5, + }, + }, + expectedImageSize: 1095501, + }, + + { + name: "manifest with shorter history", + manifestString: manifestSchema1ShortHistory, + metadataString: manifestSchema1Metadata, + blobDescriptors: blobDescriptors{ + digest.Digest(manifestSchema1Layers[1]): distribution.Descriptor{ + Digest: digest.Digest(manifestSchema1Layers[1]), + Size: 100, + }, + }, + expectedImageSize: 100, + }, + + { + name: "manifest with shorter fs layers", + manifestString: manifestSchema1ShortFSLayers, + metadataString: manifestSchema1Metadata, + blobDescriptors: blobDescriptors{ + digest.Digest(manifestSchema1Layers[0]): distribution.Descriptor{ + Digest: digest.Digest(manifestSchema1Layers[0]), + Size: 100, + }, + }, + expectedImageSize: 5, + }, + + { + name: "manifest with no layers", + manifestString: manifestSchema1NoLayers, + expectedError: ErrNoManifestMetadata, + }, + + { + name: "blob unknown", + manifestString: manifestSchema1WithoutSize, + expectedError: distribution.ErrBlobUnknown, + }, + } { + + t.Run(tc.name, func(t *testing.T) { + manifest := tryUnmarshalManifestSchema1OrGenerateSignatures(t, tc.manifestString) + + bds := blobDescriptors{ + digestSHA256GzippedEmptyTar: distribution.Descriptor{ + Digest: digestSHA256GzippedEmptyTar, + Size: 0, + }, + } + for d, desc := range tc.blobDescriptors { + bds[d] = desc + } + + bs := newTestBlobStore(bds, nil) + _, imageClient := testutil.NewFakeOpenShiftWithClient() + repo := newTestRepository(t, "nm", "repo", testRepositoryOptions{ + client: registryclient.NewFakeRegistryAPIClient(nil, imageClient), + blobs: bs, + }) + h, err := NewManifestHandler(repo, manifest) + if err != nil { + t.Fatal(err) + } + + ctx := withAuthPerformed(context.Background()) + meta, err := h.Metadata(ctx) + if !assertErrorAndContinue(t, err, tc.expectedError) { + return + } + + if meta.Created.IsZero() { + t.Errorf("unexpected non-zero Created value") + } + + expMeta := imageapi.DockerImage{} + if err := json.Unmarshal([]byte(tc.metadataString), &expMeta); err != nil { + t.Fatal(err) + } + expMeta.Size = tc.expectedImageSize + expMeta.TypeMeta = meta.TypeMeta + + if !reflect.DeepEqual(meta, &expMeta) { + t.Fatalf("got unexpected image metadata: %s", diff.ObjectGoPrintDiff(meta, &expMeta)) + } + }) + } +} + +var _signingKey libtrust.PrivateKey + +func getSigningKey(t *testing.T) libtrust.PrivateKey { + if _signingKey == nil { + sk, err := libtrust.GenerateECP256PrivateKey() + if err != nil { + t.Fatalf("failed to generate signing key: %v", err) + } + _signingKey = sk + } + return _signingKey +} + +func assertErrorAndContinue(t *testing.T, e, exp error) bool { + if e != nil { + if exp == nil { + t.Fatalf("got unexpected error: (%T) %v", e, e) + } + if e.Error() != exp.Error() { + t.Fatalf("got unexpected error: %s", diff.ObjectGoPrintDiff(e, exp)) + } + return false + } + if e == nil && exp != nil { + t.Fatalf("got non-error while expecting: %v", exp) + } + + return true +} + +func tryUnmarshalManifestSchema1OrGenerateSignatures(t *testing.T, manifestString string) *schema1.SignedManifest { + manifest, err := unmarshalManifestSchema1([]byte(manifestString), [][]byte{}) + if err != nil { + t.Logf("failed to unmarshal manifest because of: %v\ntrying to generate signatures...", err) + ms1 := schema1.Manifest{} + if err := json.Unmarshal([]byte(manifestString), &ms1); err != nil { + t.Fatalf("failed to unmarshal manifest json: %v", err) + } + signedManifest, err := schema1.Sign(&ms1, getSigningKey(t)) + if err != nil { + t.Fatalf("failed to sign manifest: %v", err) + } + payload, err := signedManifest.MarshalJSON() + if err != nil { + t.Fatalf("failed to serialize manifest: %v", err) + } + t.Logf("signed manifest:\n%s\n", string(payload)) + t.Fatalf("rewrite the manifest string according to the output") + } + + sm, ok := manifest.(*schema1.SignedManifest) + if !ok { + t.Fatalf("got unexpected manifest schema: %T", sm) + } + + return sm +} + +// imported from docker.io/busybox:1.23 +const manifestSchema1Digest = `sha256:2780635f864cc66c7a5c74aca8047970b95cb91b6d5c135964d984ffe07a2024` +const manifestSchema1Metadata = "{\"Id\":\"d7057cb020844f245031d27b76cb18af05db1cc3a96a29fa7777af75f5ac91a3\",\"Parent\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Created\":\"2015-09-21T20:15:47.866196515Z\",\"Container\":\"7f652467f9e6d1b3bf51172868b9b0c2fa1c711b112f4e987029b1624dd6295f\",\"ContainerConfig\":{\"Hostname\":\"5f8e0e129ff1\",\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"sh\\\"]\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\"},\"DockerVersion\":\"1.8.2\",\"Config\":{\"Hostname\":\"5f8e0e129ff1\",\"Cmd\":[\"sh\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\"},\"Architecture\":\"amd64\",\"Size\":1095501}\n" const manifestSchema1Signature = "{\"header\":{\"jwk\":{\"crv\":\"P-256\",\"kid\":\"QKEZ:N7ZA:BUSY:KPSH:PARP:NU4K:POHK:VLWF:EW22:4JFB:MKYJ:ZYSE\",\"kty\":\"EC\",\"x\":\"ppU7aXPngzHYJUswWcpDDL50hYkHWanmcrs_0X8L8Pc\",\"y\":\"dRpAggds8FfHRZsOms_g13XBOMnuqkP1fEWisGwvXso\"},\"alg\":\"ES256\"},\"signature\":\"KixitWkKYsVqNL0mkSxVSZMXQ61tzgXTlTlyeLHz4I2dZNXdDwHJZmYeoMGnYKM_HQKDcQHQeYSoxlu8AMTLOQ\",\"protected\":\"eyJmb3JtYXRMZW5ndGgiOjMyMTAsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wOS0xNVQwOTo0MzowNFoifQ\"}" var manifestSchema1Layers = []string{ @@ -327,6 +687,31 @@ const manifestSchema1ShortFSLayers = `{ ] }` +const manifestSchema1NoLayers = `{ + "schemaVersion": 1, + "name": "library/busybox", + "tag": "1.23", + "architecture": "amd64", + "fsLayers": [], + "history": [], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "4ZEJ:RG7V:AYDT:YJDG:E4QU:3PDO:KZBH:REE3:VMB5:2MBZ:BW7L:3HUF", + "kty": "EC", + "x": "mEFmDF5f4rVaJSNwLH7dyaaYPPi--L3V6Oqq5bvtZTA", + "y": "RqymHTBZ7UQenhOsqKhzwDDNjmMHSEuVujYZxwoJVjw" + }, + "alg": "ES256" + }, + "signature": "OK7YO7yFRTBcipZ7qgx7K5SHSEzqV99D9EkKM5oLBYbKl2ouQDv-wORH3QNARynRGqPbQ1Dyjpi-4z2kSvc74w", + "protected": "eyJmb3JtYXRMZW5ndGgiOjEzNiwiZm9ybWF0VGFpbCI6IkNuMCIsInRpbWUiOiIyMDE3LTA5LTE5VDEzOjA0OjA4WiJ9" + } + ] +}` + var manifestSchema1ExternalSignatures = [][]byte{[]byte(`{ "header": { "jwk": { diff --git a/pkg/dockerregistry/server/manifestschema2handler.go b/pkg/dockerregistry/server/manifestschema2handler.go index 18e45a9c7223..4f948ab127c7 100644 --- a/pkg/dockerregistry/server/manifestschema2handler.go +++ b/pkg/dockerregistry/server/manifestschema2handler.go @@ -10,6 +10,9 @@ import ( "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest/schema2" + + imageapi "github.com/openshift/origin/pkg/image/apis/image" + imageapiv1 "github.com/openshift/origin/pkg/image/apis/image/v1" ) var ( @@ -61,10 +64,18 @@ func (h *manifestSchema2Handler) Digest() (digest.Digest, error) { return digest.FromBytes(p), nil } +func (h *manifestSchema2Handler) Layers(ctx context.Context) ([]imageapiv1.ImageLayer, error) { + return nil, ErrNotImplemented +} + func (h *manifestSchema2Handler) Manifest() distribution.Manifest { return h.manifest } +func (h *manifestSchema2Handler) Metadata(ctx context.Context) (*imageapi.DockerImage, error) { + return nil, ErrNotImplemented +} + func (h *manifestSchema2Handler) Payload() (mediaType string, payload []byte, canonical []byte, err error) { mt, p, err := h.manifest.Payload() return mt, p, p, err diff --git a/pkg/dockerregistry/server/manifestservice.go b/pkg/dockerregistry/server/manifestservice.go index 9297301486f8..3b04ce796fb2 100644 --- a/pkg/dockerregistry/server/manifestservice.go +++ b/pkg/dockerregistry/server/manifestservice.go @@ -9,12 +9,16 @@ import ( "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/registry/api/errcode" regapi "github.com/docker/distribution/registry/api/v2" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + kapi "k8s.io/kubernetes/pkg/api" imageapi "github.com/openshift/origin/pkg/image/apis/image" imageapiv1 "github.com/openshift/origin/pkg/image/apis/image/v1" @@ -144,17 +148,24 @@ func (m *manifestService) Put(ctx context.Context, manifest distribution.Manifes ObjectMeta: metav1.ObjectMeta{ Name: dgst.String(), Annotations: map[string]string{ - imageapi.ManagedByOpenShiftAnnotation: "true", + imageapi.ManagedByOpenShiftAnnotation: "true", + // indicate to the master that the manifest and config objects can be safely unset imageapi.ImageManifestBlobStoredAnnotation: "true", }, }, DockerImageReference: fmt.Sprintf("%s/%s/%s@%s", m.repo.config.registryAddr, m.repo.namespace, m.repo.name, dgst.String()), - DockerImageManifest: string(payload), DockerImageManifestMediaType: mediaType, - DockerImageConfig: string(config), + // the following attributes will be unset by the master once it fills the metadata + DockerImageManifest: string(payload), + DockerImageConfig: string(config), }, } + err = m.fillImageMetadata(ctx, mh, &ism.Image) + if err != nil { + return "", err + } + for _, option := range options { if opt, ok := option.(distribution.WithTagOption); ok { ism.Tag = opt.Tag @@ -219,6 +230,56 @@ var manifestInflight = make(map[digest.Digest]struct{}) // manifestInflightSync protects manifestInflight var manifestInflightSync sync.Mutex +// fillImageMetadata fills metadata for image if needed. The metadata is filled by the master API if not +// already filled and if the maniest and config blobs are sent together with the image. The registry needs to +// fill the metadata only for schema 1 manifests that don't contain image sizes. Ideally, the master API +// should parse the manifest and fill the metadata for any manifest schema. In case of schema 1, it would have +// to stat the manifest blobs or they would have to be provided extra. +func (m *manifestService) fillImageMetadata(ctx context.Context, mh ManifestHandler, image *imageapiv1.Image) error { + if image.DockerImageManifestMediaType != schema1.MediaTypeManifest && image.DockerImageManifestMediaType != schema1.MediaTypeSignedManifest { + return nil + } + + layers, err := mh.Layers(ctx) + if err != nil { + return err + } + image.DockerImageLayers = layers + metadata, err := mh.Metadata(ctx) + if err != nil { + return err + } + image.DockerImageMetadata.Object = metadata + + gvString := image.DockerImageMetadataVersion + if len(gvString) == 0 { + gvString = "1.0" + } + if !strings.Contains(gvString, "/") { + gvString = "/" + gvString + } + + version, err := schema.ParseGroupVersion(gvString) + if err != nil { + return err + } + data, err := runtime.Encode(kapi.Codecs.LegacyCodec(version), metadata) + if err != nil { + return err + } + image.DockerImageMetadata.Raw = data + image.DockerImageMetadataVersion = version.Version + + if image.Annotations == nil { + image.Annotations = make(map[string]string) + } + // In earlier releases, the layers had a reversed order. This annotation indicates that the image has + // expected order of layers. + image.Annotations[imageapi.DockerImageLayersOrderAnnotation] = imageapi.DockerImageLayersOrderAscending + + return nil +} + func (m *manifestService) migrateManifest(ctx context.Context, image *imageapiv1.Image, dgst digest.Digest, manifest distribution.Manifest, isLocalStored bool) { // Everything in its place and nothing to do. if isLocalStored && len(image.DockerImageManifest) == 0 { diff --git a/pkg/dockerregistry/server/pullthroughblobstore_test.go b/pkg/dockerregistry/server/pullthroughblobstore_test.go index a5254c1d0b9f..34bfd249ee79 100644 --- a/pkg/dockerregistry/server/pullthroughblobstore_test.go +++ b/pkg/dockerregistry/server/pullthroughblobstore_test.go @@ -751,17 +751,17 @@ func (t *testBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribut func (t *testBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { t.calls["Put"]++ - return distribution.Descriptor{}, fmt.Errorf("method not implemented") + return distribution.Descriptor{}, ErrNotImplemented } func (t *testBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { t.calls["Create"]++ - return nil, fmt.Errorf("method not implemented") + return nil, ErrNotImplemented } func (t *testBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { t.calls["Resume"]++ - return nil, fmt.Errorf("method not implemented") + return nil, ErrNotImplemented } func (t *testBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, req *http.Request, dgst digest.Digest) error { @@ -783,7 +783,7 @@ func (t *testBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, re func (t *testBlobStore) Delete(ctx context.Context, dgst digest.Digest) error { t.calls["Delete"]++ - return fmt.Errorf("method not implemented") + return ErrNotImplemented } type testBlobFileReader struct { diff --git a/pkg/dockerregistry/server/pullthroughmanifestservice_test.go b/pkg/dockerregistry/server/pullthroughmanifestservice_test.go index 941d6f274ac8..02a875bfd3ae 100644 --- a/pkg/dockerregistry/server/pullthroughmanifestservice_test.go +++ b/pkg/dockerregistry/server/pullthroughmanifestservice_test.go @@ -571,7 +571,7 @@ func (t *testManifestService) Put(ctx context.Context, manifest distribution.Man func (t *testManifestService) Delete(ctx context.Context, dgst digest.Digest) error { t.calls["Delete"]++ - return fmt.Errorf("method not implemented") + return ErrNotImplemented } const etcdDigest = "sha256:958608f8ecc1dc62c93b6c610f3a834dae4220c9642e6e8b4e0f2b3ad7cbd238" diff --git a/pkg/image/util/helpers.go b/pkg/image/util/helpers.go index eae76e40cbf9..7760418dc233 100644 --- a/pkg/image/util/helpers.go +++ b/pkg/image/util/helpers.go @@ -167,7 +167,7 @@ func ReorderImageLayers(image *imageapi.Image) { layersOrder, ok := image.Annotations[imageapi.DockerImageLayersOrderAnnotation] if !ok { switch image.DockerImageManifestMediaType { - case schema1.MediaTypeManifest: + case schema1.MediaTypeManifest, schema1.MediaTypeSignedManifest: layersOrder = imageapi.DockerImageLayersOrderAscending case schema2.MediaTypeManifest: layersOrder = imageapi.DockerImageLayersOrderDescending @@ -212,7 +212,7 @@ func ManifestMatchesImage(image *imageapi.Image, newManifest []byte) (bool, erro if err != nil { return false, err } - case schema1.MediaTypeManifest, "": + case schema1.MediaTypeManifest, schema1.MediaTypeSignedManifest, "": var m schema1.SignedManifest if err := json.Unmarshal(newManifest, &m); err != nil { return false, err