mirror of
				https://github.com/kubernetes/client-go.git
				synced 2025-10-31 05:40:57 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			372 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			372 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2015 The Kubernetes Authors.
 | |
| 
 | |
| 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 jsonpath
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"reflect"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| )
 | |
| 
 | |
| type jsonpathTest struct {
 | |
| 	name        string
 | |
| 	template    string
 | |
| 	input       interface{}
 | |
| 	expect      string
 | |
| 	expectError bool
 | |
| }
 | |
| 
 | |
| func testJSONPath(tests []jsonpathTest, allowMissingKeys bool, t *testing.T) {
 | |
| 	for _, test := range tests {
 | |
| 		j := New(test.name)
 | |
| 		j.AllowMissingKeys(allowMissingKeys)
 | |
| 		err := j.Parse(test.template)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
 | |
| 		}
 | |
| 		buf := new(bytes.Buffer)
 | |
| 		err = j.Execute(buf, test.input)
 | |
| 		if test.expectError {
 | |
| 			if test.expectError && err == nil {
 | |
| 				t.Errorf("in %s, expected execute error", test.name)
 | |
| 			}
 | |
| 			continue
 | |
| 		} else if err != nil {
 | |
| 			t.Errorf("in %s, execute error %v", test.name, err)
 | |
| 		}
 | |
| 		out := buf.String()
 | |
| 		if out != test.expect {
 | |
| 			t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // testJSONPathSortOutput test cases related to map, the results may print in random order
 | |
| func testJSONPathSortOutput(tests []jsonpathTest, t *testing.T) {
 | |
| 	for _, test := range tests {
 | |
| 		j := New(test.name)
 | |
| 		err := j.Parse(test.template)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
 | |
| 		}
 | |
| 		buf := new(bytes.Buffer)
 | |
| 		err = j.Execute(buf, test.input)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("in %s, execute error %v", test.name, err)
 | |
| 		}
 | |
| 		out := buf.String()
 | |
| 		//since map is visited in random order, we need to sort the results.
 | |
| 		sortedOut := strings.Fields(out)
 | |
| 		sort.Strings(sortedOut)
 | |
| 		sortedExpect := strings.Fields(test.expect)
 | |
| 		sort.Strings(sortedExpect)
 | |
| 		if !reflect.DeepEqual(sortedOut, sortedExpect) {
 | |
| 			t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func testFailJSONPath(tests []jsonpathTest, t *testing.T) {
 | |
| 	for _, test := range tests {
 | |
| 		j := New(test.name)
 | |
| 		err := j.Parse(test.template)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
 | |
| 		}
 | |
| 		buf := new(bytes.Buffer)
 | |
| 		err = j.Execute(buf, test.input)
 | |
| 		var out string
 | |
| 		if err == nil {
 | |
| 			out = "nil"
 | |
| 		} else {
 | |
| 			out = err.Error()
 | |
| 		}
 | |
| 		if out != test.expect {
 | |
| 			t.Errorf("in %s, expect to get error %q, got %q", test.name, test.expect, out)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type book struct {
 | |
| 	Category string
 | |
| 	Author   string
 | |
| 	Title    string
 | |
| 	Price    float32
 | |
| }
 | |
| 
 | |
| func (b book) String() string {
 | |
| 	return fmt.Sprintf("{Category: %s, Author: %s, Title: %s, Price: %v}", b.Category, b.Author, b.Title, b.Price)
 | |
| }
 | |
| 
 | |
| type bicycle struct {
 | |
| 	Color string
 | |
| 	Price float32
 | |
| 	IsNew bool
 | |
| }
 | |
| 
 | |
| type empName string
 | |
| type job string
 | |
| type store struct {
 | |
| 	Book      []book
 | |
| 	Bicycle   []bicycle
 | |
| 	Name      string
 | |
| 	Labels    map[string]int
 | |
| 	Employees map[empName]job
 | |
| }
 | |
| 
 | |
| func TestStructInput(t *testing.T) {
 | |
| 
 | |
| 	storeData := store{
 | |
| 		Name: "jsonpath",
 | |
| 		Book: []book{
 | |
| 			{"reference", "Nigel Rees", "Sayings of the Centurey", 8.95},
 | |
| 			{"fiction", "Evelyn Waugh", "Sword of Honour", 12.99},
 | |
| 			{"fiction", "Herman Melville", "Moby Dick", 8.99},
 | |
| 		},
 | |
| 		Bicycle: []bicycle{
 | |
| 			{"red", 19.95, true},
 | |
| 			{"green", 20.01, false},
 | |
| 		},
 | |
| 		Labels: map[string]int{
 | |
| 			"engieer":  10,
 | |
| 			"web/html": 15,
 | |
| 			"k8s-app":  20,
 | |
| 		},
 | |
| 		Employees: map[empName]job{
 | |
| 			"jason": "manager",
 | |
| 			"dan":   "clerk",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	storeTests := []jsonpathTest{
 | |
| 		{"plain", "hello jsonpath", nil, "hello jsonpath", false},
 | |
| 		{"recursive", "{..}", []int{1, 2, 3}, "[1 2 3]", false},
 | |
| 		{"filter", "{[?(@<5)]}", []int{2, 6, 3, 7}, "2 3", false},
 | |
| 		{"quote", `{"{"}`, nil, "{", false},
 | |
| 		{"union", "{[1,3,4]}", []int{0, 1, 2, 3, 4}, "1 3 4", false},
 | |
| 		{"array", "{[0:2]}", []string{"Monday", "Tudesday"}, "Monday Tudesday", false},
 | |
| 		{"variable", "hello {.Name}", storeData, "hello jsonpath", false},
 | |
| 		{"dict/", "{$.Labels.web/html}", storeData, "15", false},
 | |
| 		{"dict/", "{$.Employees.jason}", storeData, "manager", false},
 | |
| 		{"dict/", "{$.Employees.dan}", storeData, "clerk", false},
 | |
| 		{"dict-", "{.Labels.k8s-app}", storeData, "20", false},
 | |
| 		{"nest", "{.Bicycle[*].Color}", storeData, "red green", false},
 | |
| 		{"allarray", "{.Book[*].Author}", storeData, "Nigel Rees Evelyn Waugh Herman Melville", false},
 | |
| 		{"allfileds", "{.Bicycle.*}", storeData, "{red 19.95 true} {green 20.01 false}", false},
 | |
| 		{"recurfileds", "{..Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false},
 | |
| 		{"lastarray", "{.Book[-1:]}", storeData,
 | |
| 			"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false},
 | |
| 		{"recurarray", "{..Book[2]}", storeData,
 | |
| 			"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false},
 | |
| 		{"bool", "{.Bicycle[?(@.IsNew==true)]}", storeData, "{red 19.95 true}", false},
 | |
| 	}
 | |
| 	testJSONPath(storeTests, false, t)
 | |
| 
 | |
| 	missingKeyTests := []jsonpathTest{
 | |
| 		{"nonexistent field", "{.hello}", storeData, "", false},
 | |
| 	}
 | |
| 	testJSONPath(missingKeyTests, true, t)
 | |
| 
 | |
| 	failStoreTests := []jsonpathTest{
 | |
| 		{"invalid identifier", "{hello}", storeData, "unrecognized identifier hello", false},
 | |
| 		{"nonexistent field", "{.hello}", storeData, "hello is not found", false},
 | |
| 		{"invalid array", "{.Labels[0]}", storeData, "map[string]int is not array or slice", false},
 | |
| 		{"invalid filter operator", "{.Book[?(@.Price<>10)]}", storeData, "unrecognized filter operator <>", false},
 | |
| 		{"redundant end", "{range .Labels.*}{@}{end}{end}", storeData, "not in range, nothing to end", false},
 | |
| 	}
 | |
| 	testFailJSONPath(failStoreTests, t)
 | |
| }
 | |
| 
 | |
| func TestJSONInput(t *testing.T) {
 | |
| 	var pointsJSON = []byte(`[
 | |
| 		{"id": "i1", "x":4, "y":-5},
 | |
| 		{"id": "i2", "x":-2, "y":-5, "z":1},
 | |
| 		{"id": "i3", "x":  8, "y":  3 },
 | |
| 		{"id": "i4", "x": -6, "y": -1 },
 | |
| 		{"id": "i5", "x":  0, "y":  2, "z": 1 },
 | |
| 		{"id": "i6", "x":  1, "y":  4 }
 | |
| 	]`)
 | |
| 	var pointsData interface{}
 | |
| 	err := json.Unmarshal(pointsJSON, &pointsData)
 | |
| 	if err != nil {
 | |
| 		t.Error(err)
 | |
| 	}
 | |
| 	pointsTests := []jsonpathTest{
 | |
| 		{"exists filter", "{[?(@.z)].id}", pointsData, "i2 i5", false},
 | |
| 		{"bracket key", "{[0]['id']}", pointsData, "i1", false},
 | |
| 	}
 | |
| 	testJSONPath(pointsTests, false, t)
 | |
| }
 | |
| 
 | |
| // TestKubernetes tests some use cases from kubernetes
 | |
| func TestKubernetes(t *testing.T) {
 | |
| 	var input = []byte(`{
 | |
| 	  "kind": "List",
 | |
| 	  "items":[
 | |
| 		{
 | |
| 		  "kind":"None",
 | |
| 		  "metadata":{
 | |
| 		    "name":"127.0.0.1",
 | |
| 			"labels":{
 | |
| 			  "kubernetes.io/hostname":"127.0.0.1"
 | |
| 			}
 | |
| 		  },
 | |
| 		  "status":{
 | |
| 			"capacity":{"cpu":"4"},
 | |
| 			"ready": true,
 | |
| 			"addresses":[{"type": "LegacyHostIP", "address":"127.0.0.1"}]
 | |
| 		  }
 | |
| 		},
 | |
| 		{
 | |
| 		  "kind":"None",
 | |
| 		  "metadata":{
 | |
| 			"name":"127.0.0.2",
 | |
| 			"labels":{
 | |
| 			  "kubernetes.io/hostname":"127.0.0.2"
 | |
| 			}
 | |
| 		  },
 | |
| 		  "status":{
 | |
| 			"capacity":{"cpu":"8"},
 | |
| 			"ready": false,
 | |
| 			"addresses":[
 | |
| 			  {"type": "LegacyHostIP", "address":"127.0.0.2"},
 | |
| 			  {"type": "another", "address":"127.0.0.3"}
 | |
| 			]
 | |
| 		  }
 | |
| 		}
 | |
| 	  ],
 | |
| 	  "users":[
 | |
| 	    {
 | |
| 	      "name": "myself",
 | |
| 	      "user": {}
 | |
| 	    },
 | |
| 	    {
 | |
| 	      "name": "e2e",
 | |
| 	      "user": {"username": "admin", "password": "secret"}
 | |
| 	  	}
 | |
| 	  ]
 | |
| 	}`)
 | |
| 	var nodesData interface{}
 | |
| 	err := json.Unmarshal(input, &nodesData)
 | |
| 	if err != nil {
 | |
| 		t.Error(err)
 | |
| 	}
 | |
| 
 | |
| 	nodesTests := []jsonpathTest{
 | |
| 		{"range item", `{range .items[*]}{.metadata.name}, {end}{.kind}`, nodesData, "127.0.0.1, 127.0.0.2, List", false},
 | |
| 		{"range item with quote", `{range .items[*]}{.metadata.name}{"\t"}{end}`, nodesData, "127.0.0.1\t127.0.0.2\t", false},
 | |
| 		{"range addresss", `{.items[*].status.addresses[*].address}`, nodesData,
 | |
| 			"127.0.0.1 127.0.0.2 127.0.0.3", false},
 | |
| 		{"double range", `{range .items[*]}{range .status.addresses[*]}{.address}, {end}{end}`, nodesData,
 | |
| 			"127.0.0.1, 127.0.0.2, 127.0.0.3, ", false},
 | |
| 		{"item name", `{.items[*].metadata.name}`, nodesData, "127.0.0.1 127.0.0.2", false},
 | |
| 		{"union nodes capacity", `{.items[*]['metadata.name', 'status.capacity']}`, nodesData,
 | |
| 			"127.0.0.1 127.0.0.2 map[cpu:4] map[cpu:8]", false},
 | |
| 		{"range nodes capacity", `{range .items[*]}[{.metadata.name}, {.status.capacity}] {end}`, nodesData,
 | |
| 			"[127.0.0.1, map[cpu:4]] [127.0.0.2, map[cpu:8]] ", false},
 | |
| 		{"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret", false},
 | |
| 		{"hostname", `{.items[0].metadata.labels.kubernetes\.io/hostname}`, &nodesData, "127.0.0.1", false},
 | |
| 		{"hostname filter", `{.items[?(@.metadata.labels.kubernetes\.io/hostname=="127.0.0.1")].kind}`, &nodesData, "None", false},
 | |
| 		{"bool item", `{.items[?(@..ready==true)].metadata.name}`, &nodesData, "127.0.0.1", false},
 | |
| 	}
 | |
| 	testJSONPath(nodesTests, false, t)
 | |
| 
 | |
| 	randomPrintOrderTests := []jsonpathTest{
 | |
| 		{"recursive name", "{..name}", nodesData, `127.0.0.1 127.0.0.2 myself e2e`, false},
 | |
| 	}
 | |
| 	testJSONPathSortOutput(randomPrintOrderTests, t)
 | |
| }
 | |
| 
 | |
| func TestFilterPartialMatchesSometimesMissingAnnotations(t *testing.T) {
 | |
| 	// for https://issues.k8s.io/45546
 | |
| 	var input = []byte(`{
 | |
| 		"kind": "List",
 | |
| 		"items": [
 | |
| 			{
 | |
| 				"kind": "Pod",
 | |
| 				"metadata": {
 | |
| 					"name": "pod1",
 | |
| 					"annotations": {
 | |
| 						"color": "blue"
 | |
| 					}
 | |
| 				}
 | |
| 			},
 | |
| 			{
 | |
| 				"kind": "Pod",
 | |
| 				"metadata": {
 | |
| 					"name": "pod2"
 | |
| 				}
 | |
| 			},
 | |
| 			{
 | |
| 				"kind": "Pod",
 | |
| 				"metadata": {
 | |
| 					"name": "pod3",
 | |
| 					"annotations": {
 | |
| 						"color": "green"
 | |
| 					}
 | |
| 				}
 | |
| 			},
 | |
| 			{
 | |
| 				"kind": "Pod",
 | |
| 				"metadata": {
 | |
| 					"name": "pod4",
 | |
| 					"annotations": {
 | |
| 						"color": "blue"
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		]
 | |
| 	}`)
 | |
| 	var data interface{}
 | |
| 	err := json.Unmarshal(input, &data)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	testJSONPath(
 | |
| 		[]jsonpathTest{
 | |
| 			{
 | |
| 				"filter, should only match a subset, some items don't have annotations, tolerate missing items",
 | |
| 				`{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`,
 | |
| 				data,
 | |
| 				"pod1 pod4",
 | |
| 				false, // expect no error
 | |
| 			},
 | |
| 		},
 | |
| 		true, // allow missing keys
 | |
| 		t,
 | |
| 	)
 | |
| 
 | |
| 	testJSONPath(
 | |
| 		[]jsonpathTest{
 | |
| 			{
 | |
| 				"filter, should only match a subset, some items don't have annotations, error on missing items",
 | |
| 				`{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`,
 | |
| 				data,
 | |
| 				"",
 | |
| 				true, // expect an error
 | |
| 			},
 | |
| 		},
 | |
| 		false, // don't allow missing keys
 | |
| 		t,
 | |
| 	)
 | |
| }
 |