Skip to content

Commit ad90c55

Browse files
committed
schema: Use Validators map and prepare to extend beyond JSON Schema
With image-tools split off into its own repository, the plan seems to be to keep all intra-blob JSON validation in this repository and to move all other validation (e.g. for layers or for walking Merkle trees) in image-tools [1]. All the non-validation logic currently in image/ is moving into image-tools as well [2]. Some requirements (e.g. multi-parameter checks like allowed OS/arch pairs [3]) are difficult to handle in JSON Schema but easy to handle in Go. And callers won't care if we're using JSON Schema or not; they just want to know if their blob is valid. This commit restructures intra-blob validation to ease the path going forward (although it doesn't actually change the current validation significantly). The old method: func (v Validator) Validate(src io.Reader) error is now a new Validator type: type Validator(blob io.Reader, descriptor *v1.Descriptor, strict bool) (err error) and instead of instantiating an old Validator instance: schema.MediaTypeImageConfig.Validate(reader) there's a Validators registry mapping from the media type strings to the appropriate Validator instance (which may or may not use JSON Schema under the hood). And there's a Validate function (with the same Validator interface) that looks up the appropriate entry in Validators for you so you have: schema.Validate(reader, descriptor, true) By using a Validators map, we make it easy for library consumers to register (or override) intra-blob validators for a particular type. Locations that call Validate(...) will automatically pick up the new validators without needing local changes. All of the old validation was based on JSON Schema, so currently all Validators values are ValidateJSONSchema. As the schema package grows non-JSON-Schema validation, entries will start to look like: var Validators = map[string]Validator{ v1.MediaTypeImageConfig: ValidateConfig, ... } although ValidateConfig will probably use ValidateJSONSchema internally. By passing through a descriptor, we get a chance to validate the digest and size (which we were not doing before). Digest and size validation for a byte array are also exposed directly (as ValidateByteDigest and ValidateByteSize) for use in validators that are not based on ValidateJSONSchema. Access to the digest also gives us a way to print specific error messages on failures. In situations where you don't know the blob digest, the new DigestByte will help you calculate it (for a byte array). There is also a new 'strict' parameter to distinguish between compliant images (which should only pass when strict is false) and images that only use features which the spec requires implementations to support (which should pass regardless of strict). The current JSON Schemas are not strict, and I expect we'll soon gain Go code to handle the distinction (e.g. [4]). So the presence of 'strict' in the Validator type is future-proofing our API and not exposing a currently-implemented feature. I've made the minimal sane changes to cmd/ and image/, because we're dropping them from this repository [2] (and continuing them in runtime-tools). [1]: http://ircbot.wl.linuxfoundation.org/meetings/opencontainers/2016/opencontainers.2016-10-12-21.01.log.html#l-71 [2]: opencontainers#337 [3]: https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.5 [4]: opencontainers#341 Signed-off-by: W. Trevor King <[email protected]>
1 parent d4ca161 commit ad90c55

File tree

11 files changed

+220
-88
lines changed

11 files changed

+220
-88
lines changed

cmd/oci-image-tool/autodetect.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os"
2323

2424
"github.com/opencontainers/image-spec/schema"
25+
"github.com/opencontainers/image-spec/specs-go/v1"
2526
"github.com/pkg/errors"
2627
)
2728

@@ -97,10 +98,10 @@ func autodetect(path string) (string, error) {
9798
}
9899

