1// Copyright 2019 Istio Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package structpath 16 17import ( 18 "bytes" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "reflect" 23 "regexp" 24 "strings" 25 26 "github.com/gogo/protobuf/proto" 27 "github.com/golang/protobuf/jsonpb" 28 29 "gopkg.in/d4l3k/messagediff.v1" 30 31 "istio.io/istio/pkg/test" 32 33 "k8s.io/client-go/util/jsonpath" 34) 35 36var ( 37 fixupNumericJSONComparison = regexp.MustCompile(`([=<>]+)\s*([0-9]+)\s*\)`) 38 fixupAttributeReference = regexp.MustCompile(`\[\s*'[^']+\s*'\s*]`) 39) 40 41type Instance struct { 42 structure interface{} 43 isJSON bool 44 constraints []constraint 45 creationError error 46} 47 48type constraint func() error 49 50// ForProto creates a structpath Instance by marshaling the proto to JSON and then evaluating over that 51// structure. This is the most generally useful form as serialization to JSON also automatically 52// converts proto.Any and proto.Struct to the serialized JSON forms which can then be evaluated 53// over. The downside is the loss of type fidelity for numeric types as JSON can only represent 54// floats. 55func ForProto(proto proto.Message) *Instance { 56 if proto == nil { 57 return newErrorInstance(errors.New("expected non-nil proto")) 58 } 59 60 parsed, err := protoToParsedJSON(proto) 61 if err != nil { 62 return newErrorInstance(err) 63 } 64 65 i := &Instance{ 66 isJSON: true, 67 structure: parsed, 68 } 69 i.structure = parsed 70 return i 71} 72 73func newErrorInstance(err error) *Instance { 74 return &Instance{ 75 isJSON: true, 76 creationError: err, 77 } 78} 79 80func protoToParsedJSON(message proto.Message) (interface{}, error) { 81 // Convert proto to json and then parse into struct 82 jsonText, err := (&jsonpb.Marshaler{Indent: " "}).MarshalToString(message) 83 if err != nil { 84 return nil, fmt.Errorf("failed to convert proto to JSON: %v", err) 85 } 86 var parsed interface{} 87 err = json.Unmarshal([]byte(jsonText), &parsed) 88 if err != nil { 89 return nil, fmt.Errorf("failed to parse into JSON struct: %v", err) 90 } 91 return parsed, nil 92} 93 94func (i *Instance) Select(path string, args ...interface{}) *Instance { 95 if i.creationError != nil { 96 // There was an error during the creation of this Instance. Just return the 97 // same instance since it will error on Check anyway. 98 return i 99 } 100 101 path = fmt.Sprintf(path, args...) 102 value, err := i.findValue(path) 103 if err != nil { 104 return newErrorInstance(err) 105 } 106 if value == nil { 107 return newErrorInstance(fmt.Errorf("cannot select non-existent path: %v", path)) 108 } 109 110 // Success. 111 return &Instance{ 112 isJSON: i.isJSON, 113 structure: value, 114 } 115} 116 117func (i *Instance) appendConstraint(fn func() error) *Instance { 118 i.constraints = append(i.constraints, fn) 119 return i 120} 121 122func (i *Instance) Equals(expected interface{}, path string, args ...interface{}) *Instance { 123 path = fmt.Sprintf(path, args...) 124 return i.appendConstraint(func() error { 125 typeOf := reflect.TypeOf(expected) 126 protoMessageType := reflect.TypeOf((*proto.Message)(nil)).Elem() 127 if typeOf.Implements(protoMessageType) { 128 return i.equalsStruct(expected.(proto.Message), path) 129 } 130 switch kind := typeOf.Kind(); kind { 131 case reflect.String: 132 return i.equalsString(reflect.ValueOf(expected).String(), path) 133 case reflect.Bool: 134 return i.equalsBool(reflect.ValueOf(expected).Bool(), path) 135 case reflect.Float32, reflect.Float64: 136 return i.equalsNumber(reflect.ValueOf(expected).Float(), path) 137 case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64: 138 return i.equalsNumber(float64(reflect.ValueOf(expected).Int()), path) 139 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 140 return i.equalsNumber(float64(reflect.ValueOf(expected).Uint()), path) 141 case protoMessageType.Kind(): 142 } 143 // TODO: Add struct support 144 return fmt.Errorf("attempt to call Equals for unsupported type: %v", expected) 145 }) 146} 147 148func (i *Instance) ContainSubstring(substr, path string) *Instance { 149 return i.appendConstraint(func() error { 150 value, err := i.execute(path) 151 if err != nil { 152 return err 153 } 154 if found := strings.Contains(value, substr); !found { 155 return fmt.Errorf("substring %v did not match: %v", substr, value) 156 } 157 return nil 158 }) 159} 160 161func (i *Instance) equalsString(expected string, path string) error { 162 value, err := i.execute(path) 163 if err != nil { 164 return err 165 } 166 if value != expected { 167 return fmt.Errorf("expected %v but got %v for path %v", expected, value, path) 168 } 169 return nil 170} 171 172func (i *Instance) equalsNumber(expected float64, path string) error { 173 v, err := i.findValue(path) 174 if err != nil { 175 return err 176 } 177 result := reflect.ValueOf(v).Float() 178 if result != expected { 179 return fmt.Errorf("expected %v but got %v for path %v", expected, result, path) 180 } 181 return nil 182} 183 184func (i *Instance) equalsBool(expected bool, path string) error { 185 v, err := i.findValue(path) 186 if err != nil { 187 return err 188 } 189 result := reflect.ValueOf(v).Bool() 190 if result != expected { 191 return fmt.Errorf("expected %v but got %v for path %v", expected, result, path) 192 } 193 return nil 194} 195 196func (i *Instance) equalsStruct(proto proto.Message, path string) error { 197 jsonStruct, err := protoToParsedJSON(proto) 198 if err != nil { 199 return err 200 } 201 v, err := i.findValue(path) 202 if err != nil { 203 return err 204 } 205 diff, b := messagediff.PrettyDiff(reflect.ValueOf(v).Interface(), jsonStruct) 206 if !b { 207 return fmt.Errorf("structs did not match: %v", diff) 208 } 209 return nil 210} 211 212func (i *Instance) Exists(path string, args ...interface{}) *Instance { 213 path = fmt.Sprintf(path, args...) 214 return i.appendConstraint(func() error { 215 v, err := i.findValue(path) 216 if err != nil { 217 return err 218 } 219 if v == nil { 220 return fmt.Errorf("no entry exists at path: %v", path) 221 } 222 return nil 223 }) 224} 225 226func (i *Instance) NotExists(path string, args ...interface{}) *Instance { 227 path = fmt.Sprintf(path, args...) 228 return i.appendConstraint(func() error { 229 parser := jsonpath.New("path") 230 err := parser.Parse(i.fixPath(path)) 231 if err != nil { 232 return fmt.Errorf("invalid path: %v - %v", path, err) 233 } 234 values, err := parser.AllowMissingKeys(true).FindResults(i.structure) 235 if err != nil { 236 return fmt.Errorf("err finding results for path: %v - %v", path, err) 237 } 238 if len(values[0]) > 0 { 239 return fmt.Errorf("expected no result but got: %v for path: %v", values[0], path) 240 } 241 return nil 242 }) 243} 244 245// Check executes the set of constraints for this selection 246// and returns the first error encountered, or nil if all constraints 247// have been successfully met. All constraints are removed after them 248// check is performed. 249func (i *Instance) Check() error { 250 // After the check completes, clear out the constraints. 251 defer func() { 252 i.constraints = i.constraints[:0] 253 }() 254 255 // If there was a creation error, just return that immediately. 256 if i.creationError != nil { 257 return i.creationError 258 } 259 260 for _, c := range i.constraints { 261 if err := c(); err != nil { 262 return err 263 } 264 } 265 return nil 266} 267 268// CheckOrFail calls Check on this selection and fails the given test if an 269// error is encountered. 270func (i *Instance) CheckOrFail(t test.Failer) *Instance { 271 t.Helper() 272 if err := i.Check(); err != nil { 273 t.Fatal(err) 274 } 275 return i 276} 277 278func (i *Instance) execute(path string) (string, error) { 279 parser := jsonpath.New("path") 280 err := parser.Parse(i.fixPath(path)) 281 if err != nil { 282 return "", fmt.Errorf("invalid path: %v - %v", path, err) 283 } 284 buf := new(bytes.Buffer) 285 err = parser.Execute(buf, i.structure) 286 if err != nil { 287 return "", fmt.Errorf("err finding results for path: %v - %v", path, err) 288 } 289 return buf.String(), nil 290} 291 292func (i *Instance) findValue(path string) (interface{}, error) { 293 parser := jsonpath.New("path") 294 err := parser.Parse(i.fixPath(path)) 295 if err != nil { 296 return nil, fmt.Errorf("invalid path: %v - %v", path, err) 297 } 298 values, err := parser.FindResults(i.structure) 299 if err != nil { 300 return nil, fmt.Errorf("err finding results for path: %v - %v", path, err) 301 } 302 if len(values) == 0 || len(values[0]) == 0 { 303 return nil, fmt.Errorf("no value for path: %v", path) 304 } 305 return values[0][0].Interface(), nil 306} 307 308// Fixes up some quirks in jsonpath handling. 309// See https://github.com/kubernetes/client-go/issues/553 310func (i *Instance) fixPath(path string) string { 311 // jsonpath doesn't handle numeric comparisons in a tolerant way. All json numbers are floats 312 // and filter expressions on the form {.x[?(@.some.value==123]} won't work but 313 // {.x[?(@.some.value==123.0]} will. 314 result := path 315 if i.isJSON { 316 template := "$1$2.0)" 317 result = fixupNumericJSONComparison.ReplaceAllString(path, template) 318 } 319 // jsonpath doesn't like map literal references that contain periods. I.e 320 // you can't do x['user.map'] but x.user\.map works so we just translate to that 321 result = string(fixupAttributeReference.ReplaceAllFunc([]byte(result), func(i []byte) []byte { 322 input := string(i) 323 input = strings.Replace(input, "[", "", 1) 324 input = strings.Replace(input, "]", "", 1) 325 input = strings.Replace(input, "'", "", 2) 326 parts := strings.Split(input, ".") 327 output := "." 328 for i := 0; i < len(parts)-1; i++ { 329 output += parts[i] 330 output += "\\." 331 } 332 output += parts[len(parts)-1] 333 return []byte(output) 334 })) 335 336 return result 337} 338