config/validate: add new config validator
This validator is still experimental and is going to need new rules in the future. This lays out the general framework.
This commit is contained in:
		| @@ -1,3 +1,19 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package config | ||||
|  | ||||
| import ( | ||||
|   | ||||
| @@ -27,7 +27,7 @@ type Unit struct { | ||||
| 	Enable  bool   `yaml:"enable"` | ||||
| 	Runtime bool   `yaml:"runtime"` | ||||
| 	Content string `yaml:"content"` | ||||
| 	Command string `yaml:"command"` | ||||
| 	Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"` | ||||
|  | ||||
| 	// For drop-in units, a cloudinit.conf is generated. | ||||
| 	// This is currently unbound in YAML (and hence unsettable in cloud-config files) | ||||
|   | ||||
							
								
								
									
										54
									
								
								config/validate/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								config/validate/context.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // context represents the current position within a newline-delimited string. | ||||
| // Each line is loaded, one by one, into currentLine (newline omitted) and | ||||
| // lineNumber keeps track of its position within the original string. | ||||
| type context struct { | ||||
| 	currentLine    string | ||||
| 	remainingLines string | ||||
| 	lineNumber     int | ||||
| } | ||||
|  | ||||
| // Increment moves the context to the next line (if available). | ||||
| func (c *context) Increment() { | ||||
| 	if c.currentLine == "" && c.remainingLines == "" { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	lines := strings.SplitN(c.remainingLines, "\n", 2) | ||||
| 	c.currentLine = lines[0] | ||||
| 	if len(lines) == 2 { | ||||
| 		c.remainingLines = lines[1] | ||||
| 	} else { | ||||
| 		c.remainingLines = "" | ||||
| 	} | ||||
| 	c.lineNumber++ | ||||
| } | ||||
|  | ||||
| // NewContext creates a context from the provided data. It strips out all | ||||
| // carriage returns and moves to the first line (if available). | ||||
| func NewContext(content []byte) context { | ||||
| 	c := context{remainingLines: strings.Replace(string(content), "\r", "", -1)} | ||||
| 	c.Increment() | ||||
| 	return c | ||||
| } | ||||
							
								
								
									
										133
									
								
								config/validate/context_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								config/validate/context_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestNewContext(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		in string | ||||
|  | ||||
| 		out context | ||||
| 	}{ | ||||
| 		{ | ||||
| 			out: context{ | ||||
| 				currentLine:    "", | ||||
| 				remainingLines: "", | ||||
| 				lineNumber:     0, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			in: "this\r\nis\r\na\r\ntest", | ||||
| 			out: context{ | ||||
| 				currentLine:    "this", | ||||
| 				remainingLines: "is\na\ntest", | ||||
| 				lineNumber:     1, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		if out := NewContext([]byte(tt.in)); !reflect.DeepEqual(tt.out, out) { | ||||
| 			t.Errorf("bad context (%q): want %#v, got %#v", tt.in, tt.out, out) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestIncrement(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		init context | ||||
| 		op   func(c *context) | ||||
|  | ||||
| 		res context | ||||
| 	}{ | ||||
| 		{ | ||||
| 			init: context{ | ||||
| 				currentLine:    "", | ||||
| 				remainingLines: "", | ||||
| 				lineNumber:     0, | ||||
| 			}, | ||||
| 			res: context{ | ||||
| 				currentLine:    "", | ||||
| 				remainingLines: "", | ||||
| 				lineNumber:     0, | ||||
| 			}, | ||||
| 			op: func(c *context) { | ||||
| 				c.Increment() | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			init: context{ | ||||
| 				currentLine:    "test", | ||||
| 				remainingLines: "", | ||||
| 				lineNumber:     1, | ||||
| 			}, | ||||
| 			res: context{ | ||||
| 				currentLine:    "", | ||||
| 				remainingLines: "", | ||||
| 				lineNumber:     2, | ||||
| 			}, | ||||
| 			op: func(c *context) { | ||||
| 				c.Increment() | ||||
| 				c.Increment() | ||||
| 				c.Increment() | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			init: context{ | ||||
| 				currentLine:    "this", | ||||
| 				remainingLines: "is\na\ntest", | ||||
| 				lineNumber:     1, | ||||
| 			}, | ||||
| 			res: context{ | ||||
| 				currentLine:    "is", | ||||
| 				remainingLines: "a\ntest", | ||||
| 				lineNumber:     2, | ||||
| 			}, | ||||
| 			op: func(c *context) { | ||||
| 				c.Increment() | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			init: context{ | ||||
| 				currentLine:    "this", | ||||
| 				remainingLines: "is\na\ntest", | ||||
| 				lineNumber:     1, | ||||
| 			}, | ||||
| 			res: context{ | ||||
| 				currentLine:    "test", | ||||
| 				remainingLines: "", | ||||
| 				lineNumber:     4, | ||||
| 			}, | ||||
| 			op: func(c *context) { | ||||
| 				c.Increment() | ||||
| 				c.Increment() | ||||
| 				c.Increment() | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, tt := range tests { | ||||
| 		res := tt.init | ||||
| 		if tt.op(&res); !reflect.DeepEqual(tt.res, res) { | ||||
| 			t.Errorf("bad context (%d, %#v): want %#v, got %#v", i, tt.init, tt.res, res) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										159
									
								
								config/validate/node.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								config/validate/node.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"regexp" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	yamlKey  = regexp.MustCompile(`^ *-? ?(?P<key>.*?):`) | ||||
| 	yamlElem = regexp.MustCompile(`^ *-`) | ||||
| ) | ||||
|  | ||||
| type node struct { | ||||
| 	name     string | ||||
| 	line     int | ||||
| 	children []node | ||||
| 	field    reflect.StructField | ||||
| 	reflect.Value | ||||
| } | ||||
|  | ||||
| // Child attempts to find the child with the given name in the node's list of | ||||
| // children. If no such child is found, an invalid node is returned. | ||||
| func (n node) Child(name string) node { | ||||
| 	for _, c := range n.children { | ||||
| 		if c.name == name { | ||||
| 			return c | ||||
| 		} | ||||
| 	} | ||||
| 	return node{} | ||||
| } | ||||
|  | ||||
| // HumanType returns the human-consumable string representation of the type of | ||||
| // the node. | ||||
| func (n node) HumanType() string { | ||||
| 	switch k := n.Kind(); k { | ||||
| 	case reflect.Slice: | ||||
| 		c := n.Type().Elem() | ||||
| 		return "[]" + node{Value: reflect.New(c).Elem()}.HumanType() | ||||
| 	default: | ||||
| 		return k.String() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // NewNode returns the node representation of the given value. The context | ||||
| // will be used in an attempt to determine line numbers for the given value. | ||||
| func NewNode(value interface{}, context context) node { | ||||
| 	var n node | ||||
| 	toNode(value, context, &n) | ||||
| 	return n | ||||
| } | ||||
|  | ||||
| // toNode converts the given value into a node and then recursively processes | ||||
| // each of the nodes components (e.g. fields, array elements, keys). | ||||
| func toNode(v interface{}, c context, n *node) { | ||||
| 	vv := reflect.ValueOf(v) | ||||
| 	if !vv.IsValid() { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	n.Value = vv | ||||
| 	switch vv.Kind() { | ||||
| 	case reflect.Struct: | ||||
| 		// Walk over each field in the structure, skipping unexported fields, | ||||
| 		// and create a node for it. | ||||
| 		for i := 0; i < vv.Type().NumField(); i++ { | ||||
| 			ft := vv.Type().Field(i) | ||||
| 			k := ft.Tag.Get("yaml") | ||||
| 			if k == "-" || k == "" { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			cn := node{name: k, field: ft} | ||||
| 			c, ok := findKey(cn.name, c) | ||||
| 			if ok { | ||||
| 				cn.line = c.lineNumber | ||||
| 			} | ||||
| 			toNode(vv.Field(i).Interface(), c, &cn) | ||||
| 			n.children = append(n.children, cn) | ||||
| 		} | ||||
| 	case reflect.Map: | ||||
| 		// Walk over each key in the map and create a node for it. | ||||
| 		v := v.(map[interface{}]interface{}) | ||||
| 		for k, cv := range v { | ||||
| 			cn := node{name: fmt.Sprintf("%s", k)} | ||||
| 			c, ok := findKey(cn.name, c) | ||||
| 			if ok { | ||||
| 				cn.line = c.lineNumber | ||||
| 			} | ||||
| 			toNode(cv, c, &cn) | ||||
| 			n.children = append(n.children, cn) | ||||
| 		} | ||||
| 	case reflect.Slice: | ||||
| 		// Walk over each element in the slice and create a node for it. | ||||
| 		// While iterating over the slice, preserve the context after it | ||||
| 		// is modified. This allows the line numbers to reflect the current | ||||
| 		// element instead of the first. | ||||
| 		for i := 0; i < vv.Len(); i++ { | ||||
| 			cn := node{ | ||||
| 				name:  fmt.Sprintf("%s[%d]", n.name, i), | ||||
| 				field: n.field, | ||||
| 			} | ||||
| 			var ok bool | ||||
| 			c, ok = findElem(c) | ||||
| 			if ok { | ||||
| 				cn.line = c.lineNumber | ||||
| 			} | ||||
| 			toNode(vv.Index(i).Interface(), c, &cn) | ||||
| 			n.children = append(n.children, cn) | ||||
| 			c.Increment() | ||||
| 		} | ||||
| 	case reflect.String, reflect.Int, reflect.Bool: | ||||
| 	default: | ||||
| 		panic(fmt.Sprintf("toNode(): unhandled kind %s", vv.Kind())) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // findKey attempts to find the requested key within the provided context. | ||||
| // A modified copy of the context is returned with every line up to the key | ||||
| // incremented past. A boolean, true if the key was found, is also returned. | ||||
| func findKey(key string, context context) (context, bool) { | ||||
| 	return find(yamlKey, key, context) | ||||
| } | ||||
|  | ||||
| // findElem attempts to find an array element within the provided context. | ||||
| // A modified copy of the context is returned with every line up to the array | ||||
| // element incremented past. A boolean, true if the key was found, is also | ||||
| // returned. | ||||
| func findElem(context context) (context, bool) { | ||||
| 	return find(yamlElem, "", context) | ||||
| } | ||||
|  | ||||
| func find(exp *regexp.Regexp, key string, context context) (context, bool) { | ||||
| 	for len(context.currentLine) > 0 || len(context.remainingLines) > 0 { | ||||
| 		matches := exp.FindStringSubmatch(context.currentLine) | ||||
| 		if len(matches) > 0 && (key == "" || matches[1] == key) { | ||||
| 			return context, true | ||||
| 		} | ||||
|  | ||||
| 		context.Increment() | ||||
| 	} | ||||
| 	return context, false | ||||
| } | ||||
							
								
								
									
										286
									
								
								config/validate/node_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								config/validate/node_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,286 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestChild(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		parent node | ||||
| 		name   string | ||||
|  | ||||
| 		child node | ||||
| 	}{ | ||||
| 		{}, | ||||
| 		{ | ||||
| 			name: "c1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			parent: node{ | ||||
| 				children: []node{ | ||||
| 					node{name: "c1"}, | ||||
| 					node{name: "c2"}, | ||||
| 					node{name: "c3"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			parent: node{ | ||||
| 				children: []node{ | ||||
| 					node{name: "c1"}, | ||||
| 					node{name: "c2"}, | ||||
| 					node{name: "c3"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			name:  "c2", | ||||
| 			child: node{name: "c2"}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		if child := tt.parent.Child(tt.name); !reflect.DeepEqual(tt.child, child) { | ||||
| 			t.Errorf("bad child (%q): want %#v, got %#v", tt.name, tt.child, child) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHumanType(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		node node | ||||
|  | ||||
| 		humanType string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			humanType: "invalid", | ||||
| 		}, | ||||
| 		{ | ||||
| 			node:      node{Value: reflect.ValueOf("hello")}, | ||||
| 			humanType: "string", | ||||
| 		}, | ||||
| 		{ | ||||
| 			node: node{ | ||||
| 				Value: reflect.ValueOf([]int{1, 2}), | ||||
| 				children: []node{ | ||||
| 					node{Value: reflect.ValueOf(1)}, | ||||
| 					node{Value: reflect.ValueOf(2)}, | ||||
| 				}}, | ||||
| 			humanType: "[]int", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		if humanType := tt.node.HumanType(); tt.humanType != humanType { | ||||
| 			t.Errorf("bad type (%q): want %q, got %q", tt.node, tt.humanType, humanType) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestToNode(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		value   interface{} | ||||
| 		context context | ||||
|  | ||||
| 		node node | ||||
| 	}{ | ||||
| 		{}, | ||||
| 		{ | ||||
| 			value: struct{}{}, | ||||
| 			node:  node{Value: reflect.ValueOf(struct{}{})}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			value: struct { | ||||
| 				A int `yaml:"a"` | ||||
| 			}{}, | ||||
| 			node: node{ | ||||
| 				children: []node{ | ||||
| 					node{ | ||||
| 						name: "a", | ||||
| 						field: reflect.TypeOf(struct { | ||||
| 							A int `yaml:"a"` | ||||
| 						}{}).Field(0), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			value: struct { | ||||
| 				A []int `yaml:"a"` | ||||
| 			}{}, | ||||
| 			node: node{ | ||||
| 				children: []node{ | ||||
| 					node{ | ||||
| 						name: "a", | ||||
| 						field: reflect.TypeOf(struct { | ||||
| 							A []int `yaml:"a"` | ||||
| 						}{}).Field(0), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			value: map[interface{}]interface{}{ | ||||
| 				"a": map[interface{}]interface{}{ | ||||
| 					"b": 2, | ||||
| 				}, | ||||
| 			}, | ||||
| 			context: NewContext([]byte("a:\n  b: 2")), | ||||
| 			node: node{ | ||||
| 				children: []node{ | ||||
| 					node{ | ||||
| 						line: 1, | ||||
| 						name: "a", | ||||
| 						children: []node{ | ||||
| 							node{name: "b", line: 2}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			value: struct { | ||||
| 				A struct { | ||||
| 					Jon bool `yaml:"b"` | ||||
| 				} `yaml:"a"` | ||||
| 			}{}, | ||||
| 			node: node{ | ||||
| 				children: []node{ | ||||
| 					node{ | ||||
| 						name: "a", | ||||
| 						children: []node{ | ||||
| 							node{ | ||||
| 								name: "b", | ||||
| 								field: reflect.TypeOf(struct { | ||||
| 									Jon bool `yaml:"b"` | ||||
| 								}{}).Field(0), | ||||
| 								Value: reflect.ValueOf(false), | ||||
| 							}, | ||||
| 						}, | ||||
| 						field: reflect.TypeOf(struct { | ||||
| 							A struct { | ||||
| 								Jon bool `yaml:"b"` | ||||
| 							} `yaml:"a"` | ||||
| 						}{}).Field(0), | ||||
| 						Value: reflect.ValueOf(struct { | ||||
| 							Jon bool `yaml:"b"` | ||||
| 						}{}), | ||||
| 					}, | ||||
| 				}, | ||||
| 				Value: reflect.ValueOf(struct { | ||||
| 					A struct { | ||||
| 						Jon bool `yaml:"b"` | ||||
| 					} `yaml:"a"` | ||||
| 				}{}), | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		var node node | ||||
| 		toNode(tt.value, tt.context, &node) | ||||
| 		if !nodesEqual(tt.node, node) { | ||||
| 			t.Errorf("bad node (%#v): want %#v, got %#v", tt.value, tt.node, node) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFindKey(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		key     string | ||||
| 		context context | ||||
|  | ||||
| 		found bool | ||||
| 	}{ | ||||
| 		{}, | ||||
| 		{ | ||||
| 			key:     "key1", | ||||
| 			context: NewContext([]byte("key1: hi")), | ||||
| 			found:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			key:     "key2", | ||||
| 			context: NewContext([]byte("key1: hi")), | ||||
| 			found:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			key:     "key3", | ||||
| 			context: NewContext([]byte("key1:\n  key2:\n    key3: hi")), | ||||
| 			found:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			key:     "key4", | ||||
| 			context: NewContext([]byte("key1:\n  - key4: hi")), | ||||
| 			found:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			key:     "key5", | ||||
| 			context: NewContext([]byte("#key5")), | ||||
| 			found:   false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		if _, found := findKey(tt.key, tt.context); tt.found != found { | ||||
| 			t.Errorf("bad find (%q): want %t, got %t", tt.key, tt.found, found) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFindElem(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		context context | ||||
|  | ||||
| 		found bool | ||||
| 	}{ | ||||
| 		{}, | ||||
| 		{ | ||||
| 			context: NewContext([]byte("test: hi")), | ||||
| 			found:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			context: NewContext([]byte("test:\n  - a\n  -b")), | ||||
| 			found:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			context: NewContext([]byte("test:\n  -\n    a")), | ||||
| 			found:   true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		if _, found := findElem(tt.context); tt.found != found { | ||||
| 			t.Errorf("bad find (%q): want %t, got %t", tt.context, tt.found, found) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func nodesEqual(a, b node) bool { | ||||
| 	if a.name != b.name || | ||||
| 		a.line != b.line || | ||||
| 		!reflect.DeepEqual(a.field, b.field) || | ||||
| 		len(a.children) != len(b.children) { | ||||
| 		return false | ||||
| 	} | ||||
| 	for i := 0; i < len(a.children); i++ { | ||||
| 		if !nodesEqual(a.children[i], b.children[i]) { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
							
								
								
									
										90
									
								
								config/validate/report.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								config/validate/report.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| // Report represents the list of entries resulting from validation. | ||||
| type Report struct { | ||||
| 	entries []Entry | ||||
| } | ||||
|  | ||||
| // Error adds an error entry to the report. | ||||
| func (r *Report) Error(line int, message string) { | ||||
| 	r.entries = append(r.entries, Entry{entryError, message, line}) | ||||
| } | ||||
|  | ||||
| // Warning adds a warning entry to the report. | ||||
| func (r *Report) Warning(line int, message string) { | ||||
| 	r.entries = append(r.entries, Entry{entryWarning, message, line}) | ||||
| } | ||||
|  | ||||
| // Info adds an info entry to the report. | ||||
| func (r *Report) Info(line int, message string) { | ||||
| 	r.entries = append(r.entries, Entry{entryInfo, message, line}) | ||||
| } | ||||
|  | ||||
| // Entries returns the list of entries in the report. | ||||
| func (r *Report) Entries() []Entry { | ||||
| 	return r.entries | ||||
| } | ||||
|  | ||||
| // Entry represents a single generic item in the report. | ||||
| type Entry struct { | ||||
| 	kind    entryKind | ||||
| 	message string | ||||
| 	line    int | ||||
| } | ||||
|  | ||||
| // String returns a human-readable representation of the entry. | ||||
| func (e Entry) String() string { | ||||
| 	return fmt.Sprintf("line %d: %s: %s", e.line, e.kind, e.message) | ||||
| } | ||||
|  | ||||
| // MarshalJSON satisfies the json.Marshaler interface, returning the entry | ||||
| // encoded as a JSON object. | ||||
| func (e Entry) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(map[string]interface{}{ | ||||
| 		"kind":    e.kind.String(), | ||||
| 		"message": e.message, | ||||
| 		"line":    e.line, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| type entryKind int | ||||
|  | ||||
| const ( | ||||
| 	entryError entryKind = iota | ||||
| 	entryWarning | ||||
| 	entryInfo | ||||
| ) | ||||
|  | ||||
| func (k entryKind) String() string { | ||||
| 	switch k { | ||||
| 	case entryError: | ||||
| 		return "error" | ||||
| 	case entryWarning: | ||||
| 		return "warning" | ||||
| 	case entryInfo: | ||||
| 		return "info" | ||||
| 	default: | ||||
| 		panic(fmt.Sprintf("invalid kind %d", k)) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										98
									
								
								config/validate/report_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								config/validate/report_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestEntry(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		entry Entry | ||||
|  | ||||
| 		str  string | ||||
| 		json []byte | ||||
| 	}{ | ||||
| 		{ | ||||
| 			Entry{entryInfo, "test info", 1}, | ||||
| 			"line 1: info: test info", | ||||
| 			[]byte(`{"kind":"info","line":1,"message":"test info"}`), | ||||
| 		}, | ||||
| 		{ | ||||
| 			Entry{entryWarning, "test warning", 1}, | ||||
| 			"line 1: warning: test warning", | ||||
| 			[]byte(`{"kind":"warning","line":1,"message":"test warning"}`), | ||||
| 		}, | ||||
| 		{ | ||||
| 			Entry{entryError, "test error", 2}, | ||||
| 			"line 2: error: test error", | ||||
| 			[]byte(`{"kind":"error","line":2,"message":"test error"}`), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		if str := tt.entry.String(); tt.str != str { | ||||
| 			t.Errorf("bad string (%q): want %q, got %q", tt.entry, tt.str, str) | ||||
| 		} | ||||
| 		json, err := tt.entry.MarshalJSON() | ||||
| 		if err != nil { | ||||
| 			t.Errorf("bad error (%q): want %v, got %q", tt.entry, nil, err) | ||||
| 		} | ||||
| 		if !bytes.Equal(tt.json, json) { | ||||
| 			t.Errorf("bad JSON (%q): want %q, got %q", tt.entry, tt.json, json) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestReport(t *testing.T) { | ||||
| 	type reportFunc struct { | ||||
| 		fn      func(*Report, int, string) | ||||
| 		line    int | ||||
| 		message string | ||||
| 	} | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		fs []reportFunc | ||||
|  | ||||
| 		es []Entry | ||||
| 	}{ | ||||
| 		{ | ||||
| 			[]reportFunc{ | ||||
| 				{(*Report).Warning, 1, "test warning 1"}, | ||||
| 				{(*Report).Error, 2, "test error 2"}, | ||||
| 				{(*Report).Info, 10, "test info 10"}, | ||||
| 			}, | ||||
| 			[]Entry{ | ||||
| 				Entry{entryWarning, "test warning 1", 1}, | ||||
| 				Entry{entryError, "test error 2", 2}, | ||||
| 				Entry{entryInfo, "test info 10", 10}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		r := Report{} | ||||
| 		for _, f := range tt.fs { | ||||
| 			f.fn(&r, f.line, f.message) | ||||
| 		} | ||||
| 		if es := r.Entries(); !reflect.DeepEqual(tt.es, es) { | ||||
| 			t.Errorf("bad entries (%v): want %#v, got %#v", tt.fs, tt.es, es) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										115
									
								
								config/validate/rules.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								config/validate/rules.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| ) | ||||
|  | ||||
| type rule func(config node, report *Report) | ||||
|  | ||||
| // Rules contains all of the validation rules. | ||||
| var Rules []rule = []rule{ | ||||
| 	checkStructure, | ||||
| 	checkValidity, | ||||
| } | ||||
|  | ||||
| // checkStructure compares the provided config to the empty config.CloudConfig | ||||
| // structure. Each node is checked to make sure that it exists in the known | ||||
| // structure and that its type is compatible. | ||||
| func checkStructure(cfg node, report *Report) { | ||||
| 	g := NewNode(config.CloudConfig{}, NewContext([]byte{})) | ||||
| 	checkNodeStructure(cfg, g, report) | ||||
| } | ||||
|  | ||||
| func checkNodeStructure(n, g node, r *Report) { | ||||
| 	if !isCompatible(n.Kind(), g.Kind()) { | ||||
| 		r.Warning(n.line, fmt.Sprintf("incorrect type for %q (want %s)", n.name, g.HumanType())) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	switch g.Kind() { | ||||
| 	case reflect.Struct: | ||||
| 		for _, cn := range n.children { | ||||
| 			if cg := g.Child(cn.name); cg.IsValid() { | ||||
| 				checkNodeStructure(cn, cg, r) | ||||
| 			} else { | ||||
| 				r.Warning(cn.line, fmt.Sprintf("unrecognized key %q", cn.name)) | ||||
| 			} | ||||
| 		} | ||||
| 	case reflect.Slice: | ||||
| 		for _, cn := range n.children { | ||||
| 			var cg node | ||||
| 			c := g.Type().Elem() | ||||
| 			toNode(reflect.New(c).Elem().Interface(), context{}, &cg) | ||||
| 			checkNodeStructure(cn, cg, r) | ||||
| 		} | ||||
| 	case reflect.String, reflect.Int, reflect.Bool: | ||||
| 	default: | ||||
| 		panic(fmt.Sprintf("checkNodeStructure(): unhandled kind %s", g.Kind())) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // checkValidity checks the value of every node in the provided config by | ||||
| // running config.AssertValid() on it. | ||||
| func checkValidity(cfg node, report *Report) { | ||||
| 	g := NewNode(config.CloudConfig{}, NewContext([]byte{})) | ||||
| 	checkNodeValidity(cfg, g, report) | ||||
| } | ||||
|  | ||||
| func checkNodeValidity(n, g node, r *Report) { | ||||
| 	if err := config.AssertValid(n.Value, g.field.Tag.Get("valid")); err != nil { | ||||
| 		r.Warning(n.line, fmt.Sprintf("invalid value %v", n.Value)) | ||||
| 	} | ||||
| 	switch g.Kind() { | ||||
| 	case reflect.Struct: | ||||
| 		for _, cn := range n.children { | ||||
| 			if cg := g.Child(cn.name); cg.IsValid() { | ||||
| 				checkNodeValidity(cn, cg, r) | ||||
| 			} | ||||
| 		} | ||||
| 	case reflect.Slice: | ||||
| 		for _, cn := range n.children { | ||||
| 			var cg node | ||||
| 			c := g.Type().Elem() | ||||
| 			toNode(reflect.New(c).Elem().Interface(), context{}, &cg) | ||||
| 			checkNodeValidity(cn, cg, r) | ||||
| 		} | ||||
| 	case reflect.String, reflect.Int, reflect.Bool: | ||||
| 	default: | ||||
| 		panic(fmt.Sprintf("checkNodeValidity(): unhandled kind %s", g.Kind())) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // isCompatible determines if the type of kind n can be converted to the type | ||||
| // of kind g in the context of YAML. This is not an exhaustive list, but its | ||||
| // enough for the purposes of cloud-config validation. | ||||
| func isCompatible(n, g reflect.Kind) bool { | ||||
| 	switch g { | ||||
| 	case reflect.String: | ||||
| 		return n == reflect.String || n == reflect.Int || n == reflect.Bool | ||||
| 	case reflect.Struct: | ||||
| 		return n == reflect.Struct || n == reflect.Map | ||||
| 	case reflect.Bool, reflect.Slice: | ||||
| 		return n == g | ||||
| 	default: | ||||
| 		panic(fmt.Sprintf("isCompatible(): unhandled kind %s", g)) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										251
									
								
								config/validate/rules_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								config/validate/rules_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,251 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestCheckStructure(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		config string | ||||
|  | ||||
| 		entries []Entry | ||||
| 	}{ | ||||
| 		{}, | ||||
|  | ||||
| 		// Test for unrecognized keys | ||||
| 		{ | ||||
| 			config:  "test:", | ||||
| 			entries: []Entry{{entryWarning, "unrecognized key \"test\"", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos:\n  etcd:\n    bad:", | ||||
| 			entries: []Entry{{entryWarning, "unrecognized key \"bad\"", 3}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config: "coreos:\n  etcd:\n    discovery: good", | ||||
| 		}, | ||||
|  | ||||
| 		// Test for error on list of nodes | ||||
| 		{ | ||||
| 			config: "coreos:\n  units:\n    - hello\n    - goodbye", | ||||
| 			entries: []Entry{ | ||||
| 				{entryWarning, "incorrect type for \"units[0]\" (want struct)", 3}, | ||||
| 				{entryWarning, "incorrect type for \"units[1]\" (want struct)", 4}, | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		// Test for incorrect types | ||||
| 		// Want boolean | ||||
| 		{ | ||||
| 			config: "coreos:\n  units:\n    - enable: true", | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos:\n  units:\n    - enable: 4", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos:\n  units:\n    - enable: bad", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos:\n  units:\n    - enable:\n        bad:", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos:\n  units:\n    - enable:\n      - bad", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}}, | ||||
| 		}, | ||||
| 		// Want string | ||||
| 		{ | ||||
| 			config: "hostname: true", | ||||
| 		}, | ||||
| 		{ | ||||
| 			config: "hostname: 4", | ||||
| 		}, | ||||
| 		{ | ||||
| 			config: "hostname: host", | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "hostname:\n  name:", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"hostname\" (want string)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "hostname:\n  - name", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"hostname\" (want string)", 1}}, | ||||
| 		}, | ||||
| 		// Want struct | ||||
| 		{ | ||||
| 			config:  "coreos: true", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos: 4", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos: hello", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config: "coreos:\n  etcd:\n    discovery: fire in the disco", | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos:\n  - hello", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}}, | ||||
| 		}, | ||||
| 		// Want []string | ||||
| 		{ | ||||
| 			config:  "ssh_authorized_keys: true", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "ssh_authorized_keys: 4", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "ssh_authorized_keys: key", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "ssh_authorized_keys:\n  key: value", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config: "ssh_authorized_keys:\n  - key", | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "ssh_authorized_keys:\n  - key: value", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys[0]\" (want string)", 2}}, | ||||
| 		}, | ||||
| 		// Want []struct | ||||
| 		{ | ||||
| 			config:  "users:\n  true", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "users:\n  4", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "users:\n  bad", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "users:\n  bad:", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config: "users:\n  - name: good", | ||||
| 		}, | ||||
| 		// Want struct within array | ||||
| 		{ | ||||
| 			config:  "users:\n  - true", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "users:\n  - name: hi\n  - true", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"users[1]\" (want struct)", 3}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "users:\n  - 4", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "users:\n  - bad", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "users:\n  - - bad", | ||||
| 			entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, tt := range tests { | ||||
| 		r := Report{} | ||||
| 		n, err := parseCloudConfig([]byte(tt.config), &r) | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 		checkStructure(n, &r) | ||||
|  | ||||
| 		if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) { | ||||
| 			t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCheckValidity(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		config string | ||||
|  | ||||
| 		entries []Entry | ||||
| 	}{ | ||||
| 		// string | ||||
| 		{ | ||||
| 			config: "hostname: test", | ||||
| 		}, | ||||
|  | ||||
| 		// int | ||||
| 		{ | ||||
| 			config: "coreos:\n  fleet:\n    verbosity: 2", | ||||
| 		}, | ||||
|  | ||||
| 		// bool | ||||
| 		{ | ||||
| 			config: "coreos:\n  units:\n    - enable: true", | ||||
| 		}, | ||||
|  | ||||
| 		// slice | ||||
| 		{ | ||||
| 			config: "coreos:\n  units:\n    - command: start\n    - name: stop", | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos:\n  units:\n    - command: lol", | ||||
| 			entries: []Entry{{entryWarning, "invalid value lol", 3}}, | ||||
| 		}, | ||||
|  | ||||
| 		// struct | ||||
| 		{ | ||||
| 			config: "coreos:\n  update:\n    reboot_strategy: off", | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "coreos:\n  update:\n    reboot_strategy: always", | ||||
| 			entries: []Entry{{entryWarning, "invalid value always", 3}}, | ||||
| 		}, | ||||
|  | ||||
| 		// unknown | ||||
| 		{ | ||||
| 			config: "unknown: hi", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, tt := range tests { | ||||
| 		r := Report{} | ||||
| 		n, err := parseCloudConfig([]byte(tt.config), &r) | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 		checkValidity(n, &r) | ||||
|  | ||||
| 		if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) { | ||||
| 			t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										113
									
								
								config/validate/validate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								config/validate/validate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	yamlLineError = regexp.MustCompile(`^YAML error: line (?P<line>[[:digit:]]+): (?P<msg>.*)$`) | ||||
| 	yamlError     = regexp.MustCompile(`^YAML error: (?P<msg>.*)$`) | ||||
| ) | ||||
|  | ||||
| // Validate runs a series of validation tests against the given userdata and | ||||
| // returns a report detailing all of the issues. Presently, only cloud-configs | ||||
| // can be validated. | ||||
| func Validate(userdataBytes []byte) (Report, error) { | ||||
| 	switch { | ||||
| 	case config.IsScript(string(userdataBytes)): | ||||
| 		return Report{}, nil | ||||
| 	case config.IsCloudConfig(string(userdataBytes)): | ||||
| 		return validateCloudConfig(userdataBytes, Rules) | ||||
| 	default: | ||||
| 		return Report{entries: []Entry{ | ||||
| 			Entry{kind: entryError, message: `must be "#cloud-config" or begin with "#!"`}, | ||||
| 		}}, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // validateCloudConfig runs all of the validation rules in Rules and returns | ||||
| // the resulting report and any errors encountered. | ||||
| func validateCloudConfig(config []byte, rules []rule) (report Report, err error) { | ||||
| 	defer func() { | ||||
| 		if r := recover(); r != nil { | ||||
| 			err = fmt.Errorf("%v", r) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	c, err := parseCloudConfig(config, &report) | ||||
| 	if err != nil { | ||||
| 		return report, err | ||||
| 	} | ||||
|  | ||||
| 	c = normalizeNodeNames(c, &report) | ||||
| 	for _, r := range rules { | ||||
| 		r(c, &report) | ||||
| 	} | ||||
| 	return report, nil | ||||
| } | ||||
|  | ||||
| // parseCloudConfig parses the provided config into a node structure and logs | ||||
| // any parsing issues into the provided report. Unrecoverable errors are | ||||
| // returned as an error. | ||||
| func parseCloudConfig(config []byte, report *Report) (n node, err error) { | ||||
| 	var raw map[interface{}]interface{} | ||||
| 	if err := yaml.Unmarshal(config, &raw); err != nil { | ||||
| 		matches := yamlLineError.FindStringSubmatch(err.Error()) | ||||
| 		if len(matches) == 3 { | ||||
| 			line, err := strconv.Atoi(matches[1]) | ||||
| 			if err != nil { | ||||
| 				return n, err | ||||
| 			} | ||||
| 			msg := matches[2] | ||||
| 			report.Error(line, msg) | ||||
| 			return n, nil | ||||
| 		} | ||||
|  | ||||
| 		matches = yamlError.FindStringSubmatch(err.Error()) | ||||
| 		if len(matches) == 2 { | ||||
| 			report.Error(1, matches[1]) | ||||
| 			return n, nil | ||||
| 		} | ||||
|  | ||||
| 		return n, errors.New("couldn't parse yaml error") | ||||
| 	} | ||||
|  | ||||
| 	return NewNode(raw, NewContext(config)), nil | ||||
| } | ||||
|  | ||||
| // normalizeNodeNames replaces all occurences of '-' with '_' within key names | ||||
| // and makes a note of each replacement in the report. | ||||
| func normalizeNodeNames(node node, report *Report) node { | ||||
| 	if strings.Contains(node.name, "-") { | ||||
| 		report.Info(node.line, fmt.Sprintf("%q uses '-' instead of '_'", node.name)) | ||||
| 		node.name = strings.Replace(node.name, "-", "_", -1) | ||||
| 	} | ||||
| 	for i := range node.children { | ||||
| 		node.children[i] = normalizeNodeNames(node.children[i], report) | ||||
| 	} | ||||
| 	return node | ||||
| } | ||||
							
								
								
									
										121
									
								
								config/validate/validate_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								config/validate/validate_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| /* | ||||
|    Copyright 2014 CoreOS, Inc. | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
|  | ||||
| package validate | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestParseCloudConfig(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		config string | ||||
|  | ||||
| 		entries []Entry | ||||
| 	}{ | ||||
| 		{}, | ||||
| 		{ | ||||
| 			config: "	", | ||||
| 			entries: []Entry{{entryError, "found character that cannot start any token", 1}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "a:\na", | ||||
| 			entries: []Entry{{entryError, "could not find expected ':'", 2}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			config:  "#hello\na:\na", | ||||
| 			entries: []Entry{{entryError, "could not find expected ':'", 3}}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		r := Report{} | ||||
| 		parseCloudConfig([]byte(tt.config), &r) | ||||
|  | ||||
| 		if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) { | ||||
| 			t.Errorf("bad report (%s): want %#v, got %#v", tt.config, tt.entries, e) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValidateCloudConfig(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		config string | ||||
| 		rules  []rule | ||||
|  | ||||
| 		report Report | ||||
| 		err    error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			rules: []rule{func(_ node, _ *Report) { panic("something happened") }}, | ||||
| 			err:   errors.New("something happened"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		r, err := validateCloudConfig([]byte(tt.config), tt.rules) | ||||
| 		if !reflect.DeepEqual(tt.err, err) { | ||||
| 			t.Errorf("bad error (%s): want %v, got %v", tt.config, tt.err, err) | ||||
| 		} | ||||
| 		if !reflect.DeepEqual(tt.report, r) { | ||||
| 			t.Errorf("bad report (%s): want %+v, got %+v", tt.config, tt.report, r) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func BenchmarkValidate(b *testing.B) { | ||||
| 	config := `#cloud-config | ||||
| hostname: test | ||||
|  | ||||
| coreos: | ||||
|   etcd: | ||||
|     name:      node001 | ||||
|     discovery: https://discovery.etcd.io/disco | ||||
|     addr:      $public_ipv4:4001 | ||||
|     peer-addr: $private_ipv4:7001 | ||||
|   fleet: | ||||
|     verbosity: 2 | ||||
|     metadata:  "hi" | ||||
|   update: | ||||
|     reboot-strategy: off | ||||
|   units: | ||||
|     - name:    hi.service | ||||
|       command: start | ||||
|       enable:  true | ||||
|     - name:    bye.service | ||||
|       command: stop | ||||
|  | ||||
| ssh_authorized_keys: | ||||
|   - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h... | ||||
|   - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h... | ||||
|  | ||||
| users: | ||||
|   - name: me | ||||
|  | ||||
| write_files: | ||||
|   - path: /etc/yes | ||||
|     content: "Hi" | ||||
|  | ||||
| manage_etc_hosts: localhost` | ||||
|  | ||||
| 	for i := 0; i < b.N; i++ { | ||||
| 		if _, err := Validate([]byte(config)); err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user