Skip to content

Commit 6e940f8

Browse files
committed
validate: Check configuration against JSON Schema
runtime-spec publishes a JSON Schema covering the configuration format (and other JSON related to runtime-spec) [1]. Reduce duplication of effort by validating configurations against that schema. For example this gives us lots allowed value/type checking: $ cat config.json { "ociVersion": "1.0.0-rc6", "process": { "cwd": "/", "args": [ "sh" ], "user": { "uid": 1, "gid": 1 }, "rlimits": [{}] }, "root": { "path": "rootfs" } } $ ./oci-runtime-tool validate 3 Errors detected: process.rlimits.0.type: Does not match pattern '^RLIMIT_[A-Z]+$' 'POSIXRlimit.Type' should not be empty. rlimit type "" is invalid without us having to duplicate all the work that the runtime-spec folks have already done for us. Only validating the JSON Schema is not sufficient, because --host-specific (e.g. you're running on a Linux box) and cross-property constraits (e.g. must create a new UTS namespace if you set hostname) are difficult/impossible to express in JSON Schema. The 1.0.0-rc5 test is an example of pulling in JSON Schema from an older release, since the 'process' property was required in rc5 and optional in rc6, with opencontainers/runtime-spec@c41ea83d, config: Make process optional, 2017-02-27, #701) landing in between. [1]: https://github.com/opencontainers/runtime-spec/tree/v1.0.0-rc2/schema Signed-off-by: W. Trevor King <[email protected]>
1 parent d09d81a commit 6e940f8

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

validate/validate.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/syndtr/gocapability/capability"
2525

2626
"github.com/opencontainers/runtime-tools/specerror"
27+
"github.com/xeipuuv/gojsonschema"
2728
)
2829

2930
const specConfig = "config.json"
@@ -47,6 +48,8 @@ var (
4748
"RLIMIT_SIGPENDING",
4849
"RLIMIT_STACK",
4950
}
51+
52+
configSchemaTemplate = "https://raw.githubusercontent.com/opencontainers/runtime-spec/v%s/schema/config-schema.json"
5053
)
5154

5255
// Validator represents a validator for runtime bundle
@@ -102,6 +105,7 @@ func NewValidatorFromPath(bundlePath string, hostSpecific bool, platform string)
102105
// CheckAll checks all parts of runtime bundle
103106
func (v *Validator) CheckAll() error {
104107
var errs *multierror.Error
108+
errs = multierror.Append(errs, v.CheckJSONSchema())
105109
errs = multierror.Append(errs, v.CheckPlatform())
106110
errs = multierror.Append(errs, v.CheckRoot())
107111
errs = multierror.Append(errs, v.CheckMandatoryFields())
@@ -114,6 +118,49 @@ func (v *Validator) CheckAll() error {
114118
return errs.ErrorOrNil()
115119
}
116120

121+
// JSONSchemaURL returns the URL for the JSON Schema specifying the
122+
// configuration format. It consumes configSchemaTemplate, but we
123+
// provide it as a function to isolate consumers from inconsistent
124+
// naming as runtime-spec evolves.
125+
func JSONSchemaURL(version string) (url string, err error) {
126+
ver, err := semver.Parse(version)
127+
if err != nil {
128+
return "", err
129+
}
130+
configRenamedToConfigSchemaVersion, err := semver.Parse("1.0.0-rc2") // config.json became config-schema.json in 1.0.0-rc2
131+
if ver.Compare(configRenamedToConfigSchemaVersion) == -1 {
132+
return "", fmt.Errorf("unsupported configuration version (older than %s)", configRenamedToConfigSchemaVersion)
133+
}
134+
return fmt.Sprintf(configSchemaTemplate, version), nil
135+
}
136+
137+
// CheckJSONSchema validates the configuration against the
138+
// runtime-spec JSON Schema, using the version of the schema that
139+
// matches the configuration's declared version.
140+
func (v *Validator) CheckJSONSchema() (errs error) {
141+
url, err := JSONSchemaURL(v.spec.Version)
142+
if err != nil {
143+
errs = multierror.Append(errs, err)
144+
return errs
145+
}
146+
147+
schemaLoader := gojsonschema.NewReferenceLoader(url)
148+
documentLoader := gojsonschema.NewGoLoader(v.spec)
149+
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
150+
if err != nil {
151+
errs = multierror.Append(errs, err)
152+
return errs
153+
}
154+
155+
if !result.Valid() {
156+
for _, resultError := range result.Errors() {
157+
errs = multierror.Append(errs, errors.New(resultError.String()))
158+
}
159+
}
160+
161+
return errs
162+
}
163+
117164
// CheckRoot checks status of v.spec.Root
118165
func (v *Validator) CheckRoot() (errs error) {
119166
logrus.Debugf("check root")

validate/validate_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"runtime"
99
"testing"
1010

11+
"github.com/hashicorp/go-multierror"
1112
rspec "github.com/opencontainers/runtime-spec/specs-go"
1213
"github.com/stretchr/testify/assert"
1314

@@ -32,6 +33,62 @@ func TestNewValidator(t *testing.T) {
3233
}
3334
}
3435

36+
func TestJSONSchema(t *testing.T) {
37+
for _, tt := range []struct {
38+
config *rspec.Spec
39+
error string
40+
}{
41+
{
42+
config: &rspec.Spec{},
43+
error: "Version string empty",
44+
},
45+
{
46+
config: &rspec.Spec{
47+
Version: "1.0.1-rc1",
48+
},
49+
error: "Could not read schema from HTTP, response status is 404 Not Found",
50+
},
51+
{
52+
config: &rspec.Spec{
53+
Version: "1.0.0",
54+
},
55+
error: "",
56+
},
57+
{
58+
config: &rspec.Spec{
59+
Version: "1.0.0",
60+
Process: &rspec.Process{},
61+
},
62+
error: "process.args: Invalid type. Expected: array, given: null",
63+
},
64+
{
65+
config: &rspec.Spec{
66+
Version: "1.0.0-rc5",
67+
},
68+
error: "process: process is required",
69+
},
70+
} {
71+
t.Run(tt.error, func(t *testing.T) {
72+
v := &Validator{spec: tt.config}
73+
errs := v.CheckJSONSchema()
74+
if tt.error == "" {
75+
assert.Equal(t, nil, errs)
76+
return
77+
}
78+
merr, ok := errs.(*multierror.Error)
79+
if !ok {
80+
t.Fatalf("non-multierror returned by CheckJSONSchema: %s", errs.Error())
81+
}
82+
for _, err := range merr.Errors {
83+
if err.Error() == tt.error {
84+
return
85+
}
86+
}
87+
assert.Equal(t, tt.error, errs.Error())
88+
})
89+
}
90+
}
91+
3592
func TestCheckRoot(t *testing.T) {
3693
tmpBundle, err := ioutil.TempDir("", "oci-check-rootfspath")
3794
if err != nil {

0 commit comments

Comments
 (0)