99100
switch {
100-
case header.MediaType == string(schema.MediaTypeManifest):
101+
case header.MediaType == v1.MediaTypeImageManifest:
101102
return typeManifest, nil
102103

103-
case header.MediaType == string(schema.MediaTypeManifestList):
104+
case header.MediaType == v1.MediaTypeImageManifestList:
104105
return typeManifestList, nil
105106

106107
case header.MediaType == "" && header.SchemaVersion == 0 && header.Config != nil:

cmd/oci-image-tool/validate.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
package main
1616

1717
import (
18+
"bytes"
1819
"fmt"
20+
"io/ioutil"
1921
"log"
2022
"os"
2123
"strings"
2224

2325
"github.com/opencontainers/image-spec/image"
2426
"github.com/opencontainers/image-spec/schema"
27+
"github.com/opencontainers/image-spec/specs-go/v1"
2528
"github.com/pkg/errors"
2629
"github.com/spf13/cobra"
2730
)
@@ -136,14 +139,35 @@ func (v *validateCmd) validatePath(name string) error {
136139
}
137140
defer f.Close()
138141

142+
blob, err := ioutil.ReadAll(f)
143+
if err != nil {
144+
return err
145+
}
146+
147+
err = f.Close()
148+
if err != nil {
149+
return err
150+
}
151+
152+
digest, err := schema.DigestByte(blob, "sha256")
153+
if err != nil {
154+
return err
155+
}
156+
157+
descriptor := v1.Descriptor{
158+
Digest: digest,
159+
Size: int64(len(blob)),
160+
}
161+
139162
switch typ {
140163
case typeManifest:
141-
return schema.MediaTypeManifest.Validate(f)
164+
descriptor.MediaType = v1.MediaTypeImageManifest
142165
case typeManifestList:
143-
return schema.MediaTypeManifestList.Validate(f)
166+
descriptor.MediaType = v1.MediaTypeImageManifestList
144167
case typeConfig:
145-
return schema.MediaTypeImageConfig.Validate(f)
168+
descriptor.MediaType = v1.MediaTypeImageConfig
146169
}
147170

148-
return fmt.Errorf("type %q unimplemented", typ)
171+
reader := bytes.NewReader(blob)
172+
return schema.Validate(reader, &descriptor, true)
149173
}

image/config.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package image
1616

