forked from coreos/ignition
-
Notifications
You must be signed in to change notification settings - Fork 0
/
validate.go
242 lines (214 loc) · 7.69 KB
/
validate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// Copyright 2015 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"
"fmt"
"io"
"reflect"
"strings"
json "github.com/ajeddeloh/go-json"
"github.com/coreos/ignition/config/validate/astjson"
"github.com/coreos/ignition/config/validate/astnode"
"github.com/coreos/ignition/config/validate/report"
)
type validator interface {
Validate() report.Report
}
// ValidateConfig validates a raw config object into a given config version
func ValidateConfig(rawConfig []byte, config interface{}) report.Report {
// Unmarshal again to a json.Node to get offset information for building a report
var ast json.Node
var r report.Report
configValue := reflect.ValueOf(config)
if err := json.Unmarshal(rawConfig, &ast); err != nil {
r.Add(report.Entry{
Kind: report.EntryWarning,
Message: "Ignition could not unmarshal your config for reporting line numbers. This should never happen. Please file a bug.",
})
r.Merge(ValidateWithoutSource(configValue))
} else {
r.Merge(Validate(configValue, astjson.FromJsonRoot(ast), bytes.NewReader(rawConfig), true))
}
return r
}
// Validate walks down a struct tree calling Validate on every node that implements it, building
// A report of all the errors, warnings, info, and deprecations it encounters. If checkUnusedKeys
// is true, Validate will generate warnings for unused keys in the ast, otherwise it will not.
func Validate(vObj reflect.Value, ast astnode.AstNode, source io.ReadSeeker, checkUnusedKeys bool) (r report.Report) {
if !vObj.IsValid() {
return
}
line, col, highlight := 0, 0, ""
if ast != nil {
line, col, highlight = ast.ValueLineCol(source)
}
// See if we A) can call Validate on vObj, and B) should call Validate. Validate should NOT be called
// when vObj is nil, as it will panic or when vObj is a pointer to a value with Validate implemented with a
// value receiver. This is to prevent Validate being called twice, as otherwise it would be called on the
// pointer version (due to go's automatic deferencing) and once when the pointer is deferenced below. The only
// time Validate should be called on a pointer is when the function is implemented with a pointer reciever.
if obj, ok := vObj.Interface().(validator); ok &&
((vObj.Kind() != reflect.Ptr) ||
(!vObj.IsNil() && !vObj.Elem().Type().Implements(reflect.TypeOf((*validator)(nil)).Elem()))) {
sub_r := obj.Validate()
sub_r.AddPosition(line, col, highlight)
r.Merge(sub_r)
// Dont recurse on invalid inner nodes, it mostly leads to bogus messages
if sub_r.IsFatal() {
return
}
}
switch vObj.Kind() {
case reflect.Ptr:
sub_report := Validate(vObj.Elem(), ast, source, checkUnusedKeys)
sub_report.AddPosition(line, col, "")
r.Merge(sub_report)
case reflect.Struct:
sub_report := validateStruct(vObj, ast, source, checkUnusedKeys)
sub_report.AddPosition(line, col, "")
r.Merge(sub_report)
case reflect.Slice:
for i := 0; i < vObj.Len(); i++ {
sub_node := ast
if ast != nil {
if n, ok := ast.SliceChild(i); ok {
sub_node = n
}
}
sub_report := Validate(vObj.Index(i), sub_node, source, checkUnusedKeys)
sub_report.AddPosition(line, col, "")
r.Merge(sub_report)
}
}
return
}
func ValidateWithoutSource(cfg reflect.Value) (report report.Report) {
return Validate(cfg, nil, nil, false)
}
type field struct {
Type reflect.StructField
Value reflect.Value
}
// getFields returns a field of all the fields in the struct, including the fields of
// embedded structs and structs inside interface{}'s
func getFields(vObj reflect.Value) []field {
if vObj.Kind() != reflect.Struct {
return nil
}
ret := []field{}
for i := 0; i < vObj.Type().NumField(); i++ {
if vObj.Type().Field(i).Anonymous {
// in the case of an embedded type that is an alias to interface, extract the
// real type contained by the interface
realObj := reflect.ValueOf(vObj.Field(i).Interface())
ret = append(ret, getFields(realObj)...)
} else {
ret = append(ret, field{Type: vObj.Type().Field(i), Value: vObj.Field(i)})
}
}
return ret
}
func validateStruct(vObj reflect.Value, ast astnode.AstNode, source io.ReadSeeker, checkUnusedKeys bool) report.Report {
r := report.Report{}
// isFromObject will be true if this struct was unmarshalled from a JSON object.
keys, isFromObject := map[string]astnode.AstNode{}, false
if ast != nil {
keys, isFromObject = ast.KeyValueMap()
}
// Maintain a set of key's that have been used.
usedKeys := map[string]struct{}{}
// Maintain a list of all the tags in the struct for fuzzy matching later.
tags := []string{}
for _, f := range getFields(vObj) {
// Default to nil astnode.AstNode if the field's corrosponding node cannot be found.
var sub_node astnode.AstNode
// Default to passing a nil source if the field's corrosponding node cannot be found.
// This ensures the line numbers reported from all sub-structs are 0 and will be changed by AddPosition
var src io.ReadSeeker
// Try to determine the json.Node that corrosponds with the struct field
if isFromObject {
tag := strings.SplitN(f.Type.Tag.Get(ast.Tag()), ",", 2)[0]
// Save the tag so we have a list of all the tags in the struct
tags = append(tags, tag)
// mark that this key was used
usedKeys[tag] = struct{}{}
if sub, ok := keys[tag]; ok {
// Found it
sub_node = sub
src = source
}
}
// Default to deepest node if the node's type isn't an object,
// such as when a json string actually unmarshal to structs (like with version)
line, col := 0, 0
highlight := ""
if ast != nil {
line, col, highlight = ast.ValueLineCol(src)
}
// If there's a Validate<Name> func for the given field, call it
funct := vObj.MethodByName("Validate" + f.Type.Name)
if funct.IsValid() {
if sub_node != nil {
// if sub_node is non-nil, we can get better line/col info
line, col, highlight = sub_node.ValueLineCol(src)
}
res := funct.Call(nil)
sub_report := res[0].Interface().(report.Report)
sub_report.AddPosition(line, col, highlight)
r.Merge(sub_report)
}
sub_report := Validate(f.Value, sub_node, src, checkUnusedKeys)
sub_report.AddPosition(line, col, highlight)
r.Merge(sub_report)
}
if !isFromObject || !checkUnusedKeys {
// If this struct was not unmarshalled from a JSON object, there cannot be unused keys.
return r
}
for k, v := range keys {
if _, hasKey := usedKeys[k]; hasKey {
continue
}
line, col, highlight := v.KeyLineCol(source)
typo := similar(k, tags)
r.Add(report.Entry{
Kind: report.EntryWarning,
Message: fmt.Sprintf("Config has unrecognized key: %s", k),
Line: line,
Column: col,
Highlight: highlight,
})
if typo != "" {
r.Add(report.Entry{
Kind: report.EntryInfo,
Message: fmt.Sprintf("Did you mean %s instead of %s", typo, k),
Line: line,
Column: col,
Highlight: highlight,
})
}
}
return r
}
// similar returns a string in candidates that is similar to str. Currently it just does case
// insensitive comparison, but it should be updated to use levenstein distances to catch typos
func similar(str string, candidates []string) string {
for _, candidate := range candidates {
if strings.EqualFold(str, candidate) {
return candidate
}
}
return ""
}