Skip to content

Commit 321d636

Browse files
committed
Merge pull request #1 from owtaylor/oci-media-types
Handle OCI manifests and image indexes without a media type Signed-off-by: Mike Brown <brownwm@us.ibm.com>
2 parents f6224f7 + 132abc6 commit 321d636

9 files changed

Lines changed: 411 additions & 26 deletions

File tree

manifest/manifestlist/manifestlist.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ func init() {
3838
return nil, distribution.Descriptor{}, err
3939
}
4040

41+
if m.MediaType != MediaTypeManifestList {
42+
err = fmt.Errorf("mediaType in manifest list should be '%s' not '%s'",
43+
MediaTypeManifestList, m.MediaType)
44+
45+
return nil, distribution.Descriptor{}, err
46+
}
47+
4148
dgst := digest.FromBytes(b)
4249
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifestList}, err
4350
}
@@ -53,6 +60,13 @@ func init() {
5360
return nil, distribution.Descriptor{}, err
5461
}
5562

63+
if m.MediaType != "" && m.MediaType != v1.MediaTypeImageIndex {
64+
err = fmt.Errorf("if present, mediaType in image index should be '%s' not '%s'",
65+
v1.MediaTypeImageIndex, m.MediaType)
66+
67+
return nil, distribution.Descriptor{}, err
68+
}
69+
5670
dgst := digest.FromBytes(b)
5771
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeImageIndex}, err
5872
}
@@ -130,15 +144,23 @@ type DeserializedManifestList struct {
130144
// DeserializedManifestList which contains the resulting manifest list
131145
// and its JSON representation.
132146
func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) {
133-
var m ManifestList
147+
var mediaType string
134148
if len(descriptors) > 0 && descriptors[0].Descriptor.MediaType == v1.MediaTypeImageManifest {
135-
m = ManifestList{
136-
Versioned: OCISchemaVersion,
137-
}
149+
mediaType = v1.MediaTypeImageIndex
138150
} else {
139-
m = ManifestList{
140-
Versioned: SchemaVersion,
141-
}
151+
mediaType = MediaTypeManifestList
152+
}
153+
154+
return FromDescriptorsWithMediaType(descriptors, mediaType)
155+
}
156+
157+
// For testing purposes, it's useful to be able to specify the media type explicitly
158+
func FromDescriptorsWithMediaType(descriptors []ManifestDescriptor, mediaType string) (*DeserializedManifestList, error) {
159+
m := ManifestList{
160+
Versioned: manifest.Versioned{
161+
SchemaVersion: 2,
162+
MediaType: mediaType,
163+
},
142164
}
143165

144166
m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors))
@@ -183,5 +205,12 @@ func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) {
183205
// Payload returns the raw content of the manifest list. The contents can be
184206
// used to calculate the content identifier.
185207
func (m DeserializedManifestList) Payload() (string, []byte, error) {
186-
return m.MediaType, m.canonical, nil
208+
var mediaType string
209+
if m.MediaType == "" {
210+
mediaType = v1.MediaTypeImageIndex
211+
} else {
212+
mediaType = m.MediaType
213+
}
214+
215+
return mediaType, m.canonical, nil
187216
}

manifest/manifestlist/manifestlist_test.go

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ var expectedManifestListSerialization = []byte(`{
3838
]
3939
}`)
4040

41-
func TestManifestList(t *testing.T) {
41+
func makeTestManifestList(t *testing.T, mediaType string) ([]ManifestDescriptor, *DeserializedManifestList) {
4242
manifestDescriptors := []ManifestDescriptor{
4343
{
4444
Descriptor: distribution.Descriptor{
@@ -65,11 +65,16 @@ func TestManifestList(t *testing.T) {
6565
},
6666
}
6767

68-
deserialized, err := FromDescriptors(manifestDescriptors)
68+
deserialized, err := FromDescriptorsWithMediaType(manifestDescriptors, mediaType)
6969
if err != nil {
7070
t.Fatalf("error creating DeserializedManifestList: %v", err)
7171
}
7272

73+
return manifestDescriptors, deserialized
74+
}
75+
76+
func TestManifestList(t *testing.T) {
77+
manifestDescriptors, deserialized := makeTestManifestList(t, MediaTypeManifestList)
7378
mediaType, canonical, _ := deserialized.Payload()
7479

7580
if mediaType != MediaTypeManifestList {
@@ -160,7 +165,7 @@ var expectedOCIImageIndexSerialization = []byte(`{
160165
]
161166
}`)
162167

163-
func TestOCIImageIndex(t *testing.T) {
168+
func makeTestOCIImageIndex(t *testing.T, mediaType string) ([]ManifestDescriptor, *DeserializedManifestList) {
164169
manifestDescriptors := []ManifestDescriptor{
165170
{
166171
Descriptor: distribution.Descriptor{
@@ -196,11 +201,17 @@ func TestOCIImageIndex(t *testing.T) {
196201
},
197202
}
198203

199-
deserialized, err := FromDescriptors(manifestDescriptors)
204+
deserialized, err := FromDescriptorsWithMediaType(manifestDescriptors, mediaType)
200205
if err != nil {
201206
t.Fatalf("error creating DeserializedManifestList: %v", err)
202207
}
203208

209+
return manifestDescriptors, deserialized
210+
}
211+
212+
func TestOCIImageIndex(t *testing.T) {
213+
manifestDescriptors, deserialized := makeTestOCIImageIndex(t, v1.MediaTypeImageIndex)
214+
204215
mediaType, canonical, _ := deserialized.Payload()
205216

206217
if mediaType != v1.MediaTypeImageIndex {
@@ -241,3 +252,54 @@ func TestOCIImageIndex(t *testing.T) {
241252
}
242253
}
243254
}
255+
256+
func mediaTypeTest(t *testing.T, contentType string, mediaType string, shouldError bool) {
257+
var m *DeserializedManifestList
258+
if contentType == MediaTypeManifestList {
259+
_, m = makeTestManifestList(t, mediaType)
260+
} else {
261+
_, m = makeTestOCIImageIndex(t, mediaType)
262+
}
263+
264+
_, canonical, err := m.Payload()
265+
if err != nil {
266+
t.Fatalf("error getting payload, %v", err)
267+
}
268+
269+
unmarshalled, descriptor, err := distribution.UnmarshalManifest(
270+
contentType,
271+
canonical)
272+
273+
if shouldError {
274+
if err == nil {
275+
t.Fatalf("bad content type should have produced error")
276+
}
277+
} else {
278+
if err != nil {
279+
t.Fatalf("error unmarshaling manifest, %v", err)
280+
}
281+
282+
asManifest := unmarshalled.(*DeserializedManifestList)
283+
if asManifest.MediaType != mediaType {
284+
t.Fatalf("Bad media type '%v' as unmarshalled", asManifest.MediaType)
285+
}
286+
287+
if descriptor.MediaType != contentType {
288+
t.Fatalf("Bad media type '%v' for descriptor", descriptor.MediaType)
289+
}
290+
291+
unmarshalledMediaType, _, _ := unmarshalled.Payload()
292+
if unmarshalledMediaType != contentType {
293+
t.Fatalf("Bad media type '%v' for payload", unmarshalledMediaType)
294+
}
295+
}
296+
}
297+
298+
func TestMediaTypes(t *testing.T) {
299+
mediaTypeTest(t, MediaTypeManifestList, "", true)
300+
mediaTypeTest(t, MediaTypeManifestList, MediaTypeManifestList, false)
301+
mediaTypeTest(t, MediaTypeManifestList, MediaTypeManifestList+"XXX", true)
302+
mediaTypeTest(t, v1.MediaTypeImageIndex, "", false)
303+
mediaTypeTest(t, v1.MediaTypeImageIndex, v1.MediaTypeImageIndex, false)
304+
mediaTypeTest(t, v1.MediaTypeImageIndex, v1.MediaTypeImageIndex+"XXX", true)
305+
}

manifest/ocischema/builder.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import (
44
"context"
55

66
"github.com/docker/distribution"
7+
"github.com/docker/distribution/manifest"
78
"github.com/opencontainers/go-digest"
89
"github.com/opencontainers/image-spec/specs-go/v1"
910
)
1011

1112
// builder is a type for constructing manifests.
12-
type builder struct {
13+
type Builder struct {
1314
// bs is a BlobService used to publish the configuration blob.
1415
bs distribution.BlobService
1516

@@ -22,26 +23,43 @@ type builder struct {
2223

2324
// Annotations contains arbitrary metadata relating to the targeted content.
2425
annotations map[string]string
26+
27+
// For testing purposes
28+
mediaType string
2529
}
2630

2731
// NewManifestBuilder is used to build new manifests for the current schema
2832
// version. It takes a BlobService so it can publish the configuration blob
2933
// as part of the Build process, and annotations.
3034
func NewManifestBuilder(bs distribution.BlobService, configJSON []byte, annotations map[string]string) distribution.ManifestBuilder {
31-
mb := &builder{
35+
mb := &Builder{
3236
bs: bs,
3337
configJSON: make([]byte, len(configJSON)),
3438
annotations: annotations,
39+
mediaType: v1.MediaTypeImageManifest,
3540
}
3641
copy(mb.configJSON, configJSON)
3742

3843
return mb
3944
}
4045

46+
// For testing purposes, we want to be able to create an OCI image with
47+
// either an MediaType either empty, or with the OCI image value
48+
func (mb *Builder) SetMediaType(mediaType string) {
49+
if mediaType != "" && mediaType != v1.MediaTypeImageManifest {
50+
panic("Invalid media type for OCI image manifest")
51+
}
52+
53+
mb.mediaType = mediaType
54+
}
55+
4156
// Build produces a final manifest from the given references.
42-
func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
57+
func (mb *Builder) Build(ctx context.Context) (distribution.Manifest, error) {
4358
m := Manifest{
44-
Versioned: SchemaVersion,
59+
Versioned: manifest.Versioned{
60+
SchemaVersion: 2,
61+
MediaType: mb.mediaType,
62+
},
4563
Layers: make([]distribution.Descriptor, len(mb.layers)),
4664
Annotations: mb.annotations,
4765
}
@@ -76,12 +94,12 @@ func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
7694
}
7795

7896
// AppendReference adds a reference to the current ManifestBuilder.
79-
func (mb *builder) AppendReference(d distribution.Describable) error {
97+
func (mb *Builder) AppendReference(d distribution.Describable) error {
8098
mb.layers = append(mb.layers, d.Descriptor())
8199
return nil
82100
}
83101

84102
// References returns the current references added to this builder.
85-
func (mb *builder) References() []distribution.Descriptor {
103+
func (mb *Builder) References() []distribution.Descriptor {
86104
return mb.layers
87105
}

manifest/ocischema/manifest.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
9797
return err
9898
}
9999

100+
if manifest.MediaType != "" && manifest.MediaType != v1.MediaTypeImageManifest {
101+
return fmt.Errorf("if present, mediaType in manifest should be '%s' not '%s'",
102+
v1.MediaTypeImageManifest, manifest.MediaType)
103+
}
104+
100105
m.Manifest = manifest
101106

102107
return nil
@@ -115,5 +120,5 @@ func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
115120
// Payload returns the raw content of the manifest. The contents can be used to
116121
// calculate the content identifier.
117122
func (m DeserializedManifest) Payload() (string, []byte, error) {
118-
return m.MediaType, m.canonical, nil
123+
return v1.MediaTypeImageManifest, m.canonical, nil
119124
}

manifest/ocischema/manifest_test.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/docker/distribution"
10+
"github.com/docker/distribution/manifest"
1011
"github.com/opencontainers/image-spec/specs-go/v1"
1112
)
1213

@@ -36,9 +37,12 @@ var expectedManifestSerialization = []byte(`{
3637
}
3738
}`)
3839

39-
func TestManifest(t *testing.T) {
40-
manifest := Manifest{
41-
Versioned: SchemaVersion,
40+
func makeTestManifest(mediaType string) Manifest {
41+
return Manifest{
42+
Versioned: manifest.Versioned{
43+
SchemaVersion: 2,
44+
MediaType: mediaType,
45+
},
4246
Config: distribution.Descriptor{
4347
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
4448
Size: 985,
@@ -55,6 +59,10 @@ func TestManifest(t *testing.T) {
5559
},
5660
Annotations: map[string]string{"hot": "potato"},
5761
}
62+
}
63+
64+
func TestManifest(t *testing.T) {
65+
manifest := makeTestManifest(v1.MediaTypeImageManifest)
5866

5967
deserialized, err := FromStruct(manifest)
6068
if err != nil {
@@ -131,3 +139,46 @@ func TestManifest(t *testing.T) {
131139
t.Fatalf("unexpected annotation in reference: %s", references[1].Annotations["lettuce"])
132140
}
133141
}
142+
143+
func mediaTypeTest(t *testing.T, mediaType string, shouldError bool) {
144+
manifest := makeTestManifest(mediaType)
145+
146+
deserialized, err := FromStruct(manifest)
147+
if err != nil {
148+
t.Fatalf("error creating DeserializedManifest: %v", err)
149+
}
150+
151+
unmarshalled, descriptor, err := distribution.UnmarshalManifest(
152+
v1.MediaTypeImageManifest,
153+
deserialized.canonical)
154+
155+
if shouldError {
156+
if err == nil {
157+
t.Fatalf("bad content type should have produced error")
158+
}
159+
} else {
160+
if err != nil {
161+
t.Fatalf("error unmarshaling manifest, %v", err)
162+
}
163+
164+
asManifest := unmarshalled.(*DeserializedManifest)
165+
if asManifest.MediaType != mediaType {
166+
t.Fatalf("Bad media type '%v' as unmarshalled", asManifest.MediaType)
167+
}
168+
169+
if descriptor.MediaType != v1.MediaTypeImageManifest {
170+
t.Fatalf("Bad media type '%v' for descriptor", descriptor.MediaType)
171+
}
172+
173+
unmarshalledMediaType, _, _ := unmarshalled.Payload()
174+
if unmarshalledMediaType != v1.MediaTypeImageManifest {
175+
t.Fatalf("Bad media type '%v' for payload", unmarshalledMediaType)
176+
}
177+
}
178+
}
179+
180+
func TestMediaTypes(t *testing.T) {
181+
mediaTypeTest(t, "", false)
182+
mediaTypeTest(t, v1.MediaTypeImageManifest, false)
183+
mediaTypeTest(t, v1.MediaTypeImageManifest+"XXX", true)
184+
}

manifest/schema2/manifest.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
116116
return err
117117
}
118118

119+
if manifest.MediaType != MediaTypeManifest {
120+
return fmt.Errorf("mediaType in manifest should be '%s' not '%s'",
121+
MediaTypeManifest, manifest.MediaType)
122+
123+
}
124+
119125
m.Manifest = manifest
120126

121127
return nil

0 commit comments

Comments
 (0)