Merge pull request #115694 from mpuckett159/fix/explain-jsonpath
Fix/explain jsonpath
This commit is contained in:
		@@ -17,51 +17,85 @@ limitations under the License.
 | 
				
			|||||||
package explain
 | 
					package explain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/kube-openapi/pkg/util/proto"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"k8s.io/apimachinery/pkg/api/meta"
 | 
						"k8s.io/apimachinery/pkg/api/meta"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/runtime/schema"
 | 
						"k8s.io/apimachinery/pkg/runtime/schema"
 | 
				
			||||||
 | 
						"k8s.io/client-go/util/jsonpath"
 | 
				
			||||||
 | 
						"k8s.io/kube-openapi/pkg/util/proto"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type fieldsPrinter interface {
 | 
					type fieldsPrinter interface {
 | 
				
			||||||
	PrintFields(proto.Schema) error
 | 
						PrintFields(proto.Schema) error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func splitDotNotation(model string) (string, []string) {
 | 
					// jsonPathParse gets back the inner list of nodes we want to work with
 | 
				
			||||||
	var fieldsPath []string
 | 
					func jsonPathParse(in string) ([]jsonpath.Node, error) {
 | 
				
			||||||
 | 
						// Remove trailing period just in case
 | 
				
			||||||
 | 
						in = strings.TrimSuffix(in, ".")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ignore trailing period
 | 
						// Define initial jsonpath Parser
 | 
				
			||||||
	model = strings.TrimSuffix(model, ".")
 | 
						jpp, err := jsonpath.Parse("user", "{."+in+"}")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
	dotModel := strings.Split(model, ".")
 | 
							return nil, err
 | 
				
			||||||
	if len(dotModel) >= 1 {
 | 
					 | 
				
			||||||
		fieldsPath = dotModel[1:]
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return dotModel[0], fieldsPath
 | 
					
 | 
				
			||||||
 | 
						// Because of the way the jsonpath library works, the schema of the parser is [][]NodeList
 | 
				
			||||||
 | 
						// meaning we need to get the outer node list, make sure it's only length 1, then get the inner node
 | 
				
			||||||
 | 
						// list, and only then can we look at the individual nodes themselves.
 | 
				
			||||||
 | 
						outerNodeList := jpp.Root.Nodes
 | 
				
			||||||
 | 
						if len(outerNodeList) != 1 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("must pass in 1 jsonpath string")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The root node is always a list node so this type assertion is safe
 | 
				
			||||||
 | 
						return outerNodeList[0].(*jsonpath.ListNode).Nodes, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SplitAndParseResourceRequest separates the users input into a model and fields
 | 
					// SplitAndParseResourceRequest separates the users input into a model and fields
 | 
				
			||||||
func SplitAndParseResourceRequest(inResource string, mapper meta.RESTMapper) (schema.GroupVersionResource, []string, error) {
 | 
					func SplitAndParseResourceRequest(inResource string, mapper meta.RESTMapper) (schema.GroupVersionResource, []string, error) {
 | 
				
			||||||
	inResource, fieldsPath := splitDotNotation(inResource)
 | 
						inResourceNodeList, err := jsonPathParse(inResource)
 | 
				
			||||||
	gvr, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: inResource})
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return schema.GroupVersionResource{}, nil, err
 | 
							return schema.GroupVersionResource{}, nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if inResourceNodeList[0].Type() != jsonpath.NodeField {
 | 
				
			||||||
 | 
							return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						resource := inResourceNodeList[0].(*jsonpath.FieldNode).Value
 | 
				
			||||||
 | 
						gvr, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: resource})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return schema.GroupVersionResource{}, nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var fieldsPath []string
 | 
				
			||||||
 | 
						for _, node := range inResourceNodeList[1:] {
 | 
				
			||||||
 | 
							if node.Type() != jsonpath.NodeField {
 | 
				
			||||||
 | 
								return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, all nodes must be field nodes")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							fieldsPath = append(fieldsPath, node.(*jsonpath.FieldNode).Value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return gvr, fieldsPath, nil
 | 
						return gvr, fieldsPath, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SplitAndParseResourceRequestWithMatchingPrefix separates the users input into a model and fields
 | 
					// SplitAndParseResourceRequestWithMatchingPrefix separates the users input into a model and fields
 | 
				
			||||||
// while selecting gvr whose (resource, group) prefix matches the resource
 | 
					// while selecting gvr whose (resource, group) prefix matches the resource
 | 
				
			||||||
func SplitAndParseResourceRequestWithMatchingPrefix(inResource string, mapper meta.RESTMapper) (gvr schema.GroupVersionResource, fieldsPath []string, err error) {
 | 
					func SplitAndParseResourceRequestWithMatchingPrefix(inResource string, mapper meta.RESTMapper) (gvr schema.GroupVersionResource, fieldsPath []string, err error) {
 | 
				
			||||||
	// ignore trailing period
 | 
						inResourceNodeList, err := jsonPathParse(inResource)
 | 
				
			||||||
	inResource = strings.TrimSuffix(inResource, ".")
 | 
						if err != nil {
 | 
				
			||||||
	dotParts := strings.Split(inResource, ".")
 | 
							return schema.GroupVersionResource{}, nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gvrs, err := mapper.ResourcesFor(schema.GroupVersionResource{Resource: dotParts[0]})
 | 
						// Get resource from first node of jsonpath
 | 
				
			||||||
 | 
						if inResourceNodeList[0].Type() != jsonpath.NodeField {
 | 
				
			||||||
 | 
							return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						resource := inResourceNodeList[0].(*jsonpath.FieldNode).Value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gvrs, err := mapper.ResourcesFor(schema.GroupVersionResource{Resource: resource})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return schema.GroupVersionResource{}, nil, err
 | 
							return schema.GroupVersionResource{}, nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -71,10 +105,22 @@ func SplitAndParseResourceRequestWithMatchingPrefix(inResource string, mapper me
 | 
				
			|||||||
		groupResource := gvrItem.GroupResource().String()
 | 
							groupResource := gvrItem.GroupResource().String()
 | 
				
			||||||
		if strings.HasPrefix(inResource, groupResource) {
 | 
							if strings.HasPrefix(inResource, groupResource) {
 | 
				
			||||||
			resourceSuffix := inResource[len(groupResource):]
 | 
								resourceSuffix := inResource[len(groupResource):]
 | 
				
			||||||
 | 
								var fieldsPath []string
 | 
				
			||||||
			if len(resourceSuffix) > 0 {
 | 
								if len(resourceSuffix) > 0 {
 | 
				
			||||||
				dotParts := strings.Split(resourceSuffix, ".")
 | 
									// Define another jsonpath Parser for the resource suffix
 | 
				
			||||||
				if len(dotParts) > 0 {
 | 
									resourceSuffixNodeList, err := jsonPathParse(resourceSuffix)
 | 
				
			||||||
					fieldsPath = dotParts[1:]
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return schema.GroupVersionResource{}, nil, err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if len(resourceSuffixNodeList) > 0 {
 | 
				
			||||||
 | 
										nodeList := resourceSuffixNodeList[1:]
 | 
				
			||||||
 | 
										for _, node := range nodeList {
 | 
				
			||||||
 | 
											if node.Type() != jsonpath.NodeField {
 | 
				
			||||||
 | 
												return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node")
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											fieldsPath = append(fieldsPath, node.(*jsonpath.FieldNode).Value)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return gvrItem, fieldsPath, nil
 | 
								return gvrItem, fieldsPath, nil
 | 
				
			||||||
@@ -82,9 +128,21 @@ func SplitAndParseResourceRequestWithMatchingPrefix(inResource string, mapper me
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// If no match, take the first (the highest priority) gvr
 | 
						// If no match, take the first (the highest priority) gvr
 | 
				
			||||||
 | 
						fieldsPath = []string{}
 | 
				
			||||||
	if len(gvrs) > 0 {
 | 
						if len(gvrs) > 0 {
 | 
				
			||||||
		gvr = gvrs[0]
 | 
							gvr = gvrs[0]
 | 
				
			||||||
		_, fieldsPath = splitDotNotation(inResource)
 | 
					
 | 
				
			||||||
 | 
							fieldsPathNodeList, err := jsonPathParse(inResource)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return schema.GroupVersionResource{}, nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, node := range fieldsPathNodeList[1:] {
 | 
				
			||||||
 | 
								if node.Type() != jsonpath.NodeField {
 | 
				
			||||||
 | 
									return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								fieldsPath = append(fieldsPath, node.(*jsonpath.FieldNode).Value)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return gvr, fieldsPath, nil
 | 
						return gvr, fieldsPath, nil
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,6 +48,20 @@ func TestSplitAndParseResourceRequest(t *testing.T) {
 | 
				
			|||||||
			expectedGVR:        schema.GroupVersionResource{Resource: "services", Version: "v1"},
 | 
								expectedGVR:        schema.GroupVersionResource{Resource: "services", Version: "v1"},
 | 
				
			||||||
			expectedFieldsPath: []string{"field2", "field3"},
 | 
								expectedFieldsPath: []string{"field2", "field3"},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "field with dots 1",
 | 
				
			||||||
 | 
								inResource: `service.field2['field\.with\.dots']`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectedGVR:        schema.GroupVersionResource{Resource: "services", Version: "v1"},
 | 
				
			||||||
 | 
								expectedFieldsPath: []string{"field2", "field.with.dots"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "field with dots 2",
 | 
				
			||||||
 | 
								inResource: `service.field2.field\.with\.dots`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectedGVR:        schema.GroupVersionResource{Resource: "services", Version: "v1"},
 | 
				
			||||||
 | 
								expectedFieldsPath: []string{"field2", "field.with.dots"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name:       "trailing period with incorrect fieldsPath",
 | 
								name:       "trailing period with incorrect fieldsPath",
 | 
				
			||||||
			inResource: "node.field2.field3.",
 | 
								inResource: "node.field2.field3.",
 | 
				
			||||||
@@ -76,3 +90,69 @@ func TestSplitAndParseResourceRequest(t *testing.T) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestSplitAndParseResourceRequestWithMatchingPrefix(t *testing.T) {
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name       string
 | 
				
			||||||
 | 
							inResource string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							expectedGVR        schema.GroupVersionResource
 | 
				
			||||||
 | 
							expectedFieldsPath []string
 | 
				
			||||||
 | 
							expectedErr        bool
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "no trailing period",
 | 
				
			||||||
 | 
								inResource: "pods.field2.field3",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectedGVR:        schema.GroupVersionResource{Resource: "pods", Version: "v1"},
 | 
				
			||||||
 | 
								expectedFieldsPath: []string{"field2", "field3"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "trailing period with correct fieldsPath",
 | 
				
			||||||
 | 
								inResource: "service.field2.field3.",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectedGVR:        schema.GroupVersionResource{Resource: "services", Version: "v1"},
 | 
				
			||||||
 | 
								expectedFieldsPath: []string{"field2", "field3"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "field with dots 1",
 | 
				
			||||||
 | 
								inResource: `service.field2['field\.with\.dots']`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectedGVR:        schema.GroupVersionResource{Resource: "services", Version: "v1"},
 | 
				
			||||||
 | 
								expectedFieldsPath: []string{"field2", "field.with.dots"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "field with dots 2",
 | 
				
			||||||
 | 
								inResource: `service.field2.field\.with\.dots`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectedGVR:        schema.GroupVersionResource{Resource: "services", Version: "v1"},
 | 
				
			||||||
 | 
								expectedFieldsPath: []string{"field2", "field.with.dots"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:       "trailing period with incorrect fieldsPath",
 | 
				
			||||||
 | 
								inResource: "node.field2.field3.",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectedGVR:        schema.GroupVersionResource{Resource: "nodes", Version: "v1"},
 | 
				
			||||||
 | 
								expectedFieldsPath: []string{"field2", "field3", ""},
 | 
				
			||||||
 | 
								expectedErr:        true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mapper := testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...)
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								gotGVR, gotFieldsPath, err := SplitAndParseResourceRequestWithMatchingPrefix(tt.inResource, mapper)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Errorf("unexpected error: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if !reflect.DeepEqual(tt.expectedGVR, gotGVR) && !tt.expectedErr {
 | 
				
			||||||
 | 
									t.Errorf("%s: expected inResource: %s, got: %s", tt.name, tt.expectedGVR, gotGVR)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if !reflect.DeepEqual(tt.expectedFieldsPath, gotFieldsPath) && !tt.expectedErr {
 | 
				
			||||||
 | 
									t.Errorf("%s: expected fieldsPath: %s, got: %s", tt.name, tt.expectedFieldsPath, gotFieldsPath)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user