1717
import (
18-
"bytes"
1918
"encoding/json"
2019
"fmt"
2120
"io"
@@ -25,7 +24,6 @@ import (
2524
"strconv"
2625
"strings"
2726

28-
"github.com/opencontainers/image-spec/schema"
2927
"github.com/opencontainers/image-spec/specs-go/v1"
3028
"github.com/opencontainers/runtime-spec/specs-go"
3129
"github.com/pkg/errors"
@@ -46,10 +44,6 @@ func findConfig(w walker, d *descriptor) (*config, error) {
4644
return errors.Wrapf(err, "%s: error reading config", path)
4745
}
4846

49-
if err := schema.MediaTypeImageConfig.Validate(bytes.NewReader(buf)); err != nil {
50-
return errors.Wrapf(err, "%s: config validation failed", path)
51-
}
52-
5347
if err := json.Unmarshal(buf, &c); err != nil {
5448
return err
5549
}

image/manifest.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package image
1616

1717
import (
1818
"archive/tar"
19-
"bytes"
2019
"compress/gzip"
2120
"encoding/json"
2221
"fmt"
@@ -27,7 +26,6 @@ import (
2726
"strings"
2827
"time"
2928

30-
"github.com/opencontainers/image-spec/schema"
3129
"github.com/opencontainers/image-spec/specs-go/v1"
3230
"github.com/pkg/errors"
3331
)
@@ -51,10 +49,6 @@ func findManifest(w walker, d *descriptor) (*manifest, error) {
5149
return errors.Wrapf(err, "%s: error reading manifest", path)
5250
}
5351

54-
if err := schema.MediaTypeManifest.Validate(bytes.NewReader(buf)); err != nil {
55-
return errors.Wrapf(err, "%s: manifest validation failed", path)
56-
}
57-
5852
if err := json.Unmarshal(buf, &m); err != nil {
5953
return err
6054
}
@@ -90,7 +84,7 @@ func (m *manifest) validate(w walker) error {
9084

9185
func (m *manifest) unpack(w walker, dest string) error {
9286
for _, d := range m.Layers {
93-
if d.MediaType != string(schema.MediaTypeImageLayer) {
87+
if d.MediaType != v1.MediaTypeImageLayer {
9488
continue
9589
}
9690

schema/config_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"testing"
2020

2121
"github.com/opencontainers/image-spec/schema"
22+
"github.com/opencontainers/image-spec/specs-go/v1"
2223
)
2324

2425
func TestConfig(t *testing.T) {
@@ -155,9 +156,19 @@ func TestConfig(t *testing.T) {
155156
fail: false,
156157
},
157158
} {
158-
r := strings.NewReader(tt.config)
159-
err := schema.MediaTypeImageConfig.Validate(r)
159+
configBytes := []byte(tt.config)
160+
digest, err := schema.DigestByte(configBytes, "sha256")
161+
if err != nil {
162+
t.Fatal(err)
163+
}
160164

165+
reader := strings.NewReader(tt.config)
166+
descriptor := v1.Descriptor{
167+
MediaType: v1.MediaTypeImageConfig,
168+
Digest: digest,
169+
Size: int64(len(configBytes)),
170+
}
171+
err = schema.Validate(reader, &descriptor, true)
161172
if got := err != nil; tt.fail != got {
162173
t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err)
163174
}

schema/descriptor.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2016 The Linux Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package schema
16+
17+
import (
18+
"crypto/sha256"
19+
"encoding/hex"
20+
"fmt"
21+
"hash"
22+
)
23+
24+
// DigestByte computes the digest of a blob using the requested
25+
// algorithm.
26+
func DigestByte(data []byte, algorithm string) (digest string, err error){
27+
var hasher hash.Hash
28+
switch algorithm {
29+
case "sha256":
30+
hasher = sha256.New()
31+
default:
32+
return "", fmt.Errorf("unrecognized algorithm: %q", algorithm)
33+
}
34+
35+
_, err = hasher.Write(data)
36+
if err != nil {
37+
return "", err
38+
}
39+
40+
hashBytes := hasher.Sum(nil)
41+
hashHex := hex.EncodeToString(hashBytes[:])
42+
return fmt.Sprintf("%s:%s", algorithm, hashHex), nil
43+
}

schema/manifest_backwards_compatibility_test.go

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
package schema_test
1616

1717
import (
18-
"crypto/sha256"
19-
"encoding/hex"
20-
"fmt"
2118
"strings"
2219
"testing"
2320

@@ -110,16 +107,14 @@ func TestBackwardsCompatibilityManifestList(t *testing.T) {
110107
fail: false,
111108
},
112109
} {
113-
sum := sha256.Sum256([]byte(tt.manifest))
114-
got := fmt.Sprintf("sha256:%s", hex.EncodeToString(sum[:]))
115-
if tt.digest != got {
116-
t.Errorf("test %d: expected digest %s but got %s", i, tt.digest, got)
117-
}
118-
119110
manifest := convertFormats(tt.manifest)
120-
r := strings.NewReader(manifest)
121-
err := schema.MediaTypeManifestList.Validate(r)
122-
111+
reader := strings.NewReader(manifest)
112+
descriptor := v1.Descriptor{
113+
MediaType: v1.MediaTypeImageManifestList,
114+
Digest: tt.digest,
115+
Size: int64(len(manifest)),
116+
}
117+
err := schema.Validate(reader, &descriptor, true)
123118
if got := err != nil; tt.fail != got {
124119
t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err)
125120
}
@@ -173,16 +168,14 @@ func TestBackwardsCompatibilityManifest(t *testing.T) {
173168
fail: false,
174169
},
175170
} {
176-
sum := sha256.Sum256([]byte(tt.manifest))
177-
got := fmt.Sprintf("sha256:%s", hex.EncodeToString(sum[:]))
178-
if tt.digest != got {
179-
t.Errorf("test %d: expected digest %s but got %s", i, tt.digest, got)
180-
}
181-
182171
manifest := convertFormats(tt.manifest)
183-
r := strings.NewReader(manifest)
184-
err := schema.MediaTypeManifest.Validate(r)
185-
172+
reader := strings.NewReader(manifest)
173+
descriptor := v1.Descriptor{
174+
MediaType: v1.MediaTypeImageManifest,
175+
Digest: tt.digest,
176+
Size: int64(len(manifest)),
177+
}
178+
err := schema.Validate(reader, &descriptor, true)
186179
if got := err != nil; tt.fail != got {
187180
t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err)
188181
}
@@ -213,16 +206,14 @@ func TestBackwardsCompatibilityConfig(t *testing.T) {
213206
fail: false,
214207
},
215208
} {
216-
sum := sha256.Sum256([]byte(tt.manifest))
217-
got := fmt.Sprintf("sha256:%s", hex.EncodeToString(sum[:]))
218-
if tt.digest != got {
219-
t.Errorf("test %d: expected digest %s but got %s", i, tt.digest, got)
220-
}
221-
222209
manifest := convertFormats(tt.manifest)
223-
r := strings.NewReader(manifest)
224-
err := schema.MediaTypeImageConfig.Validate(r)
225-
210+
reader := strings.NewReader(manifest)
211+
descriptor := v1.Descriptor{
212+
MediaType: v1.MediaTypeImageConfig,
213+
Digest: tt.digest,
214+
Size: int64(len(manifest)),
215+
}
216+
err := schema.Validate(reader, &descriptor, true)
226217
if got := err != nil; tt.fail != got {
227218
t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err)
228219
}

schema/manifest_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"testing"
2020

2121
"github.com/opencontainers/image-spec/schema"
22+
"github.com/opencontainers/image-spec/specs-go/v1"
2223
)
2324

2425
func TestManifest(t *testing.T) {
@@ -114,9 +115,19 @@ func TestManifest(t *testing.T) {
114115
fail: false,
115116
},
116117
} {
117-
r := strings.NewReader(tt.manifest)
118-
err := schema.MediaTypeManifest.Validate(r)
118+
manifestBytes := []byte(tt.manifest)
119+
digest, err := schema.DigestByte(manifestBytes, "sha256")
120+
if err != nil {
121+
t.Fatal(err)
122+
}
119123

124+
reader := strings.NewReader(tt.manifest)
125+
descriptor := v1.Descriptor{
126+
MediaType: v1.MediaTypeImageManifest,
127+
Digest: digest,
128+
Size: int64(len(manifestBytes)),
129+
}
130+
err = schema.Validate(reader, &descriptor, true)
120131
if got := err != nil; tt.fail != got {
121132
t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err)
122133
}

schema/schema.go

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,17 @@ import (
2020
"github.com/opencontainers/image-spec/specs-go/v1"
2121
)
2222

23-
// Media types for the OCI image formats
24-
const (
25-
MediaTypeDescriptor Validator = v1.MediaTypeDescriptor
26-
MediaTypeManifest Validator = v1.MediaTypeImageManifest
27-
MediaTypeManifestList Validator = v1.MediaTypeImageManifestList
28-
MediaTypeImageConfig Validator = v1.MediaTypeImageConfig
29-
MediaTypeImageLayer unimplemented = v1.MediaTypeImageLayer
30-
)
31-
3223
var (
3324
// fs stores the embedded http.FileSystem
3425
// having the OCI JSON schema files in root "/".
3526
fs = _escFS(false)
3627

37-
// specs maps OCI schema media types to schema files.
38-
specs = map[Validator]string{
39-
MediaTypeDescriptor: "content-descriptor.json",
40-
MediaTypeManifest: "image-manifest-schema.json",
41-
MediaTypeManifestList: "manifest-list-schema.json",
42-
MediaTypeImageConfig: "config-schema.json",
28+
// Schemas maps OCI media types to JSON Schema files.
29+
Schemas = map[string]string{
30+
v1.MediaTypeDescriptor: "content-descriptor.json",
31+
v1.MediaTypeImageManifest: "image-manifest-schema.json",
32+
v1.MediaTypeImageManifestList: "manifest-list-schema.json",
33+
v1.MediaTypeImageConfig: "config-schema.json",
4334
}
4435
)
4536

schema/spec_test.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"testing"
2626

2727
"github.com/opencontainers/image-spec/schema"
28+
"github.com/opencontainers/image-spec/specs-go/v1"
2829
"github.com/pkg/errors"
2930
"github.com/russross/blackfriday"
3031
)
@@ -73,7 +74,19 @@ func validate(t *testing.T, name string) {
7374
continue
7475
}
7576

76-
err = schema.Validator(example.Mediatype).Validate(strings.NewReader(example.Body))
77+
bodyBytes := []byte(example.Body)
78+
digest, err := schema.DigestByte(bodyBytes, "sha256")
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
83+
reader := strings.NewReader(example.Body)
84+
descriptor := v1.Descriptor{
85+
MediaType: example.Mediatype,
86+
Digest: digest,
87+
Size: int64(len(bodyBytes)),
88+
}
89+
err = schema.Validate(reader, &descriptor, true)
7790
if err == nil {
7891
printFields(t, "ok", example.Mediatype, example.Title)
7992
t.Log(example.Body, "---")

0 commit comments

Comments
 (0)