Skip to content
This repository has been archived by the owner on Nov 8, 2022. It is now read-only.

Commit

Permalink
adds schema validation to the config file parsing process
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom McSweeney committed May 4, 2016
1 parent 636b9fd commit 1e44f11
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 22 deletions.
12 changes: 12 additions & 0 deletions Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions control/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,40 @@ type Config struct {
Plugins *pluginConfig `json:"plugins,omitempty"yaml:"plugins,omitempty"`
}

const (
CONFIG_CONSTRAINTS = `
"control" : {
"type": ["object", "null"],
"properties": {
"plugin_trust_level": {
"type": "integer",
"minimum": 0,
"maximum": 2
},
"auto_discover_path": {
"type": "string"
},
"cache_expiration": {
"type": "string"
},
"max_running_plugins": {
"type": "integer",
"minimum": 1
},
"keyring_paths" : {
"type": "string"
},
"plugins": {
"type": ["object", "null"],
"properties" : {},
"additionalProperties": true
}
},
"additionalProperties": false
}
`
)

// get the default snapd configuration
func GetDefaultConfig() *Config {
return &Config{
Expand Down
24 changes: 21 additions & 3 deletions control/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ import (
. "github.com/smartystreets/goconvey/convey"
)

const (
MOCK_CONSTRAINTS = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "snapd global config schema",
"type": ["object", "null"],
"properties": {
"control": { "$ref": "#/definitions/control" },
"scheduler": { "$ref": "#/definitions/scheduler"},
"restapi" : { "$ref": "#/definitions/restapi"},
"tribe": { "$ref": "#/definitions/tribe"}
},
"additionalProperties": true,
"definitions": { ` +
CONFIG_CONSTRAINTS + `,` + `"scheduler": {}, "restapi": {}, "tribe":{}` +
`}` +
`}`
)

type mockConfig struct {
Control *Config
}
Expand Down Expand Up @@ -68,7 +86,7 @@ func TestPluginConfig(t *testing.T) {
Control: GetDefaultConfig(),
}
path := "../examples/configs/snap-config-sample.json"
err := cfgfile.Read(path, &config)
err := cfgfile.Read(path, &config, MOCK_CONSTRAINTS)
So(err, ShouldBeNil)
cfg := config.Control
So(cfg.Plugins, ShouldNotBeNil)
Expand Down Expand Up @@ -124,7 +142,7 @@ func TestControlConfigJSON(t *testing.T) {
Control: GetDefaultConfig(),
}
path := "../examples/configs/snap-config-sample.json"
err := cfgfile.Read(path, &config)
err := cfgfile.Read(path, &config, MOCK_CONSTRAINTS)
var cfg *Config
if err == nil {
cfg = config.Control
Expand Down Expand Up @@ -187,7 +205,7 @@ func TestControlConfigYaml(t *testing.T) {
Control: GetDefaultConfig(),
}
path := "../examples/configs/snap-config-sample.yaml"
err := cfgfile.Read(path, &config)
err := cfgfile.Read(path, &config, MOCK_CONSTRAINTS)
var cfg *Config
if err == nil {
cfg = config.Control
Expand Down
20 changes: 19 additions & 1 deletion mgmt/rest/rest_func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ var (
UploadCount = 0
)

const (
MOCK_CONSTRAINTS = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "snapd global config schema",
"type": ["object", "null"],
"properties": {
"control": { "$ref": "#/definitions/control" },
"scheduler": { "$ref": "#/definitions/scheduler"},
"restapi" : { "$ref": "#/definitions/restapi"},
"tribe": { "$ref": "#/definitions/tribe"}
},
"additionalProperties": true,
"definitions": { ` +
`"control": {}, "scheduler": {}, ` + CONFIG_CONSTRAINTS + `, "tribe":{}` +
`}` +
`}`
)

type restAPIInstance struct {
port int
server *Server
Expand Down Expand Up @@ -560,7 +578,7 @@ func TestPluginRestCalls(t *testing.T) {
})
Convey("Plugin config is set at startup", func() {
cfg := getDefaultMockConfig()
err := cfgfile.Read("../../examples/configs/snap-config-sample.json", &cfg)
err := cfgfile.Read("../../examples/configs/snap-config-sample.json", &cfg, MOCK_CONSTRAINTS)
So(err, ShouldBeNil)
r := startAPI(cfg)
port := r.port
Expand Down
34 changes: 34 additions & 0 deletions mgmt/rest/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,40 @@ type Config struct {
RestAuthPassword string `json:"rest_auth_password,omitempty"yaml:"rest_auth_password,omitempty"`
}

const (
CONFIG_CONSTRAINTS = `
"restapi" : {
"type": ["object", "null"],
"properties" : {
"enable": {
"type": "boolean"
},
"https" : {
"type": "boolean"
},
"rest_auth": {
"type": "boolean"
},
"rest_auth_password": {
"type": "string"
},
"rest_certificate": {
"type": "string"
},
"rest_key" : {
"type": "string"
},
"port" : {
"type": "integer",
"minimum": 0,
"maximum": 65535
}
},
"additionalProperties": false
}
`
)

type managesMetrics interface {
MetricCatalog() ([]core.CatalogedMetric, error)
FetchMetrics(core.Namespace, int) ([]core.CatalogedMetric, error)
Expand Down
4 changes: 2 additions & 2 deletions mgmt/rest/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestRestAPIConfigJSON(t *testing.T) {
RestAPI: GetDefaultConfig(),
}
path := "../../examples/configs/snap-config-sample.json"
err := cfgfile.Read(path, &config)
err := cfgfile.Read(path, &config, MOCK_CONSTRAINTS)
var cfg *Config
if err == nil {
cfg = config.RestAPI
Expand Down Expand Up @@ -74,7 +74,7 @@ func TestRestAPIConfigYaml(t *testing.T) {
RestAPI: GetDefaultConfig(),
}
path := "../../examples/configs/snap-config-sample.yaml"
err := cfgfile.Read(path, &config)
err := cfgfile.Read(path, &config, MOCK_CONSTRAINTS)
var cfg *Config
if err == nil {
cfg = config.RestAPI
Expand Down
28 changes: 28 additions & 0 deletions mgmt/tribe/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ type Config struct {
RestAPIInsecureSkipVerify string `json:"-"yaml:"-"`
}

const (
CONFIG_CONSTRAINTS = `
"tribe": {
"type": ["object", "null"],
"properties": {
"enable" : {
"type": "boolean"
},
"bind_addr": {
"type": "string"
},
"bind_port": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"name": {
"type": "string"
},
"seed": {
"type" : "string"
}
},
"additionalProperties": false
}
`
)

// get the default snapd configuration
func GetDefaultConfig() *Config {
mlCfg := memberlist.DefaultLANConfig()
Expand Down
22 changes: 20 additions & 2 deletions mgmt/tribe/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ import (
. "github.com/smartystreets/goconvey/convey"
)

const (
MOCK_CONSTRAINTS = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "snapd global config schema",
"type": ["object", "null"],
"properties": {
"control": { "$ref": "#/definitions/control" },
"scheduler": { "$ref": "#/definitions/scheduler"},
"restapi" : { "$ref": "#/definitions/restapi"},
"tribe": { "$ref": "#/definitions/tribe"}
},
"additionalProperties": true,
"definitions": { ` +
`"control": {}, "scheduler": {}, "restapi":{}, ` + CONFIG_CONSTRAINTS +
`}` +
`}`
)

type mockConfig struct {
Tribe *Config
}
Expand All @@ -36,7 +54,7 @@ func TestTribeConfigJSON(t *testing.T) {
Tribe: GetDefaultConfig(),
}
path := "../../examples/configs/snap-config-sample.json"
err := cfgfile.Read(path, &config)
err := cfgfile.Read(path, &config, MOCK_CONSTRAINTS)
var cfg *Config
if err == nil {
cfg = config.Tribe
Expand Down Expand Up @@ -69,7 +87,7 @@ func TestTribeConfigYaml(t *testing.T) {
Tribe: GetDefaultConfig(),
}
path := "../../examples/configs/snap-config-sample.yaml"
err := cfgfile.Read(path, &config)
err := cfgfile.Read(path, &config, MOCK_CONSTRAINTS)
var cfg *Config
if err == nil {
cfg = config.Tribe
Expand Down
54 changes: 47 additions & 7 deletions pkg/cfgfile/cfgfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,73 @@ limitations under the License.
package cfgfile

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"strings"

"github.com/ghodss/yaml"
"github.com/intelsdi-x/snap/core/serror"
"github.com/xeipuuv/gojsonschema"
)

// Read an input configuration file, parsing it (as YAML or JSON)
// into the input 'interface{}'', v
func Read(path string, v interface{}) error {
func Read(path string, v interface{}, schema string) []serror.SnapError {
// read bytes from file
b, err := ioutil.ReadFile(path)
if err != nil {
return err
return []serror.SnapError{serror.New(err)}
}
// parse the byte-stream read (above); Note that we can parse both JSON
// and YAML using the same yaml.Unmarshal() method since a JSON string is
// a valid YAML string (but the converse is not true)
if parseErr := yaml.Unmarshal(b, v); parseErr != nil {
// convert from YAML to JSON (remember, JSON is actually valid YAML)
jb, err := yaml.YAMLToJSON(b)
if err != nil {
return []serror.SnapError{serror.New(fmt.Errorf("error converting YAML to JSON: %v", err))}
}
// validate the resulting JSON against the input the schema
if errors := ValidateSchema(schema, string(jb)); errors != nil {
// if invalid, construct (and return?) a SnapError from the errors identified
// during schema validation
return errors
}
// if valid, parse the JSON byte-stream (above)
if parseErr := json.Unmarshal(jb, v); parseErr != nil {
// remove any YAML-specific prefix that might have been added by then
// yaml.Unmarshal() method or JSON-specific prefix that might have been
// added if the resulting JSON string could not be marshalled into our
// input interface correctly (note, if there is no match to either of
// these prefixes then the error message will be passed through unchanged)
tmpErr := strings.TrimPrefix(parseErr.Error(), "error converting YAML to JSON: yaml: ")
errRet := strings.TrimPrefix(tmpErr, "error unmarshaling JSON: json: ")
return fmt.Errorf("Error while parsing configuration file: %v", errRet)
return []serror.SnapError{serror.New(fmt.Errorf("Error while parsing configuration file: %v", errRet))}
}
return nil
}

// Validate the configuration (JSON) string against the input schema
func ValidateSchema(schema, cfg string) []serror.SnapError {
schemaLoader := gojsonschema.NewStringLoader(schema)
testDoc := gojsonschema.NewStringLoader(cfg)
result, err := gojsonschema.Validate(schemaLoader, testDoc)
var serrors []serror.SnapError
// Check for invalid json
if err != nil {
serrors = append(serrors, serror.New(err))
return serrors
}
// check if result passes validation
if result.Valid() {
return nil
}
for _, err := range result.Errors() {
serr := serror.New(errors.New("Validate schema error"))
serr.SetFields(map[string]interface{}{
"value": err.Value(),
"context": err.Context().String("::"),
"description": err.Description(),
})
serrors = append(serrors, serr)
}
return serrors
}
Loading

0 comments on commit 1e44f11

Please sign in to comment.