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