From 25e5ca5e4cbc69160ffcf20831d25243343496d6 Mon Sep 17 00:00:00 2001 From: Josh Curl Date: Sun, 6 Nov 2016 13:41:46 -0800 Subject: [PATCH] Add command to validate configuration --- cmd/control/config.go | 53 ++++++++--- config/schema.go | 195 +++++++++++++++++++++++++++++++++++++++ config/validate.go | 42 +++++++++ config/validate_test.go | 29 ++++++ scripts/inline_schema.go | 32 +++++++ scripts/schema.json | 192 ++++++++++++++++++++++++++++++++++++++ scripts/schema_template | 3 + 7 files changed, 534 insertions(+), 12 deletions(-) create mode 100644 config/schema.go create mode 100644 config/validate.go create mode 100644 config/validate_test.go create mode 100644 scripts/inline_schema.go create mode 100644 scripts/schema.json create mode 100644 scripts/schema_template diff --git a/cmd/control/config.go b/cmd/control/config.go index 69f9cb51..d660aad3 100644 --- a/cmd/control/config.go +++ b/cmd/control/config.go @@ -76,6 +76,17 @@ func configSubcommands() []cli.Command { }, }, }, + { + Name: "validate", + Usage: "validate configuration from stdin", + Action: validate, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "input, i", + Usage: "File from which to read", + }, + }, + }, } } @@ -183,18 +194,7 @@ func configGet(c *cli.Context) error { } func merge(c *cli.Context) error { - input := os.Stdin - inputFile := c.String("input") - if inputFile != "" { - var err error - input, err = os.Open(inputFile) - if err != nil { - log.Fatal(err) - } - defer input.Close() - } - - bytes, err := ioutil.ReadAll(input) + bytes, err := inputBytes(c) if err != nil { log.Fatal(err) } @@ -224,3 +224,32 @@ func export(c *cli.Context) error { return nil } + +func validate(c *cli.Context) error { + bytes, err := inputBytes(c) + if err != nil { + log.Fatal(err) + } + validationErrors, err := config.Validate(bytes) + if err != nil { + log.Fatal(err) + } + for _, validationError := range validationErrors.Errors() { + log.Error(validationError) + } + return nil +} + +func inputBytes(c *cli.Context) ([]byte, error) { + input := os.Stdin + inputFile := c.String("input") + if inputFile != "" { + var err error + input, err = os.Open(inputFile) + if err != nil { + return nil, err + } + defer input.Close() + } + return ioutil.ReadAll(input) +} diff --git a/config/schema.go b/config/schema.go new file mode 100644 index 00000000..9c4ea1cb --- /dev/null +++ b/config/schema.go @@ -0,0 +1,195 @@ +package config + +var schema = `{ + "type": "object", + "additionalProperties": false, + + "properties": { + "ssh_authorized_keys": {"$ref": "#/definitions/list_of_strings"}, + "write_files": { + "type": "array", + "items": {"$ref": "#/definitions/file_config"} + }, + "hostname": {"type": "string"}, + "mounts": {"type": "array"}, + "rancher": {"$ref": "#/definitions/rancher_config"}, + "runcmd": {"type": "array"} + }, + + "definitions": { + "rancher_config": { + "id": "#/definitions/rancher_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "console": {"type": "string"}, + "environment": {"type": "object"}, + "services": {"type": "object"}, + "bootstrap_containers": {"type": "object"}, + "autoformat": {"type": "object"}, + "bootstrap_docker": {"$ref": "#/definitions/docker_config"}, + "cloud_init": {"$ref": "#/definitions/cloud_init_config"}, + "debug": {"type": "boolean"}, + "rm_usr": {"type": "boolean"}, + "no_sharedroot": {"type": "boolean"}, + "log": {"type": "boolean"}, + "force_console_rebuild": {"type": "boolean"}, + "disable": {"$ref": "#/definitions/list_of_strings"}, + "services_include": {"type": "object"}, + "modules": {"$ref": "#/definitions/list_of_strings"}, + "network": {"$ref": "#/definitions/network_config"}, + "default_network": {"type": "object"}, + "repositories": {"type": "object"}, + "ssh": {"$ref": "#/definitions/ssh_config"}, + "state": {"$ref": "#/definitions/state_config"}, + "system_docker": {"$ref": "#/definitions/docker_config"}, + "upgrade": {"$ref": "#/definitions/upgrade_config"}, + "docker": {"$ref": "#/definitions/docker_config"}, + "registry_auths": {"type": "object"}, + "defaults": {"$ref": "#/definitions/defaults_config"}, + "resize_device": {"type": "string"}, + "sysctl": {"type": "object"} + } + }, + + "file_config": { + "id": "#/definitions/file_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "encoding": {"type": "string"}, + "content": {"type": "string"}, + "owner": {"type": "string"}, + "path": {"type": "string"}, + "permissions": {"type": "string"} + } + }, + + "network_config": { + "id": "#/definitions/network_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "pre_cmds": {"$ref": "#/definitions/list_of_strings"}, + "dns": {"type": "object"}, + "interfaces": {"type": "object"}, + "post_cmds": {"$ref": "#/definitions/list_of_strings"}, + "http_proxy": {"type": "string"}, + "https_proxy": {"type": "string"}, + "no_proxy": {"type": "string"} + } + }, + + "upgrade_config": { + "id": "#/definitions/upgrade_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "url": {"type": "string"}, + "image": {"type": "string"}, + "rollback": {"type": "string"} + } + }, + + "docker_config": { + "id": "#/definitions/docker_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "engine": {"type": "string"}, + "tls": {"type": "boolean"}, + "tls_args": {"$ref": "#/definitions/list_of_strings"}, + "args": {"$ref": "#/definitions/list_of_strings"}, + "extra_args": {"$ref": "#/definitions/list_of_strings"}, + "server_cert": {"type": "string"}, + "server_key": {"type": "string"}, + "ca_cert": {"type": "string"}, + "ca_key": {"type": "string"}, + "environment": {"$ref": "#/definitions/list_of_strings"}, + "storage_context": {"type": "string"}, + "exec": {"type": "boolean"}, + "bridge": {"type": "string"}, + "config_file": {"type": "string"}, + "containerd": {"type": "string"}, + "debug": {"type": "boolean"}, + "exec_root": {"type": "string"}, + "group": {"type": "string"}, + "graph": {"type": "string"}, + "host": {"type": "string"}, + "live_restore": {"type": "boolean"}, + "log_driver": {"type": "string"}, + "log_opts": {"type": "object"}, + "pid_file": {"type": "string"}, + "registry_mirror": {"type": "string"}, + "restart": {"type": "boolean"}, + "selinux_enabled": {"type": "boolean"}, + "storage_driver": {"type": "string"}, + "userland_proxy": {"type": "boolean"} + } + }, + + "ssh_config": { + "id": "#/definitions/ssh_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "keys": {"type": "object"} + } + }, + + "state_config": { + "id": "#/definitions/state_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "directory": {"type": "string"}, + "fstype": {"type": "string"}, + "dev": {"type": "string"}, + "wait": {"type": "boolean"}, + "required": {"type": "boolean"}, + "autoformat": {"$ref": "#/definitions/list_of_strings"}, + "mdadm_scan": {"type": "boolean"}, + "script": {"type": "string"}, + "oem_fstype": {"type": "string"}, + "oem_dev": {"type": "string"} + } + }, + + "cloud_init_config": { + "id": "#/definitions/cloud_init_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "datasources": {"$ref": "#/definitions/list_of_strings"} + } + }, + + "defaults_config": { + "id": "#/definitions/defaults_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "hostname": {"type": "string"}, + "docker": {"type": "object"}, + "network": {"$ref": "#/definitions/network_config"} + } + }, + + "list_of_strings": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + } + } +} + +` diff --git a/config/validate.go b/config/validate.go new file mode 100644 index 00000000..f7027137 --- /dev/null +++ b/config/validate.go @@ -0,0 +1,42 @@ +package config + +import ( + yaml "github.com/cloudfoundry-incubator/candiedyaml" + "github.com/xeipuuv/gojsonschema" +) + +// TODO: use this function from libcompose +func convertKeysToStrings(item interface{}) interface{} { + switch typedDatas := item.(type) { + case map[string]interface{}: + for key, value := range typedDatas { + typedDatas[key] = convertKeysToStrings(value) + } + return typedDatas + case map[interface{}]interface{}: + newMap := make(map[string]interface{}) + for key, value := range typedDatas { + stringKey := key.(string) + newMap[stringKey] = convertKeysToStrings(value) + } + return newMap + case []interface{}: + for i, value := range typedDatas { + typedDatas[i] = append(typedDatas, convertKeysToStrings(value)) + } + return typedDatas + default: + return item + } +} + +func Validate(bytes []byte) (*gojsonschema.Result, error) { + var rawCfg map[string]interface{} + if err := yaml.Unmarshal([]byte(bytes), &rawCfg); err != nil { + return nil, err + } + rawCfg = convertKeysToStrings(rawCfg).(map[string]interface{}) + loader := gojsonschema.NewGoLoader(rawCfg) + schemaLoader := gojsonschema.NewStringLoader(schema) + return gojsonschema.Validate(schemaLoader, loader) +} diff --git a/config/validate_test.go b/config/validate_test.go new file mode 100644 index 00000000..fd0db132 --- /dev/null +++ b/config/validate_test.go @@ -0,0 +1,29 @@ +package config + +import ( + "fmt" + "strings" + "testing" +) + +func testValidate(t *testing.T, cfg []byte, contains string) { + validationErrors, err := Validate(cfg) + if err != nil { + t.Fatal(err) + } + if contains == "" && len(validationErrors.Errors()) != 0 { + t.Fail() + } + if !strings.Contains(fmt.Sprint(validationErrors.Errors()), contains) { + t.Fail() + } +} + +func TestValidate(t *testing.T) { + testValidate(t, []byte("{}"), "") + testValidate(t, []byte(`rancher: + log: true +`), "") + testValidate(t, []byte("bad_key: {}"), "Additional property bad_key is not allowed") + testValidate(t, []byte("rancher: []"), "rancher: Invalid type. Expected: object, given: array") +} diff --git a/scripts/inline_schema.go b/scripts/inline_schema.go new file mode 100644 index 00000000..6251dbbf --- /dev/null +++ b/scripts/inline_schema.go @@ -0,0 +1,32 @@ +package main + +import ( + "io/ioutil" + "os" + "text/template" +) + +func main() { + t, err := template.New("schema_template").ParseFiles("./scripts/schema_template") + if err != nil { + panic(err) + } + + schema, err := ioutil.ReadFile("./scripts/schema.json") + if err != nil { + panic(err) + } + + inlinedFile, err := os.Create("config/schema.go") + if err != nil { + panic(err) + } + + err = t.Execute(inlinedFile, map[string]string{ + "schema": string(schema), + }) + + if err != nil { + panic(err) + } +} diff --git a/scripts/schema.json b/scripts/schema.json new file mode 100644 index 00000000..0fe87ff0 --- /dev/null +++ b/scripts/schema.json @@ -0,0 +1,192 @@ +{ + "type": "object", + "additionalProperties": false, + + "properties": { + "ssh_authorized_keys": {"$ref": "#/definitions/list_of_strings"}, + "write_files": { + "type": "array", + "items": {"$ref": "#/definitions/file_config"} + }, + "hostname": {"type": "string"}, + "mounts": {"type": "array"}, + "rancher": {"$ref": "#/definitions/rancher_config"}, + "runcmd": {"type": "array"} + }, + + "definitions": { + "rancher_config": { + "id": "#/definitions/rancher_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "console": {"type": "string"}, + "environment": {"type": "object"}, + "services": {"type": "object"}, + "bootstrap_containers": {"type": "object"}, + "autoformat": {"type": "object"}, + "bootstrap_docker": {"$ref": "#/definitions/docker_config"}, + "cloud_init": {"$ref": "#/definitions/cloud_init_config"}, + "debug": {"type": "boolean"}, + "rm_usr": {"type": "boolean"}, + "no_sharedroot": {"type": "boolean"}, + "log": {"type": "boolean"}, + "force_console_rebuild": {"type": "boolean"}, + "disable": {"$ref": "#/definitions/list_of_strings"}, + "services_include": {"type": "object"}, + "modules": {"$ref": "#/definitions/list_of_strings"}, + "network": {"$ref": "#/definitions/network_config"}, + "default_network": {"type": "object"}, + "repositories": {"type": "object"}, + "ssh": {"$ref": "#/definitions/ssh_config"}, + "state": {"$ref": "#/definitions/state_config"}, + "system_docker": {"$ref": "#/definitions/docker_config"}, + "upgrade": {"$ref": "#/definitions/upgrade_config"}, + "docker": {"$ref": "#/definitions/docker_config"}, + "registry_auths": {"type": "object"}, + "defaults": {"$ref": "#/definitions/defaults_config"}, + "resize_device": {"type": "string"}, + "sysctl": {"type": "object"} + } + }, + + "file_config": { + "id": "#/definitions/file_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "encoding": {"type": "string"}, + "content": {"type": "string"}, + "owner": {"type": "string"}, + "path": {"type": "string"}, + "permissions": {"type": "string"} + } + }, + + "network_config": { + "id": "#/definitions/network_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "pre_cmds": {"$ref": "#/definitions/list_of_strings"}, + "dns": {"type": "object"}, + "interfaces": {"type": "object"}, + "post_cmds": {"$ref": "#/definitions/list_of_strings"}, + "http_proxy": {"type": "string"}, + "https_proxy": {"type": "string"}, + "no_proxy": {"type": "string"} + } + }, + + "upgrade_config": { + "id": "#/definitions/upgrade_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "url": {"type": "string"}, + "image": {"type": "string"}, + "rollback": {"type": "string"} + } + }, + + "docker_config": { + "id": "#/definitions/docker_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "engine": {"type": "string"}, + "tls": {"type": "boolean"}, + "tls_args": {"$ref": "#/definitions/list_of_strings"}, + "args": {"$ref": "#/definitions/list_of_strings"}, + "extra_args": {"$ref": "#/definitions/list_of_strings"}, + "server_cert": {"type": "string"}, + "server_key": {"type": "string"}, + "ca_cert": {"type": "string"}, + "ca_key": {"type": "string"}, + "environment": {"$ref": "#/definitions/list_of_strings"}, + "storage_context": {"type": "string"}, + "exec": {"type": "boolean"}, + "bridge": {"type": "string"}, + "config_file": {"type": "string"}, + "containerd": {"type": "string"}, + "debug": {"type": "boolean"}, + "exec_root": {"type": "string"}, + "group": {"type": "string"}, + "graph": {"type": "string"}, + "host": {"type": "string"}, + "live_restore": {"type": "boolean"}, + "log_driver": {"type": "string"}, + "log_opts": {"type": "object"}, + "pid_file": {"type": "string"}, + "registry_mirror": {"type": "string"}, + "restart": {"type": "boolean"}, + "selinux_enabled": {"type": "boolean"}, + "storage_driver": {"type": "string"}, + "userland_proxy": {"type": "boolean"} + } + }, + + "ssh_config": { + "id": "#/definitions/ssh_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "keys": {"type": "object"} + } + }, + + "state_config": { + "id": "#/definitions/state_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "directory": {"type": "string"}, + "fstype": {"type": "string"}, + "dev": {"type": "string"}, + "wait": {"type": "boolean"}, + "required": {"type": "boolean"}, + "autoformat": {"$ref": "#/definitions/list_of_strings"}, + "mdadm_scan": {"type": "boolean"}, + "script": {"type": "string"}, + "oem_fstype": {"type": "string"}, + "oem_dev": {"type": "string"} + } + }, + + "cloud_init_config": { + "id": "#/definitions/cloud_init_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "datasources": {"$ref": "#/definitions/list_of_strings"} + } + }, + + "defaults_config": { + "id": "#/definitions/defaults_config", + "type": "object", + "additionalProperties": false, + + "properties": { + "hostname": {"type": "string"}, + "docker": {"type": "object"}, + "network": {"$ref": "#/definitions/network_config"} + } + }, + + "list_of_strings": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + } + } +} + diff --git a/scripts/schema_template b/scripts/schema_template new file mode 100644 index 00000000..84035db0 --- /dev/null +++ b/scripts/schema_template @@ -0,0 +1,3 @@ +package config + +var schema = `{{.schema}}`