diff --git a/pkg/dockerregistry/server/manifesthandler.go b/pkg/dockerregistry/server/manifesthandler.go index 3d6509f60348..bf298e24b355 100644 --- a/pkg/dockerregistry/server/manifesthandler.go +++ b/pkg/dockerregistry/server/manifesthandler.go @@ -14,11 +14,12 @@ import ( // A ManifestHandler defines a common set of operations on all versions of manifest schema. type ManifestHandler interface { - // FillImageMetadata fills a given image with metadata parsed from manifest. It also corrects layer sizes - // with blob sizes. Newer Docker client versions don't set layer sizes in the manifest schema 1 at all. - // Origin master needs correct layer sizes for proper image quota support. That's why we need to fill the - // metadata in the registry. - FillImageMetadata(ctx context.Context, image *imageapiv1.Image) error + // Config returns a blob with image configuration associated with the manifest. This applies only to + // manifet schema 2. + Config(ctx context.Context) ([]byte, error) + + // Digest returns manifest's digest. + Digest() (manifestDigest digest.Digest, err error) // Manifest returns a deserialized manifest object. Manifest() distribution.Manifest @@ -29,9 +30,6 @@ type ManifestHandler interface { // Verify returns an error if the contained manifest is not valid or has missing dependencies. Verify(ctx context.Context, skipDependencyVerification bool) error - - // Digest returns manifest's digest - Digest() (manifestDigest digest.Digest, err error) } // NewManifestHandler creates a manifest handler for the given manifest. diff --git a/pkg/dockerregistry/server/manifestschema1handler.go b/pkg/dockerregistry/server/manifestschema1handler.go index df9bb6911f30..c821a4871f5b 100644 --- a/pkg/dockerregistry/server/manifestschema1handler.go +++ b/pkg/dockerregistry/server/manifestschema1handler.go @@ -11,11 +11,6 @@ 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" ) func unmarshalManifestSchema1(content []byte, signatures [][]byte) (distribution.Manifest, error) { @@ -52,57 +47,12 @@ type manifestSchema1Handler struct { var _ ManifestHandler = &manifestSchema1Handler{} -func (h *manifestSchema1Handler) FillImageMetadata(ctx context.Context, image *imageapiv1.Image) error { - signatures, err := h.manifest.Signatures() - if err != nil { - return err - } - - for _, signDigest := range signatures { - image.DockerImageSignatures = append(image.DockerImageSignatures, signDigest) - } - - refs := h.manifest.References() - - if err := imageMetadataFromManifest(image); err != nil { - return fmt.Errorf("unable to fill image %s metadata: %v", image.Name, err) - } - - blobSet := sets.NewString() - meta, ok := image.DockerImageMetadata.Object.(*imageapi.DockerImage) - if !ok { - return fmt.Errorf("image %q does not have metadata", image.Name) - } - meta.Size = int64(0) - - blobs := h.repo.Blobs(ctx) - for i := range image.DockerImageLayers { - layer := &image.DockerImageLayers[i] - // DockerImageLayers represents h.manifest.Manifest.FSLayers in reversed order - desc, err := blobs.Stat(ctx, refs[len(image.DockerImageLayers)-i-1].Digest) - if err != nil { - context.GetLogger(ctx).Errorf("failed to stat blob %s of image %s", layer.Name, image.DockerImageReference) - return err - } - // The MediaType appeared in manifest schema v2. We need to fill it - // manually in the old images if it is not already filled. - if len(layer.MediaType) == 0 { - if len(desc.MediaType) > 0 { - layer.MediaType = desc.MediaType - } else { - layer.MediaType = schema1.MediaTypeManifestLayer - } - } - layer.LayerSize = desc.Size - // count empty layer just once (empty layer may actually have non-zero size) - if !blobSet.Has(layer.Name) { - meta.Size += desc.Size - blobSet.Insert(layer.Name) - } - } - image.DockerImageMetadata.Object = meta +func (h *manifestSchema1Handler) Config(ctx context.Context) ([]byte, error) { + return nil, nil +} - return nil +func (h *manifestSchema1Handler) Digest() (digest.Digest, error) { + return digest.FromBytes(h.manifest.Canonical), nil } func (h *manifestSchema1Handler) Manifest() distribution.Manifest { @@ -183,7 +133,3 @@ func (h *manifestSchema1Handler) Verify(ctx context.Context, skipDependencyVerif } return nil } - -func (h *manifestSchema1Handler) Digest() (digest.Digest, error) { - return digest.FromBytes(h.manifest.Canonical), nil -} diff --git a/pkg/dockerregistry/server/manifestschema1handler_test.go b/pkg/dockerregistry/server/manifestschema1handler_test.go new file mode 100644 index 000000000000..f578b17ef7b9 --- /dev/null +++ b/pkg/dockerregistry/server/manifestschema1handler_test.go @@ -0,0 +1,375 @@ +package server + +import ( + "reflect" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/util/diff" + + "github.com/docker/distribution" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema1" +) + +func TestUnmarshalManifestSchema1(t *testing.T) { + for _, tc := range []struct { + name string + manifestString string + signatures [][]byte + expectedName string + expectedTag string + expectedReferences []distribution.Descriptor + expectedSignatures [][]byte + expectedErrorSubstring string + }{ + { + name: "valid manifest with sizes", + manifestString: manifestSchema1, + expectedName: "library/busybox", + expectedTag: "1.23", + expectedReferences: []distribution.Descriptor{ + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[0])}, + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[1])}, + }, + expectedSignatures: [][]byte{[]byte(manifestSchema1Signature)}, + }, + + { + name: "valid manifest with missing sizes", + manifestString: manifestSchema1WithoutSize, + expectedName: "library/busybox", + expectedTag: "1.23", + expectedReferences: []distribution.Descriptor{ + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[0])}, + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[1])}, + }, + expectedSignatures: [][]byte{[]byte(manifestSchema1WithoutSizeSignature)}, + }, + + { + name: "having shorter history", + manifestString: manifestSchema1ShortHistory, + expectedName: "library/busybox", + expectedTag: "1.23", + expectedReferences: []distribution.Descriptor{ + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[0])}, + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[1])}, + }, + expectedSignatures: [][]byte{[]byte(manifestSchema1ShortHistorySignature)}, + }, + + { + name: "having shorter fs layers", + manifestString: manifestSchema1ShortFSLayers, + expectedName: "library/busybox", + expectedTag: "1.23", + expectedReferences: []distribution.Descriptor{ + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[0])}, + }, + expectedSignatures: [][]byte{[]byte(manifestSchema1ShortFSLayersSignature)}, + }, + + { + name: "additional signatures", + manifestString: manifestSchema1, + signatures: [][]byte{[]byte("my signature")}, + expectedName: "library/busybox", + expectedTag: "1.23", + expectedReferences: []distribution.Descriptor{ + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[0])}, + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[1])}, + }, + // the additional signature is ignored + expectedSignatures: [][]byte{[]byte(manifestSchema1Signature)}, + }, + + { + name: "manifest missing signatures", + manifestString: manifestSchema1MissingSignatures, + expectedErrorSubstring: "no signatures", + }, + + { + name: "just external signatures", + manifestString: manifestSchema1MissingSignatures, + signatures: manifestSchema1ExternalSignatures, + expectedName: "library/busybox", + expectedTag: "1.23", + expectedReferences: []distribution.Descriptor{ + {MediaType: schema1.MediaTypeManifestLayer, Digest: digest.Digest(manifestSchema1Layers[0])}, + }, + expectedSignatures: manifestSchema1ExternalSignaturesCompact, + }, + + { + name: "invalid manifest", + manifestString: manifestSchema1Invalid, + expectedErrorSubstring: "invalid character", + }, + + { + name: "manifest schema 2", + manifestString: manifestSchema2, + // FIXME: this could report some better error + expectedErrorSubstring: "no signatures", + }, + } { + + t.Run(tc.name, func(t *testing.T) { + manifest, err := unmarshalManifestSchema1([]byte(tc.manifestString), tc.signatures) + if err != nil { + if len(tc.expectedErrorSubstring) == 0 { + t.Fatalf("got unexpected error: (%T) %v", err, err) + } + if !strings.Contains(err.Error(), tc.expectedErrorSubstring) { + t.Fatalf("expected error with string %q, instead got: %v", tc.expectedErrorSubstring, err) + } + return + } + if err == nil && len(tc.expectedErrorSubstring) > 0 { + t.Fatalf("got non-error while expecting: %s", tc.expectedErrorSubstring) + } + + sm, ok := manifest.(*schema1.SignedManifest) + if !ok { + t.Fatalf("got unexpected manifest schema: %T", sm) + } + + if sm.Name != tc.expectedName { + t.Errorf("got unexpected image name: %s", diff.ObjectGoPrintDiff(sm.Name, tc.expectedName)) + } + if sm.Tag != tc.expectedTag { + t.Errorf("got unexpected image tag: %s", diff.ObjectGoPrintDiff(sm.Tag, tc.expectedTag)) + } + + refs := manifest.References() + if !reflect.DeepEqual(refs, tc.expectedReferences) { + t.Errorf("got unexpected image references: %s", diff.ObjectGoPrintDiff(refs, tc.expectedReferences)) + } + + signatures, err := sm.Signatures() + if err != nil { + t.Fatalf("failed to get manifest signatures: %v", err) + } + if !reflect.DeepEqual(signatures, tc.expectedSignatures) { + t.Errorf("got unexpected image signatures: %s", diff.ObjectGoPrintDiff(signatures, tc.expectedSignatures)) + for i, sig := range signatures { + t.Logf("signature #%d: %#v", i, string(sig)) + + } + for i, sig := range tc.expectedSignatures { + t.Logf("expected signature #%d: %#v", i, string(sig)) + } + } + }) + } +} + +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{ + digestSHA256GzippedEmptyTar.String(), + "sha256:9d7588d3c0635b53bd9a7dcd40bdf5d2d32cd3fb919c3a29ec2febbc2449eb19", +} + +// imported from docker.io/busybox:1.23 +const manifestSchema1 = `{ + "schemaVersion": 1, + "name": "library/busybox", + "tag": "1.23", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:9d7588d3c0635b53bd9a7dcd40bdf5d2d32cd3fb919c3a29ec2febbc2449eb19" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"d7057cb020844f245031d27b76cb18af05db1cc3a96a29fa7777af75f5ac91a3\",\"parent\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"created\":\"2015-09-21T20:15:47.866196515Z\",\"container\":\"7f652467f9e6d1b3bf51172868b9b0c2fa1c711b112f4e987029b1624dd6295f\",\"container_config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"sh\\\"]\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2\",\"config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"sh\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"created\":\"2015-09-21T20:15:47.433616227Z\",\"container\":\"5f8e0e129ff1e03bbb50a8b6ba7636fa5503c695125b1c392490d8aa113e8cf6\",\"container_config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:6cccb5f0a3b3947116a0c0f55d071980d94427ba0d6dad17bc68ead832cc0a8f in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2\",\"config\":{\"Hostname\":\"5f8e0e129ff1\",\"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\":1095501}\n" + } + ], + "signatures": [ + { + "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" + } + ] +}` + +const manifestSchema1WithoutSizeSignature = `{"header":{"jwk":{"crv":"P-256","kid":"IA3H:ZTL6:ZE5F:YBJU:TV2M:NSYK:W7ON:3D2K:5R3T:B7ZR:7J6X:IY4F","kty":"EC","x":"hM0pR9f7IIqWoKsD62bL_9tMmi1l04YRsVcCV_Q8ePw","y":"Lw1BZJLmNnII5Zt0Uk3nfqbDSDvqbZ5_ay4CM89AUTc"},"alg":"ES256"},"signature":"xlqhy7h3GLoiG_Z4sTwuvjA7t7pv9Jmc74kKkv8cozxvEPGvNOVgpnFDXtRkcfPIUNZAB8LJ6zMQWGkB5akSZA","protected":"eyJmb3JtYXRMZW5ndGgiOjMxOTMsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wOS0xOFQxMzozNDowNFoifQ"}` +const manifestSchema1WithoutSize = `{ + "schemaVersion": 1, + "name": "library/busybox", + "tag": "1.23", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:9d7588d3c0635b53bd9a7dcd40bdf5d2d32cd3fb919c3a29ec2febbc2449eb19" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"d7057cb020844f245031d27b76cb18af05db1cc3a96a29fa7777af75f5ac91a3\",\"parent\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"created\":\"2015-09-21T20:15:47.866196515Z\",\"container\":\"7f652467f9e6d1b3bf51172868b9b0c2fa1c711b112f4e987029b1624dd6295f\",\"container_config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"sh\\\"]\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2\",\"config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"sh\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"created\":\"2015-09-21T20:15:47.433616227Z\",\"container\":\"5f8e0e129ff1e03bbb50a8b6ba7636fa5503c695125b1c392490d8aa113e8cf6\",\"container_config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:6cccb5f0a3b3947116a0c0f55d071980d94427ba0d6dad17bc68ead832cc0a8f in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2\",\"config\":{\"Hostname\":\"5f8e0e129ff1\",\"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\"}\n" + } + ], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "IA3H:ZTL6:ZE5F:YBJU:TV2M:NSYK:W7ON:3D2K:5R3T:B7ZR:7J6X:IY4F", + "kty": "EC", + "x": "hM0pR9f7IIqWoKsD62bL_9tMmi1l04YRsVcCV_Q8ePw", + "y": "Lw1BZJLmNnII5Zt0Uk3nfqbDSDvqbZ5_ay4CM89AUTc" + }, + "alg": "ES256" + }, + "signature": "xlqhy7h3GLoiG_Z4sTwuvjA7t7pv9Jmc74kKkv8cozxvEPGvNOVgpnFDXtRkcfPIUNZAB8LJ6zMQWGkB5akSZA", + "protected": "eyJmb3JtYXRMZW5ndGgiOjMxOTMsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wOS0xOFQxMzozNDowNFoifQ" + } + ] +}` + +const manifestSchema1ShortHistorySignature = `{"header":{"jwk":{"crv":"P-256","kid":"BMQ5:5OIV:TJXC:IJQE:BYCE:7UBD:SWFQ:HFBN:STVV:XDNE:VJRG:KUUA","kty":"EC","x":"rZo1KLwKH0ZfiTzGFxTTQxbarJZ7gE4fWuPrucpZwjo","y":"QkoUQ3HauBjMythY94qevDCKzMEiLYJse3cVSqrSO4k"},"alg":"ES256"},"signature":"Fn_Diinka9s_cYTBvHoSklrBm3oM8rYe7PNZwEg_hAB-g0SOvTmiCqFjC9QahvhFtUZYT3cpZpJLFzRVAU32Tg","protected":"eyJmb3JtYXRMZW5ndGgiOjE4NTksImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wOS0xOFQxNDozMzo0MFoifQ"}` +const manifestSchema1ShortHistory = `{ + "schemaVersion": 1, + "name": "library/busybox", + "tag": "1.23", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:9d7588d3c0635b53bd9a7dcd40bdf5d2d32cd3fb919c3a29ec2febbc2449eb19" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"d7057cb020844f245031d27b76cb18af05db1cc3a96a29fa7777af75f5ac91a3\",\"parent\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"created\":\"2015-09-21T20:15:47.866196515Z\",\"container\":\"7f652467f9e6d1b3bf51172868b9b0c2fa1c711b112f4e987029b1624dd6295f\",\"container_config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"sh\\\"]\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2\",\"config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"sh\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"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": "BMQ5:5OIV:TJXC:IJQE:BYCE:7UBD:SWFQ:HFBN:STVV:XDNE:VJRG:KUUA", + "kty": "EC", + "x": "rZo1KLwKH0ZfiTzGFxTTQxbarJZ7gE4fWuPrucpZwjo", + "y": "QkoUQ3HauBjMythY94qevDCKzMEiLYJse3cVSqrSO4k" + }, + "alg": "ES256" + }, + "signature": "Fn_Diinka9s_cYTBvHoSklrBm3oM8rYe7PNZwEg_hAB-g0SOvTmiCqFjC9QahvhFtUZYT3cpZpJLFzRVAU32Tg", + "protected": "eyJmb3JtYXRMZW5ndGgiOjE4NTksImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wOS0xOFQxNDozMzo0MFoifQ" + } + ] +}` + +const manifestSchema1ShortFSLayersSignature = `{"header":{"jwk":{"crv":"P-256","kid":"JV5N:BVLF:L6WC:TVCF:7QJS:FB63:DGAS:IVJV:QQ2U:P77G:SVUF:TJPL","kty":"EC","x":"6cbmNJxXJi09n1hM1Yw5_vWeueCDjHGKXTyzQkH6KkM","y":"XSoPqwZ9pL8mQZkKAJb_SuUhtHsBN1_MP0sB6Bz4RN4"},"alg":"ES256"},"signature":"sdJzNKAlPrIeV4ftAwoSGBO3SP0p3ciqsSaj19Q-zDpgrU6R5L4uGp2OiP7yt5gz8w5kQScbjACrrfS-hcZTkA","protected":"eyJmb3JtYXRMZW5ndGgiOjMwODIsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wOS0xOFQxNTowNDowMFoifQ"}` +const manifestSchema1ShortFSLayers = `{ + "schemaVersion": 1, + "name": "library/busybox", + "tag": "1.23", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"d7057cb020844f245031d27b76cb18af05db1cc3a96a29fa7777af75f5ac91a3\",\"parent\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"created\":\"2015-09-21T20:15:47.866196515Z\",\"container\":\"7f652467f9e6d1b3bf51172868b9b0c2fa1c711b112f4e987029b1624dd6295f\",\"container_config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"sh\\\"]\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2\",\"config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"sh\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":5}\n" + }, + { + "v1Compatibility": "{\"id\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"created\":\"2015-09-21T20:15:47.433616227Z\",\"container\":\"5f8e0e129ff1e03bbb50a8b6ba7636fa5503c695125b1c392490d8aa113e8cf6\",\"container_config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:6cccb5f0a3b3947116a0c0f55d071980d94427ba0d6dad17bc68ead832cc0a8f in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2\",\"config\":{\"Hostname\":\"5f8e0e129ff1\",\"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\"}\n" + } + ], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "JV5N:BVLF:L6WC:TVCF:7QJS:FB63:DGAS:IVJV:QQ2U:P77G:SVUF:TJPL", + "kty": "EC", + "x": "6cbmNJxXJi09n1hM1Yw5_vWeueCDjHGKXTyzQkH6KkM", + "y": "XSoPqwZ9pL8mQZkKAJb_SuUhtHsBN1_MP0sB6Bz4RN4" + }, + "alg": "ES256" + }, + "signature": "sdJzNKAlPrIeV4ftAwoSGBO3SP0p3ciqsSaj19Q-zDpgrU6R5L4uGp2OiP7yt5gz8w5kQScbjACrrfS-hcZTkA", + "protected": "eyJmb3JtYXRMZW5ndGgiOjMwODIsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wOS0xOFQxNTowNDowMFoifQ" + } + ] +}` + +var manifestSchema1ExternalSignatures = [][]byte{[]byte(`{ + "header": { + "jwk": { + "crv": "P-256", + "kid": "QGG7:JZ2V:PFXZ:NKUP:XDPM:V3GS:KRRG:NB27:D4RF:2FQY:ISZV:TYUB", + "kty": "EC", + "x": "9itRpQlCqD-vlbSvGH9laJIuM9PfDOU7-mJ42zkFu7E", + "y": "zGP4n85_A2XgzZ770E3IWAijI0W5kbmv0FrgDPEcFMM" + }, + "alg": "ES256" + }, + "signature": "HbWKBd8wRh20G0HAO7qfFgviW2AI8a5woKM48fhTcPuJXr0qA9CyMoEdfrHFk_vwplv4w8CZImizfHbZ3UxzoQ", + "protected": "eyJmb3JtYXRMZW5ndGgiOjE3NDgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wOS0yMFQxMzoxNDozOVoifQ" +}`)} +var manifestSchema1ExternalSignaturesCompact = [][]byte{[]byte("{\"header\":{\"jwk\":{\"crv\":\"P-256\",\"kid\":\"QGG7:JZ2V:PFXZ:NKUP:XDPM:V3GS:KRRG:NB27:D4RF:2FQY:ISZV:TYUB\",\"kty\":\"EC\",\"x\":\"9itRpQlCqD-vlbSvGH9laJIuM9PfDOU7-mJ42zkFu7E\",\"y\":\"zGP4n85_A2XgzZ770E3IWAijI0W5kbmv0FrgDPEcFMM\"},\"alg\":\"ES256\"},\"signature\":\"HbWKBd8wRh20G0HAO7qfFgviW2AI8a5woKM48fhTcPuJXr0qA9CyMoEdfrHFk_vwplv4w8CZImizfHbZ3UxzoQ\",\"protected\":\"eyJmb3JtYXRMZW5ndGgiOjE3NDgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNy0wOS0yMFQxMzoxNDozOVoifQ\"}")} + +const manifestSchema1MissingSignatures = `{ + "schemaVersion": 1, + "name": "library/busybox", + "tag": "1.23", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"d7057cb020844f245031d27b76cb18af05db1cc3a96a29fa7777af75f5ac91a3\",\"parent\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"created\":\"2015-09-21T20:15:47.866196515Z\",\"container\":\"7f652467f9e6d1b3bf51172868b9b0c2fa1c711b112f4e987029b1624dd6295f\",\"container_config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"sh\\\"]\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2\",\"config\":{\"Hostname\":\"5f8e0e129ff1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"sh\"],\"Image\":\"cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":5}\n" + } + ] +}` + +const manifestSchema1Invalid = `{ + "schemaVersion": 1, + "name": "library/busybox", + "tag": "1.23", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + ], + "history": [], + "signatures": [], +}` diff --git a/pkg/dockerregistry/server/manifestschema2handler.go b/pkg/dockerregistry/server/manifestschema2handler.go index 3dae8df5d47d..18e45a9c7223 100644 --- a/pkg/dockerregistry/server/manifestschema2handler.go +++ b/pkg/dockerregistry/server/manifestschema2handler.go @@ -3,13 +3,13 @@ package server import ( "encoding/json" "errors" + "fmt" + "reflect" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest/schema2" - - imageapiv1 "github.com/openshift/origin/pkg/image/apis/image/v1" ) var ( @@ -23,28 +23,42 @@ func unmarshalManifestSchema2(content []byte) (distribution.Manifest, error) { return nil, err } + if !reflect.DeepEqual(deserializedManifest.Versioned, schema2.SchemaVersion) { + return nil, fmt.Errorf("unexpected manifest schema version=%d, mediaType=%q", + deserializedManifest.SchemaVersion, + deserializedManifest.MediaType) + } + return &deserializedManifest, nil } type manifestSchema2Handler struct { - repo *repository - manifest *schema2.DeserializedManifest + repo *repository + manifest *schema2.DeserializedManifest + cachedConfig []byte } var _ ManifestHandler = &manifestSchema2Handler{} -func (h *manifestSchema2Handler) FillImageMetadata(ctx context.Context, image *imageapiv1.Image) error { - // The manifest.Config references a configuration object for a container by its digest. - // It needs to be fetched in order to fill an image object metadata below. - configBytes, err := h.repo.Blobs(ctx).Get(ctx, h.manifest.Config.Digest) - if err != nil { - context.GetLogger(ctx).Errorf("failed to get image config %s: %v", h.manifest.Config.Digest.String(), err) - return err +func (h *manifestSchema2Handler) Config(ctx context.Context) ([]byte, error) { + if h.cachedConfig == nil { + blob, err := h.repo.Blobs(ctx).Get(ctx, h.manifest.Config.Digest) + if err != nil { + context.GetLogger(ctx).Errorf("failed to get manifest config: %v", err) + return nil, err + } + h.cachedConfig = blob } - image.DockerImageConfig = string(configBytes) - // We need to populate the image metadata using the manifest. - return imageMetadataFromManifest(image) + return h.cachedConfig, nil +} + +func (h *manifestSchema2Handler) Digest() (digest.Digest, error) { + _, p, err := h.manifest.Payload() + if err != nil { + return "", err + } + return digest.FromBytes(p), nil } func (h *manifestSchema2Handler) Manifest() distribution.Manifest { @@ -112,11 +126,3 @@ func (h *manifestSchema2Handler) Verify(ctx context.Context, skipDependencyVerif } return nil } - -func (h *manifestSchema2Handler) Digest() (digest.Digest, error) { - _, p, err := h.manifest.Payload() - if err != nil { - return "", err - } - return digest.FromBytes(p), nil -} diff --git a/pkg/dockerregistry/server/manifestschema2handler_test.go b/pkg/dockerregistry/server/manifestschema2handler_test.go new file mode 100644 index 000000000000..9e25fa927a48 --- /dev/null +++ b/pkg/dockerregistry/server/manifestschema2handler_test.go @@ -0,0 +1,138 @@ +package server + +import ( + "reflect" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/util/diff" + + "github.com/docker/distribution" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema2" +) + +func TestUnmarshalManifestSchema2(t *testing.T) { + for _, tc := range []struct { + name string + manifestString string + expectedConfig distribution.Descriptor + expectedReferences []distribution.Descriptor + expectedErrorSubstring string + }{ + { + name: "valid nginx image with sizes in manifest", + manifestString: manifestSchema2, + expectedConfig: manifestSchema2ConfigDescriptor, + expectedReferences: []distribution.Descriptor{ + manifestSchema2ConfigDescriptor, + manifestSchema2LayerDescriptors[0], + manifestSchema2LayerDescriptors[1], + manifestSchema2LayerDescriptors[2], + }, + }, + + { + name: "invalid schema2 image", + manifestString: manifestSchema2Invalid, + expectedErrorSubstring: "invalid character", + }, + + { + name: "manifest schema1 image", + manifestString: manifestSchema1, + expectedErrorSubstring: "unexpected manifest schema version", + }, + } { + + t.Run(tc.name, func(t *testing.T) { + manifest, err := unmarshalManifestSchema2([]byte(tc.manifestString)) + if err != nil { + if len(tc.expectedErrorSubstring) == 0 { + t.Fatalf("got unexpected error: (%T) %v", err, err) + } + if !strings.Contains(err.Error(), tc.expectedErrorSubstring) { + t.Fatalf("expected error with string %q, instead got: %v", tc.expectedErrorSubstring, err) + } + return + } + if err == nil && len(tc.expectedErrorSubstring) > 0 { + t.Fatalf("got non-error while expecting: %s", tc.expectedErrorSubstring) + } + + dm, ok := manifest.(*schema2.DeserializedManifest) + if !ok { + t.Fatalf("got unexpected manifest schema: %T", manifest) + } + + if !reflect.DeepEqual(dm.Config, tc.expectedConfig) { + t.Errorf("got unexpected image config descriptor: %s", diff.ObjectGoPrintDiff(dm.Config, tc.expectedConfig)) + } + + refs := dm.References() + if !reflect.DeepEqual(refs, tc.expectedReferences) { + t.Errorf("got unexpected image references: %s", diff.ObjectGoPrintDiff(refs, tc.expectedReferences)) + } + }) + } +} + +var manifestSchema2LayerDescriptors = []distribution.Descriptor{ + { + MediaType: schema2.MediaTypeLayer, + Digest: digest.Digest("sha256:afeb2bfd31c0760573e7262de6ae67a84da0e0a1c3e8157bbddd41a501b18a5c"), + Size: 22488057, + }, + { + MediaType: schema2.MediaTypeLayer, + Digest: digest.Digest("sha256:7ff5d10493db2cdfc1b7238434c503cc0664d48d0f7154ea9472e734b28a72dd"), + Size: 21869700, + }, + { + MediaType: schema2.MediaTypeLayer, + Digest: digest.Digest("sha256:d2562f1ae1d0593a26c54006ad0a6211c35fdc8b4067485d7208000d83477de2"), + Size: 201, + }, +} + +const manifestSchema2ConfigDigest = `sha256:da5939581ac835614e3cf6c765e7489e6d0fc602a44e98c07013f1c938f49675` + +var manifestSchema2ConfigDescriptor = distribution.Descriptor{ + Digest: digest.Digest(manifestSchema2ConfigDigest), + Size: 5838, + MediaType: schema2.MediaTypeConfig, +} + +const manifestSchema2 = `{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 5838, + "digest": "sha256:da5939581ac835614e3cf6c765e7489e6d0fc602a44e98c07013f1c938f49675" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 22488057, + "digest": "sha256:afeb2bfd31c0760573e7262de6ae67a84da0e0a1c3e8157bbddd41a501b18a5c" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 21869700, + "digest": "sha256:7ff5d10493db2cdfc1b7238434c503cc0664d48d0f7154ea9472e734b28a72dd" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 201, + "digest": "sha256:d2562f1ae1d0593a26c54006ad0a6211c35fdc8b4067485d7208000d83477de2" + } + ] +}` + +const manifestSchema2Invalid = `{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": {}, + [] +}` diff --git a/pkg/dockerregistry/server/manifestservice.go b/pkg/dockerregistry/server/manifestservice.go index 12455feae68b..9297301486f8 100644 --- a/pkg/dockerregistry/server/manifestservice.go +++ b/pkg/dockerregistry/server/manifestservice.go @@ -104,7 +104,7 @@ func (m *manifestService) Put(ctx context.Context, manifest distribution.Manifes if err != nil { return "", regapi.ErrorCodeManifestInvalid.WithDetail(err) } - mediaType, payload, canonical, err := mh.Payload() + mediaType, payload, _, err := mh.Payload() if err != nil { return "", regapi.ErrorCodeManifestInvalid.WithDetail(err) } @@ -124,8 +124,15 @@ func (m *manifestService) Put(ctx context.Context, manifest distribution.Manifes return "", err } - // Calculate digest - dgst := digest.FromBytes(canonical) + config, err := mh.Config(ctx) + if err != nil { + return "", err + } + + dgst, err := mh.Digest() + if err != nil { + return "", err + } // Upload to openshift ism := imageapiv1.ImageStreamMapping{ @@ -137,12 +144,14 @@ 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", + 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), }, } @@ -153,14 +162,6 @@ func (m *manifestService) Put(ctx context.Context, manifest distribution.Manifes } } - if err = mh.FillImageMetadata(ctx, &ism.Image); err != nil { - return "", err - } - - // Remove the raw manifest as it's very big and this leads to a large memory consumption in etcd. - ism.Image.DockerImageManifest = "" - ism.Image.DockerImageConfig = "" - if _, err = m.repo.registryOSClient.ImageStreamMappings(m.repo.namespace).Create(&ism); err != nil { // if the error was that the image stream wasn't found, try to auto provision it statusErr, ok := err.(*kerrors.StatusError) @@ -248,12 +249,14 @@ func (m *manifestService) storeManifestLocally(ctx context.Context, image *image } } - if len(image.DockerImageManifest) == 0 { + if len(image.DockerImageManifest) == 0 || image.Annotations[imageapi.ImageManifestBlobStoredAnnotation] == "true" { return } - image.DockerImageManifest = "" - image.DockerImageConfig = "" + if image.Annotations == nil { + image.Annotations = make(map[string]string) + } + image.Annotations[imageapi.ImageManifestBlobStoredAnnotation] = "true" if _, err := m.repo.updateImage(image); err != nil { context.GetLogger(ctx).Errorf("error updating Image: %v", err) diff --git a/pkg/dockerregistry/server/manifestservice_test.go b/pkg/dockerregistry/server/manifestservice_test.go index fc75cc2b1721..5738842403db 100644 --- a/pkg/dockerregistry/server/manifestservice_test.go +++ b/pkg/dockerregistry/server/manifestservice_test.go @@ -12,6 +12,7 @@ import ( 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" ) func TestManifestServiceExists(t *testing.T) { @@ -50,6 +51,7 @@ func TestManifestServiceGetDoesntChangeDockerImageReference(t *testing.T) { namespace := "user" repo := "app" tag := "latest" + const img1Manifest = `{"_":"some json to start migration"}` fos, imageClient := testutil.NewFakeOpenShiftWithClient() @@ -60,7 +62,7 @@ func TestManifestServiceGetDoesntChangeDockerImageReference(t *testing.T) { img1 := *testImage img1.DockerImageReference = "1" - img1.DockerImageManifest = `{"_":"some json to start migration"}` + img1.DockerImageManifest = img1Manifest testutil.AddUntaggedImage(t, fos, &img1) img2 := *testImage @@ -100,7 +102,10 @@ func TestManifestServiceGetDoesntChangeDockerImageReference(t *testing.T) { if err != nil { t.Fatal(err) } - if img.DockerImageManifest != "" { + if img.Annotations[imageapi.ImageManifestBlobStoredAnnotation] != "true" { + t.Errorf("missing %q annotation on image", imageapi.ImageManifestBlobStoredAnnotation) + } + if img.DockerImageManifest != img1Manifest { t.Errorf("image doesn't migrated, img.DockerImageManifest: want %q, got %q", "", img.DockerImageManifest) } if img.DockerImageReference != "1" { @@ -115,7 +120,7 @@ func TestManifestServicePut(t *testing.T) { _, imageClient := testutil.NewFakeOpenShiftWithClient() - bs := newTestBlobStore(map[digest.Digest][]byte{ + bs := newTestBlobStore(nil, blobContents{ "test:1": []byte("{}"), }) diff --git a/pkg/dockerregistry/server/pullthroughblobstore_test.go b/pkg/dockerregistry/server/pullthroughblobstore_test.go index 4de88cf6bf62..a5254c1d0b9f 100644 --- a/pkg/dockerregistry/server/pullthroughblobstore_test.go +++ b/pkg/dockerregistry/server/pullthroughblobstore_test.go @@ -133,7 +133,7 @@ func TestPullthroughServeBlob(t *testing.T) { expectedLocalCalls: map[string]int{"Stat": 1}, }, } { - localBlobStore := newTestBlobStore(tc.localBlobs) + localBlobStore := newTestBlobStore(nil, tc.localBlobs) ctx := WithTestPassthroughToUpstream(context.Background(), false) repo := newTestRepository(t, namespace, name, testRepositoryOptions{ @@ -284,7 +284,7 @@ func TestPullthroughServeNotSeekableBlob(t *testing.T) { }) registrytest.AddImage(t, fos, testImage, namespace, name, "latest") - localBlobStore := newTestBlobStore(nil) + localBlobStore := newTestBlobStore(nil, nil) ctx := WithTestPassthroughToUpstream(context.Background(), false) repo := newTestRepository(t, namespace, name, testRepositoryOptions{ @@ -602,7 +602,7 @@ func TestPullthroughServeBlobInsecure(t *testing.T) { tc.fakeOpenShiftInit(fos) - localBlobStore := newTestBlobStore(tc.localBlobs) + localBlobStore := newTestBlobStore(nil, tc.localBlobs) ctx := WithTestPassthroughToUpstream(context.Background(), false) @@ -680,9 +680,13 @@ func makeDigestFromBytes(data []byte) digest.Digest { return digest.Digest(fmt.Sprintf("sha256:%x", sha256.Sum256(data))) } +type blobContents map[digest.Digest][]byte +type blobDescriptors map[digest.Digest]distribution.Descriptor + type testBlobStore struct { + blobDescriptors blobDescriptors // blob digest mapped to content - blobs map[digest.Digest][]byte + blobs blobContents // method name mapped to number of invocations calls map[string]int bytesServed int64 @@ -690,19 +694,29 @@ type testBlobStore struct { var _ distribution.BlobStore = &testBlobStore{} -func newTestBlobStore(blobs map[digest.Digest][]byte) *testBlobStore { - b := make(map[digest.Digest][]byte) +func newTestBlobStore(blobDescriptors blobDescriptors, blobs blobContents) *testBlobStore { + bs := make(map[digest.Digest][]byte) for d, content := range blobs { - b[d] = content + bs[d] = content + } + bds := make(map[digest.Digest]distribution.Descriptor) + for d, desc := range blobDescriptors { + bds[d] = desc } return &testBlobStore{ - blobs: b, - calls: make(map[string]int), + blobDescriptors: bds, + blobs: bs, + calls: make(map[string]int), } } func (t *testBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { t.calls["Stat"]++ + desc, exists := t.blobDescriptors[dgst] + if exists { + return desc, nil + } + content, exists := t.blobs[dgst] if !exists { return distribution.Descriptor{}, distribution.ErrBlobUnknown diff --git a/pkg/dockerregistry/server/util.go b/pkg/dockerregistry/server/util.go index b4509a7dff1a..0e09b9df92b4 100644 --- a/pkg/dockerregistry/server/util.go +++ b/pkg/dockerregistry/server/util.go @@ -1,7 +1,6 @@ package server import ( - "encoding/json" "fmt" "os" "strings" @@ -10,18 +9,12 @@ 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" disterrors "github.com/docker/distribution/registry/api/v2" quotautil "github.com/openshift/origin/pkg/quota/util" 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" - "k8s.io/apimachinery/pkg/util/sets" - kapi "k8s.io/kubernetes/pkg/api" kapiv1 "k8s.io/kubernetes/pkg/api/v1" "github.com/openshift/origin/pkg/dockerregistry/server/client" @@ -219,172 +212,3 @@ func (g *cachedImageStreamGetter) cacheImageStream(is *imageapiv1.ImageStream) { context.GetLogger(g.ctx).Debugf("(*cachedImageStreamGetter).cacheImageStream: got image stream %s/%s", is.Namespace, is.Name) g.cachedImageStream = is } - -// imageMetadataFromManifest is used only when creating the image stream mapping -// in registry. In that case the image stream mapping contains image with the -// manifest and we have to pupulate the the docker image metadata field. -func imageMetadataFromManifest(image *imageapiv1.Image) error { - // Manifest must be set in order for this function to work as we extracting - // all metadata from the manifest. - if len(image.DockerImageManifest) == 0 { - return nil - } - - // If we already have metadata don't mutate existing metadata. - meta, ok := image.DockerImageMetadata.Object.(*imageapi.DockerImage) - hasMetadata := ok && meta.Size > 0 - if len(image.DockerImageLayers) > 0 && hasMetadata && len(image.DockerImageManifestMediaType) > 0 { - return nil - } - - manifestData := image.DockerImageManifest - - manifest := imageapi.DockerImageManifest{} - if err := json.Unmarshal([]byte(manifestData), &manifest); err != nil { - return err - } - - switch manifest.SchemaVersion { - case 0: - // legacy config object - case 1: - image.DockerImageManifestMediaType = schema1.MediaTypeManifest - - if len(manifest.History) == 0 { - // should never have an empty history, but just in case... - return nil - } - - v1Metadata := imageapi.DockerV1CompatibilityImage{} - if err := json.Unmarshal([]byte(manifest.History[0].DockerV1Compatibility), &v1Metadata); err != nil { - return err - } - - image.DockerImageLayers = make([]imageapiv1.ImageLayer, len(manifest.FSLayers)) - for i, layer := range manifest.FSLayers { - image.DockerImageLayers[i].MediaType = schema1.MediaTypeManifestLayer - image.DockerImageLayers[i].Name = layer.DockerBlobSum - } - if len(manifest.History) == len(image.DockerImageLayers) { - // This code does not work for images converted from v2 to v1, since V1Compatibility does not - // contain size information in this case. - image.DockerImageLayers[0].LayerSize = v1Metadata.Size - var size = imageapi.DockerV1CompatibilityImageSize{} - for i, obj := range manifest.History[1:] { - size.Size = 0 - if err := json.Unmarshal([]byte(obj.DockerV1Compatibility), &size); err != nil { - continue - } - image.DockerImageLayers[i+1].LayerSize = size.Size - } - } - // reverse order of the layers for v1 (lowest = 0, highest = i) - for i, j := 0, len(image.DockerImageLayers)-1; i < j; i, j = i+1, j-1 { - image.DockerImageLayers[i], image.DockerImageLayers[j] = image.DockerImageLayers[j], image.DockerImageLayers[i] - } - - dockerImage := &imageapi.DockerImage{} - - dockerImage.ID = v1Metadata.ID - dockerImage.Parent = v1Metadata.Parent - dockerImage.Comment = v1Metadata.Comment - dockerImage.Created = v1Metadata.Created - dockerImage.Container = v1Metadata.Container - dockerImage.ContainerConfig = v1Metadata.ContainerConfig - dockerImage.DockerVersion = v1Metadata.DockerVersion - dockerImage.Author = v1Metadata.Author - dockerImage.Config = v1Metadata.Config - dockerImage.Architecture = v1Metadata.Architecture - if len(image.DockerImageLayers) > 0 { - size := int64(0) - layerSet := sets.NewString() - for _, layer := range image.DockerImageLayers { - if layerSet.Has(layer.Name) { - continue - } - layerSet.Insert(layer.Name) - size += layer.LayerSize - } - dockerImage.Size = size - } else { - dockerImage.Size = v1Metadata.Size - } - - image.DockerImageMetadata.Object = dockerImage - case 2: - image.DockerImageManifestMediaType = schema2.MediaTypeManifest - - if len(image.DockerImageConfig) == 0 { - return fmt.Errorf("dockerImageConfig must not be empty for manifest schema 2") - } - config := imageapi.DockerImageConfig{} - if err := json.Unmarshal([]byte(image.DockerImageConfig), &config); err != nil { - return fmt.Errorf("failed to parse dockerImageConfig: %v", err) - } - - image.DockerImageLayers = make([]imageapiv1.ImageLayer, len(manifest.Layers)) - for i, layer := range manifest.Layers { - image.DockerImageLayers[i].Name = layer.Digest - image.DockerImageLayers[i].LayerSize = layer.Size - image.DockerImageLayers[i].MediaType = layer.MediaType - } - // reverse order of the layers for v1 (lowest = 0, highest = i) - for i, j := 0, len(image.DockerImageLayers)-1; i < j; i, j = i+1, j-1 { - image.DockerImageLayers[i], image.DockerImageLayers[j] = image.DockerImageLayers[j], image.DockerImageLayers[i] - } - dockerImage := &imageapi.DockerImage{} - - dockerImage.ID = manifest.Config.Digest - dockerImage.Parent = config.Parent - dockerImage.Comment = config.Comment - dockerImage.Created = config.Created - dockerImage.Container = config.Container - dockerImage.ContainerConfig = config.ContainerConfig - dockerImage.DockerVersion = config.DockerVersion - dockerImage.Author = config.Author - dockerImage.Config = config.Config - dockerImage.Architecture = config.Architecture - dockerImage.Size = int64(len(image.DockerImageConfig)) - - layerSet := sets.NewString(dockerImage.ID) - if len(image.DockerImageLayers) > 0 { - for _, layer := range image.DockerImageLayers { - if layerSet.Has(layer.Name) { - continue - } - layerSet.Insert(layer.Name) - dockerImage.Size += layer.LayerSize - } - } - image.DockerImageMetadata.Object = dockerImage - default: - return fmt.Errorf("unrecognized Docker image manifest schema %d for %q (%s)", manifest.SchemaVersion, image.Name, image.DockerImageReference) - } - - if image.DockerImageMetadata.Object != nil && len(image.DockerImageMetadata.Raw) == 0 { - meta, ok := image.DockerImageMetadata.Object.(*imageapi.DockerImage) - if !ok { - return fmt.Errorf("docker image metadata object is not docker image") - } - 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), meta) - if err != nil { - return err - } - image.DockerImageMetadata.Raw = data - image.DockerImageMetadataVersion = version.Version - } - - return nil -} diff --git a/pkg/image/apis/image/types.go b/pkg/image/apis/image/types.go index 91aacb2df9c3..c3e31fb4eea4 100644 --- a/pkg/image/apis/image/types.go +++ b/pkg/image/apis/image/types.go @@ -51,6 +51,10 @@ const ( // downconverted. ImporterPreferOSAnnotation = "importer.image.openshift.io/prefer-os" + // ImageManifestBlobStoredAnnotation indicates that manifest and config blobs of image are stored in on + // storage of integrated Docker registry. + ImageManifestBlobStoredAnnotation = "image.openshift.io/manifestBlobStored" + // DefaultImageTag is used when an image tag is needed and the configuration does not specify a tag to use. DefaultImageTag = "latest" diff --git a/pkg/image/registry/image/strategy.go b/pkg/image/registry/image/strategy.go index a049f80daf72..1e117f8799e8 100644 --- a/pkg/image/registry/image/strategy.go +++ b/pkg/image/registry/image/strategy.go @@ -43,6 +43,10 @@ func (s imageStrategy) PrepareForCreate(ctx apirequest.Context, obj runtime.Obje if err := util.ImageWithMetadata(newImage); err != nil { utilruntime.HandleError(fmt.Errorf("Unable to update image metadata for %q: %v", newImage.Name, err)) } + if newImage.Annotations[imageapi.ImageManifestBlobStoredAnnotation] == "true" { + newImage.DockerImageManifest = "" + newImage.DockerImageConfig = "" + } } // Validate validates a new image. @@ -116,6 +120,11 @@ func (s imageStrategy) PrepareForUpdate(ctx apirequest.Context, obj, old runtime if err = util.ImageWithMetadata(newImage); err != nil { utilruntime.HandleError(fmt.Errorf("Unable to update image metadata for %q: %v", newImage.Name, err)) } + + if newImage.Annotations[imageapi.ImageManifestBlobStoredAnnotation] == "true" { + newImage.DockerImageManifest = "" + newImage.DockerImageConfig = "" + } } // ValidateUpdate is the default update validation for an end user. diff --git a/test/extended/registry/registry.go b/test/extended/registry/registry.go index 1c8538f6defa..ede6038cbab8 100644 --- a/test/extended/registry/registry.go +++ b/test/extended/registry/registry.go @@ -1,6 +1,7 @@ package registry import ( + "fmt" "time" g "github.com/onsi/ginkgo" @@ -12,7 +13,6 @@ import ( "k8s.io/apimachinery/pkg/util/wait" imageapi "github.com/openshift/origin/pkg/image/apis/image" - regclient "github.com/openshift/origin/pkg/image/importer/dockerv1client" imagesutil "github.com/openshift/origin/test/extended/images" registryutil "github.com/openshift/origin/test/extended/registry/util" exutil "github.com/openshift/origin/test/extended/util" @@ -29,6 +29,29 @@ var _ = g.Describe("[Conformance][registry][migration] manifest migration from e defer g.GinkgoRecover() var oc = exutil.NewCLI("registry-migration", exutil.KubeConfigPath()) + var originalAcceptSchema2 *bool + + g.JustBeforeEach(func() { + if originalAcceptSchema2 == nil { + accepts, err := registryutil.DoesRegistryAcceptSchema2(oc) + o.Expect(err).NotTo(o.HaveOccurred()) + originalAcceptSchema2 = &accepts + } + + if !*originalAcceptSchema2 { + g.By("ensure the registry accepts schema 2") + err := registryutil.EnsureRegistryAcceptsSchema2(oc, true) + o.Expect(err).NotTo(o.HaveOccurred()) + } + }) + + g.AfterEach(func() { + if !*originalAcceptSchema2 { + err := registryutil.EnsureRegistryAcceptsSchema2(oc, false) + o.Expect(err).NotTo(o.HaveOccurred()) + } + }) + g.It("registry can get access to manifest [local]", func() { oc.SetOutputDir(exutil.TestContext.OutputDir) cleanUp := imagesutil.NewCleanUpContainer(oc) @@ -49,19 +72,18 @@ var _ = g.Describe("[Conformance][registry][migration] manifest migration from e o.Expect(err).NotTo(o.HaveOccurred()) cleanUp.AddImage(imageDigest, "", "") - g.By("checking that the image converted...") + g.By("checking that the image doesn't have the manifest...") image, err := oc.AsAdmin().ImageClient().Image().Images().Get(imageDigest, metav1.GetOptions{}) o.Expect(err).NotTo(o.HaveOccurred()) o.Expect(len(image.DockerImageManifest)).Should(o.Equal(0)) imageMetadataNotEmpty(image) + o.Expect(image.Annotations[imageapi.ImageManifestBlobStoredAnnotation]).To(o.Equal("true")) g.By("getting image manifest from docker-registry...") - conn, err := regclient.NewClient(10*time.Second, true).Connect(registryURL, true) - o.Expect(err).NotTo(o.HaveOccurred()) - - _, manifest, err := conn.ImageManifest(oc.Namespace(), repoName, tagName) + _, manifest, config, err := registryutil.GetManifestAndConfigByTag(oc, repoName, tagName) o.Expect(err).NotTo(o.HaveOccurred()) o.Expect(len(manifest)).Should(o.BeNumerically(">", 0)) + o.Expect(len(config)).Should(o.BeNumerically(">", 0)) g.By("restoring manifest...") image, err = oc.AsAdmin().ImageClient().Image().Images().Get(imageDigest, metav1.GetOptions{}) @@ -69,6 +91,8 @@ var _ = g.Describe("[Conformance][registry][migration] manifest migration from e imageMetadataNotEmpty(image) image.DockerImageManifest = string(manifest) + image.DockerImageConfig = string(config) + delete(image.Annotations, imageapi.ImageManifestBlobStoredAnnotation) newImage, err := oc.AsAdmin().ImageClient().Image().Images().Update(image) o.Expect(err).NotTo(o.HaveOccurred()) @@ -77,14 +101,16 @@ var _ = g.Describe("[Conformance][registry][migration] manifest migration from e g.By("checking that the manifest is present in the image...") image, err = oc.AsAdmin().ImageClient().Image().Images().Get(imageDigest, metav1.GetOptions{}) o.Expect(err).NotTo(o.HaveOccurred()) - o.Expect(len(image.DockerImageManifest)).Should(o.BeNumerically(">", 0)) o.Expect(image.DockerImageManifest).Should(o.Equal(string(manifest))) + o.Expect(image.DockerImageConfig).Should(o.Equal(string(config))) imageMetadataNotEmpty(image) + o.Expect(image.Annotations[imageapi.ImageManifestBlobStoredAnnotation]).To(o.Equal("")) g.By("getting image manifest from docker-registry one more time...") - _, manifest, err = conn.ImageManifest(oc.Namespace(), repoName, tagName) + _, newManifest, newConfig, err := registryutil.GetManifestAndConfigByTag(oc, repoName, tagName) o.Expect(err).NotTo(o.HaveOccurred()) - o.Expect(len(manifest)).Should(o.BeNumerically(">", 0)) + o.Expect(string(manifest)).Should(o.Equal(string(newManifest))) + o.Expect(string(config)).Should(o.Equal(string(newConfig))) g.By("waiting until image is updated...") err = waitForImageUpdate(oc, image) @@ -94,12 +120,15 @@ var _ = g.Describe("[Conformance][registry][migration] manifest migration from e image, err = oc.AsAdmin().ImageClient().Image().Images().Get(imageDigest, metav1.GetOptions{}) o.Expect(err).NotTo(o.HaveOccurred()) o.Expect(len(image.DockerImageManifest)).Should(o.Equal(0)) + o.Expect(len(image.DockerImageConfig)).Should(o.Equal(0)) imageMetadataNotEmpty(image) + o.Expect(image.Annotations[imageapi.ImageManifestBlobStoredAnnotation]).To(o.Equal("true")) - g.By("getting image manifest from docker-registry to check if he's available...") - _, manifest, err = conn.ImageManifest(oc.Namespace(), repoName, tagName) + g.By("getting image manifest from docker-registry to check if it's available...") + _, newManifest, newConfig, err = registryutil.GetManifestAndConfigByTag(oc, repoName, tagName) o.Expect(err).NotTo(o.HaveOccurred()) - o.Expect(len(manifest)).Should(o.BeNumerically(">", 0)) + o.Expect(string(manifest)).Should(o.Equal(string(newManifest))) + o.Expect(string(config)).Should(o.Equal(string(newConfig))) g.By("pulling image...") authCfg, err := exutil.BuildAuthConfiguration(registryURL, oc) @@ -114,7 +143,9 @@ var _ = g.Describe("[Conformance][registry][migration] manifest migration from e g.By("removing image...") err = dClient.RemoveImage(opts.Repository) - o.Expect(err).NotTo(o.HaveOccurred()) + if err != nil { + fmt.Fprintf(g.GinkgoWriter, "failed to remove image: %v\n", err) + } }) }) diff --git a/test/extended/registry/util/util.go b/test/extended/registry/util/util.go index 74f21e886898..806eb97cc0b3 100644 --- a/test/extended/registry/util/util.go +++ b/test/extended/registry/util/util.go @@ -1,21 +1,34 @@ package images import ( + "crypto/tls" "fmt" + "net/http" "regexp" "sort" "strconv" "strings" g "github.com/onsi/ginkgo" - //o "github.com/onsi/gomega" + + "github.com/docker/distribution" + "github.com/docker/distribution/context" + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/manifest/schema2" + "github.com/docker/distribution/reference" + distclient "github.com/docker/distribution/registry/client" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/auth/challenge" + "github.com/docker/distribution/registry/client/transport" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + restclient "k8s.io/client-go/rest" kapiv1 "k8s.io/kubernetes/pkg/api/v1" kcoreclient "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/core/v1" dockerregistryserver "github.com/openshift/origin/pkg/dockerregistry/server" + "github.com/openshift/origin/pkg/dockerregistry/testutil" exutil "github.com/openshift/origin/test/extended/util" ) @@ -167,3 +180,151 @@ func EnsureRegistryAcceptsSchema2(oc *exutil.CLI, accept bool) error { func makeReadonlyEnvValue(on bool) string { return fmt.Sprintf(`{"enabled":%t}`, on) } + +// GetRegistryClientRepository creates a repository interface to the integrated registry. +// If actions are not provided, only pull action will be requested. +func GetRegistryClientRepository(oc *exutil.CLI, repoName string, actions ...string) (distribution.Repository, error) { + endpoint, err := GetDockerRegistryURL(oc) + if err != nil { + return nil, err + } + repoName = completeRepoName(oc, repoName) + if len(actions) == 0 { + actions = []string{"pull"} + } + named, err := reference.ParseNamed(repoName) + if err != nil { + return nil, err + } + + token, err := oc.Run("whoami").Args("-t").Output() + if err != nil { + return nil, err + } + + creds := testutil.NewBasicCredentialStore(oc.Username(), token) + challengeManager := challenge.NewSimpleManager() + + url, versions, err := ping(challengeManager, endpoint, "") + if err != nil { + return nil, fmt.Errorf("failed to ping registry endpoint %s: %v", endpoint, err) + } + + fmt.Fprintf(g.GinkgoWriter, "pinged registry at %s, got api versions: %v\n", url, versions) + var rt http.RoundTripper + // TODO: use cluster certificate + rt, err = restclient.TransportFor(&restclient.Config{TLSClientConfig: restclient.TLSClientConfig{Insecure: true}}) + if err != nil { + return nil, err + } + rt = transport.NewTransport( + rt, + auth.NewAuthorizer( + challengeManager, + auth.NewTokenHandler(rt, creds, repoName, actions...), + auth.NewBasicHandler(creds))) + + ctx := context.Background() + repo, err := distclient.NewRepository(ctx, named, url, rt) + if err != nil { + return nil, fmt.Errorf("failed to get repository %q: %v", repoName, err) + } + + return repo, nil +} + +// GetManifestAndConfigByTag fetches manifest and corresponding config blob from the given repository:tag from +// the integrated registry. If the manifest is of schema 1, nil will be returned instead of config blob. +func GetManifestAndConfigByTag(oc *exutil.CLI, repoName, tag string) ( + manifest distribution.Manifest, + manifestBlob []byte, + configBlob []byte, + err error, +) { + repo, err := GetRegistryClientRepository(oc, repoName) + if err != nil { + return nil, nil, nil, err + } + + ctx := context.Background() + + desc, err := repo.Tags(ctx).Get(ctx, tag) + if err != nil { + return nil, nil, nil, err + } + + ms, err := repo.Manifests(ctx) + if err != nil { + return nil, nil, nil, err + } + + manifest, err = ms.Get(ctx, desc.Digest) + if err != nil { + return nil, nil, nil, err + } + + switch t := manifest.(type) { + case *schema1.SignedManifest: + manifestBlob, err = t.MarshalJSON() + if err != nil { + return nil, nil, nil, err + } + case *schema2.DeserializedManifest: + manifestBlob, err = t.MarshalJSON() + if err != nil { + return nil, nil, nil, err + } + configBlob, err = repo.Blobs(ctx).Get(ctx, t.Config.Digest) + default: + return nil, nil, nil, fmt.Errorf("got unexpected manifest type: %T", manifest) + } + if err != nil { + return nil, nil, nil, err + } + + return +} + +func completeRepoName(oc *exutil.CLI, name string) string { + parts := strings.SplitN(name, "/", 2) + if len(parts) > 1 { + return name + } + return strings.Join(append([]string{oc.Namespace()}, parts...), "/") +} + +func ping(manager challenge.Manager, endpoint, versionHeader string) ( + url string, + apiVersions []auth.APIVersion, + err error, +) { + var resp *http.Response + for _, s := range []string{"https", "http"} { + tr := &http.Transport{} + if s == "https" { + // TODO: use cluster certificate + tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + client := &http.Client{Transport: tr} + resp, err = client.Get(fmt.Sprintf("%s://%s/v2/", s, endpoint)) + if err == nil { + url = fmt.Sprintf("%s://%s", s, endpoint) + break + } + fmt.Fprintf(g.GinkgoWriter, "failed to ping registry at %s://%v: %v\n", s, endpoint, err) + } + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + if err := manager.AddResponse(resp); err != nil { + return "", nil, err + } + + if versionHeader == "" { + versionHeader = "Docker-Distribution-API-Version" + } + + return url, auth.APIVersions(resp, versionHeader), nil +}