Skip to content

Commit

Permalink
validate: Check configuration against JSON Schema
Browse files Browse the repository at this point in the history
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, opencontainers#701) landing in between.

[1]: https://github.com/opencontainers/runtime-spec/tree/v1.0.0-rc2/schema

Signed-off-by: W. Trevor King <wking@tremily.us>
  • Loading branch information
wking committed Sep 11, 2017
1 parent d09d81a commit 6e940f8
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 0 deletions.
47 changes: 47 additions & 0 deletions validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/syndtr/gocapability/capability"

"github.com/opencontainers/runtime-tools/specerror"
"github.com/xeipuuv/gojsonschema"
)

const specConfig = "config.json"
Expand All @@ -47,6 +48,8 @@ var (
"RLIMIT_SIGPENDING",
"RLIMIT_STACK",
}

configSchemaTemplate = "https://raw.githubusercontent.com/opencontainers/runtime-spec/v%s/schema/config-schema.json"
)

// Validator represents a validator for runtime bundle
Expand Down Expand Up @@ -102,6 +105,7 @@ func NewValidatorFromPath(bundlePath string, hostSpecific bool, platform string)
// CheckAll checks all parts of runtime bundle
func (v *Validator) CheckAll() error {
var errs *multierror.Error
errs = multierror.Append(errs, v.CheckJSONSchema())
errs = multierror.Append(errs, v.CheckPlatform())
errs = multierror.Append(errs, v.CheckRoot())
errs = multierror.Append(errs, v.CheckMandatoryFields())
Expand All @@ -114,6 +118,49 @@ func (v *Validator) CheckAll() error {
return errs.ErrorOrNil()
}

// JSONSchemaURL returns the URL for the JSON Schema specifying the
// configuration format. It consumes configSchemaTemplate, but we
// provide it as a function to isolate consumers from inconsistent
// naming as runtime-spec evolves.
func JSONSchemaURL(version string) (url string, err error) {
ver, err := semver.Parse(version)
if err != nil {
return "", err
}
configRenamedToConfigSchemaVersion, err := semver.Parse("1.0.0-rc2") // config.json became config-schema.json in 1.0.0-rc2
if ver.Compare(configRenamedToConfigSchemaVersion) == -1 {
return "", fmt.Errorf("unsupported configuration version (older than %s)", configRenamedToConfigSchemaVersion)
}
return fmt.Sprintf(configSchemaTemplate, version), nil
}

// CheckJSONSchema validates the configuration against the
// runtime-spec JSON Schema, using the version of the schema that
// matches the configuration's declared version.
func (v *Validator) CheckJSONSchema() (errs error) {
url, err := JSONSchemaURL(v.spec.Version)
if err != nil {
errs = multierror.Append(errs, err)
return errs
}

schemaLoader := gojsonschema.NewReferenceLoader(url)
documentLoader := gojsonschema.NewGoLoader(v.spec)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
errs = multierror.Append(errs, err)
return errs
}

if !result.Valid() {
for _, resultError := range result.Errors() {
errs = multierror.Append(errs, errors.New(resultError.String()))
}
}

return errs
}

// CheckRoot checks status of v.spec.Root
func (v *Validator) CheckRoot() (errs error) {
logrus.Debugf("check root")
Expand Down
57 changes: 57 additions & 0 deletions validate/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"runtime"
"testing"

"github.com/hashicorp/go-multierror"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"

Expand All @@ -32,6 +33,62 @@ func TestNewValidator(t *testing.T) {
}
}

func TestJSONSchema(t *testing.T) {
for _, tt := range []struct {
config *rspec.Spec
error string
}{
{
config: &rspec.Spec{},
error: "Version string empty",
},
{
config: &rspec.Spec{
Version: "1.0.1-rc1",
},
error: "Could not read schema from HTTP, response status is 404 Not Found",
},
{
config: &rspec.Spec{
Version: "1.0.0",
},
error: "",
},
{
config: &rspec.Spec{
Version: "1.0.0",
Process: &rspec.Process{},
},
error: "process.args: Invalid type. Expected: array, given: null",
},
{
config: &rspec.Spec{
Version: "1.0.0-rc5",
},
error: "process: process is required",
},
} {
t.Run(tt.error, func(t *testing.T) {
v := &Validator{spec: tt.config}
errs := v.CheckJSONSchema()
if tt.error == "" {
assert.Equal(t, nil, errs)
return
}
merr, ok := errs.(*multierror.Error)
if !ok {
t.Fatalf("non-multierror returned by CheckJSONSchema: %s", errs.Error())
}
for _, err := range merr.Errors {
if err.Error() == tt.error {
return
}
}
assert.Equal(t, tt.error, errs.Error())
})
}
}

func TestCheckRoot(t *testing.T) {
tmpBundle, err := ioutil.TempDir("", "oci-check-rootfspath")
if err != nil {
Expand Down

0 comments on commit 6e940f8

Please sign in to comment.