istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/cmd/mesh/test-util_test.go (about)

     1  // Copyright 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  
    15  package mesh
    16  
    17  import (
    18  	"fmt"
    19  	"reflect"
    20  	"regexp"
    21  	"strings"
    22  
    23  	. "github.com/onsi/gomega"
    24  	"github.com/onsi/gomega/types"
    25  	labels2 "k8s.io/apimachinery/pkg/labels"
    26  
    27  	name2 "istio.io/istio/operator/pkg/name"
    28  	"istio.io/istio/operator/pkg/object"
    29  	"istio.io/istio/operator/pkg/tpath"
    30  	"istio.io/istio/operator/pkg/util"
    31  	"istio.io/istio/pkg/log"
    32  	"istio.io/istio/pkg/test"
    33  )
    34  
    35  // PathValue is a path/value type.
    36  type PathValue struct {
    37  	path  string
    38  	value any
    39  }
    40  
    41  // String implements the Stringer interface.
    42  func (pv *PathValue) String() string {
    43  	return fmt.Sprintf("%s:%v", pv.path, pv.value)
    44  }
    45  
    46  // ObjectSet is a set of objects maintained both as a slice (for ordering) and map (for speed).
    47  type ObjectSet struct {
    48  	objSlice object.K8sObjects
    49  	objMap   map[string]*object.K8sObject
    50  	keySlice []string
    51  }
    52  
    53  // NewObjectSet creates a new ObjectSet from objs and returns a pointer to it.
    54  func NewObjectSet(objs object.K8sObjects) *ObjectSet {
    55  	ret := &ObjectSet{}
    56  	for _, o := range objs {
    57  		ret.append(o)
    58  	}
    59  	return ret
    60  }
    61  
    62  // parseObjectSetFromManifest parses an ObjectSet from the given manifest.
    63  func parseObjectSetFromManifest(manifest string) (*ObjectSet, error) {
    64  	objSlice, err := object.ParseK8sObjectsFromYAMLManifest(manifest)
    65  	return NewObjectSet(objSlice), err
    66  }
    67  
    68  // append appends an object to o.
    69  func (o *ObjectSet) append(obj *object.K8sObject) {
    70  	h := obj.Hash()
    71  	o.objSlice = append(o.objSlice, obj)
    72  	if o.objMap == nil {
    73  		o.objMap = make(map[string]*object.K8sObject)
    74  	}
    75  	o.objMap[h] = obj
    76  	o.keySlice = append(o.keySlice, h)
    77  }
    78  
    79  // size reports the length of o.
    80  func (o *ObjectSet) size() int {
    81  	return len(o.keySlice)
    82  }
    83  
    84  // nameMatches returns a subset of o where objects names match the given regex.
    85  func (o *ObjectSet) nameMatches(nameRegex string) *ObjectSet {
    86  	ret := &ObjectSet{}
    87  	for k, v := range o.objMap {
    88  		_, _, objName := object.FromHash(k)
    89  		m, err := regexp.MatchString(nameRegex, objName)
    90  		if err != nil {
    91  			log.Error(err.Error())
    92  			continue
    93  		}
    94  		if m {
    95  			ret.append(v)
    96  		}
    97  	}
    98  	return ret
    99  }
   100  
   101  // nameEquals returns the object in o whose name matches "name", or nil if no object name matches.
   102  func (o *ObjectSet) nameEquals(name string) *object.K8sObject {
   103  	for k, v := range o.objMap {
   104  		_, _, objName := object.FromHash(k)
   105  		if objName == name {
   106  			return v
   107  		}
   108  	}
   109  	return nil
   110  }
   111  
   112  // kind returns a subset of o where kind matches the given value.
   113  func (o *ObjectSet) kind(kind string) *ObjectSet {
   114  	ret := &ObjectSet{}
   115  	for k, v := range o.objMap {
   116  		objKind, _, _ := object.FromHash(k)
   117  		if objKind == kind {
   118  			ret.append(v)
   119  		}
   120  	}
   121  	return ret
   122  }
   123  
   124  // labels returns a subset of o where the object's labels match all the given labels.
   125  func (o *ObjectSet) labels(labels ...string) *ObjectSet {
   126  	ret := &ObjectSet{}
   127  	for _, obj := range o.objMap {
   128  		hasAll := true
   129  		for _, l := range labels {
   130  			lkv := strings.Split(l, "=")
   131  			if len(lkv) != 2 {
   132  				panic("label must have format key=value")
   133  			}
   134  			if !hasLabel(obj, lkv[0], lkv[1]) {
   135  				hasAll = false
   136  				break
   137  			}
   138  		}
   139  		if hasAll {
   140  			ret.append(obj)
   141  		}
   142  	}
   143  	return ret
   144  }
   145  
   146  // HasLabel reports whether 0 has the given label.
   147  func hasLabel(o *object.K8sObject, label, value string) bool {
   148  	got, found, err := tpath.Find(o.UnstructuredObject().UnstructuredContent(), util.PathFromString("metadata.labels"))
   149  	if err != nil {
   150  		log.Errorf("bad path: %s", err)
   151  		return false
   152  	}
   153  	if !found {
   154  		return false
   155  	}
   156  	return got.(map[string]any)[label] == value
   157  }
   158  
   159  // mustGetService returns the service with the given name or fails if it's not found in objs.
   160  func mustGetService(g *WithT, objs *ObjectSet, name string) *object.K8sObject {
   161  	obj := objs.kind(name2.ServiceStr).nameEquals(name)
   162  	g.Expect(obj).Should(Not(BeNil()))
   163  	return obj
   164  }
   165  
   166  // mustGetDeployment returns the deployment with the given name or fails if it's not found in objs.
   167  func mustGetDeployment(g *WithT, objs *ObjectSet, deploymentName string) *object.K8sObject {
   168  	obj := objs.kind(name2.DeploymentStr).nameEquals(deploymentName)
   169  	g.Expect(obj).Should(Not(BeNil()))
   170  	return obj
   171  }
   172  
   173  // mustGetClusterRole returns the clusterRole with the given name or fails if it's not found in objs.
   174  func mustGetClusterRole(g *WithT, objs *ObjectSet, name string) *object.K8sObject {
   175  	obj := objs.kind(name2.ClusterRoleStr).nameEquals(name)
   176  	g.Expect(obj).Should(Not(BeNil()))
   177  	return obj
   178  }
   179  
   180  // mustGetRole returns the role with the given name or fails if it's not found in objs.
   181  func mustGetRole(g *WithT, objs *ObjectSet, name string) *object.K8sObject {
   182  	obj := objs.kind(name2.RoleStr).nameEquals(name)
   183  	g.Expect(obj).Should(Not(BeNil()))
   184  	return obj
   185  }
   186  
   187  // mustGetContainer returns the container tree with the given name in the deployment with the given name.
   188  func mustGetContainer(g *WithT, objs *ObjectSet, deploymentName, containerName string) map[string]any {
   189  	obj := mustGetDeployment(g, objs, deploymentName)
   190  	container := obj.Container(containerName)
   191  	g.Expect(container).Should(Not(BeNil()), fmt.Sprintf("Expected to get container %s in deployment %s", containerName, deploymentName))
   192  	return container
   193  }
   194  
   195  // mustGetEndpoint returns the endpoint tree with the given name in the deployment with the given name.
   196  func mustGetEndpoint(g *WithT, objs *ObjectSet, endpointName string) *object.K8sObject {
   197  	obj := objs.kind(name2.EndpointStr).nameEquals(endpointName)
   198  	if obj == nil {
   199  		return nil
   200  	}
   201  	g.Expect(obj).Should(Not(BeNil()))
   202  	return obj
   203  }
   204  
   205  // mustGetMutatingWebhookConfiguration returns the mutatingWebhookConfiguration with the given name or fails if it's not found in objs.
   206  func mustGetMutatingWebhookConfiguration(g *WithT, objs *ObjectSet, mutatingWebhookConfigurationName string) *object.K8sObject {
   207  	obj := objs.kind(name2.MutatingWebhookConfigurationStr).nameEquals(mutatingWebhookConfigurationName)
   208  	g.Expect(obj).Should(Not(BeNil()))
   209  	return obj
   210  }
   211  
   212  // HavePathValueEqual matches map[string]interface{} tree against a PathValue.
   213  func HavePathValueEqual(expected any) types.GomegaMatcher {
   214  	return &HavePathValueEqualMatcher{
   215  		expected: expected,
   216  	}
   217  }
   218  
   219  // HavePathValueEqualMatcher is a matcher type for HavePathValueEqual.
   220  type HavePathValueEqualMatcher struct {
   221  	expected any
   222  }
   223  
   224  // Match implements the Matcher interface.
   225  func (m *HavePathValueEqualMatcher) Match(actual any) (bool, error) {
   226  	pv := m.expected.(PathValue)
   227  	node := actual.(map[string]any)
   228  	got, f, err := tpath.GetPathContext(node, util.PathFromString(pv.path), false)
   229  	if err != nil || !f {
   230  		return false, err
   231  	}
   232  	if reflect.TypeOf(got.Node) != reflect.TypeOf(pv.value) {
   233  		return false, fmt.Errorf("comparison types don't match: got %v(%T), want %v(%T)", got.Node, got.Node, pv.value, pv.value)
   234  	}
   235  	if !reflect.DeepEqual(got.Node, pv.value) {
   236  		return false, fmt.Errorf("values don't match: got %v, want %v", got.Node, pv.value)
   237  	}
   238  	return true, nil
   239  }
   240  
   241  // FailureMessage implements the Matcher interface.
   242  func (m *HavePathValueEqualMatcher) FailureMessage(actual any) string {
   243  	pv := m.expected.(PathValue)
   244  	node := actual.(map[string]any)
   245  	return fmt.Sprintf("Expected the following parseObjectSetFromManifest to have path=value %s=%v\n\n%v", pv.path, pv.value, util.ToYAML(node))
   246  }
   247  
   248  // NegatedFailureMessage implements the Matcher interface.
   249  func (m *HavePathValueEqualMatcher) NegatedFailureMessage(actual any) string {
   250  	pv := m.expected.(PathValue)
   251  	node := actual.(map[string]any)
   252  	return fmt.Sprintf("Expected the following parseObjectSetFromManifest not to have path=value %s=%v\n\n%v", pv.path, pv.value, util.ToYAML(node))
   253  }
   254  
   255  // HavePathValueMatchRegex matches map[string]interface{} tree against a PathValue.
   256  func HavePathValueMatchRegex(expected any) types.GomegaMatcher {
   257  	return &HavePathValueMatchRegexMatcher{
   258  		expected: expected,
   259  	}
   260  }
   261  
   262  // HavePathValueMatchRegexMatcher is a matcher type for HavePathValueMatchRegex.
   263  type HavePathValueMatchRegexMatcher struct {
   264  	expected any
   265  }
   266  
   267  // Match implements the Matcher interface.
   268  func (m *HavePathValueMatchRegexMatcher) Match(actual any) (bool, error) {
   269  	pv := m.expected.(PathValue)
   270  	node := actual.(map[string]any)
   271  	got, f, err := tpath.GetPathContext(node, util.PathFromString(pv.path), false)
   272  	if err != nil || !f {
   273  		return false, err
   274  	}
   275  	if reflect.TypeOf(got.Node).Kind() != reflect.String || reflect.TypeOf(pv.value).Kind() != reflect.String {
   276  		return false, fmt.Errorf("comparison types must both be string: got %v(%T), want %v(%T)", got.Node, got.Node, pv.value, pv.value)
   277  	}
   278  	gotS := got.Node.(string)
   279  	wantS := pv.value.(string)
   280  	ok, err := regexp.MatchString(wantS, gotS)
   281  	if err != nil {
   282  		return false, err
   283  	}
   284  	if !ok {
   285  		return false, fmt.Errorf("values don't match: got %v, want %v", got.Node, pv.value)
   286  	}
   287  	return true, nil
   288  }
   289  
   290  // FailureMessage implements the Matcher interface.
   291  func (m *HavePathValueMatchRegexMatcher) FailureMessage(actual any) string {
   292  	pv := m.expected.(PathValue)
   293  	node := actual.(map[string]any)
   294  	return fmt.Sprintf("Expected the following parseObjectSetFromManifest to regex match path=value %s=%v\n\n%v", pv.path, pv.value, util.ToYAML(node))
   295  }
   296  
   297  // NegatedFailureMessage implements the Matcher interface.
   298  func (m *HavePathValueMatchRegexMatcher) NegatedFailureMessage(actual any) string {
   299  	pv := m.expected.(PathValue)
   300  	node := actual.(map[string]any)
   301  	return fmt.Sprintf("Expected the following parseObjectSetFromManifest not to regex match path=value %s=%v\n\n%v", pv.path, pv.value, util.ToYAML(node))
   302  }
   303  
   304  // HavePathValueContain matches map[string]interface{} tree against a PathValue.
   305  func HavePathValueContain(expected any) types.GomegaMatcher {
   306  	return &HavePathValueContainMatcher{
   307  		expected: expected,
   308  	}
   309  }
   310  
   311  // HavePathValueContainMatcher is a matcher type for HavePathValueContain.
   312  type HavePathValueContainMatcher struct {
   313  	expected any
   314  }
   315  
   316  // Match implements the Matcher interface.
   317  func (m *HavePathValueContainMatcher) Match(actual any) (bool, error) {
   318  	pv := m.expected.(PathValue)
   319  	node := actual.(map[string]any)
   320  	got, f, err := tpath.GetPathContext(node, util.PathFromString(pv.path), false)
   321  	if err != nil || !f {
   322  		return false, err
   323  	}
   324  	if reflect.TypeOf(got.Node) != reflect.TypeOf(pv.value) {
   325  		return false, fmt.Errorf("comparison types don't match: got %T, want %T", got.Node, pv.value)
   326  	}
   327  	gotValStr := util.ToYAML(got.Node)
   328  	subsetValStr := util.ToYAML(pv.value)
   329  	overlay, err := util.OverlayYAML(gotValStr, subsetValStr)
   330  	if err != nil {
   331  		return false, err
   332  	}
   333  	if overlay != gotValStr {
   334  		return false, fmt.Errorf("actual value:\n\n%s\ndoesn't contain expected subset:\n\n%s", gotValStr, subsetValStr)
   335  	}
   336  	return true, nil
   337  }
   338  
   339  // FailureMessage implements the Matcher interface.
   340  func (m *HavePathValueContainMatcher) FailureMessage(actual any) string {
   341  	pv := m.expected.(PathValue)
   342  	node := actual.(map[string]any)
   343  	return fmt.Sprintf("Expected path %s with value \n\n%v\nto be a subset of \n\n%v", pv.path, pv.value, util.ToYAML(node))
   344  }
   345  
   346  // NegatedFailureMessage implements the Matcher interface.
   347  func (m *HavePathValueContainMatcher) NegatedFailureMessage(actual any) string {
   348  	pv := m.expected.(PathValue)
   349  	node := actual.(map[string]any)
   350  	return fmt.Sprintf("Expected path %s with value \n\n%v\nto NOT be a subset of \n\n%v", pv.path, pv.value, util.ToYAML(node))
   351  }
   352  
   353  func mustSelect(t test.Failer, selector map[string]string, labels map[string]string) {
   354  	t.Helper()
   355  	kselector := labels2.Set(selector).AsSelectorPreValidated()
   356  	if !kselector.Matches(labels2.Set(labels)) {
   357  		t.Fatalf("%v does not select %v", selector, labels)
   358  	}
   359  }
   360  
   361  func mustNotSelect(t test.Failer, selector map[string]string, labels map[string]string) {
   362  	t.Helper()
   363  	kselector := labels2.Set(selector).AsSelectorPreValidated()
   364  	if kselector.Matches(labels2.Set(labels)) {
   365  		t.Fatalf("%v selects %v when it should not", selector, labels)
   366  	}
   367  }
   368  
   369  func mustGetLabels(t test.Failer, obj object.K8sObject, path string) map[string]string {
   370  	t.Helper()
   371  	got := mustGetPath(t, obj, path)
   372  	conv, ok := got.(map[string]any)
   373  	if !ok {
   374  		t.Fatalf("could not convert %v", got)
   375  	}
   376  	ret := map[string]string{}
   377  	for k, v := range conv {
   378  		sv, ok := v.(string)
   379  		if !ok {
   380  			t.Fatalf("could not convert to string %v", v)
   381  		}
   382  		ret[k] = sv
   383  	}
   384  	return ret
   385  }
   386  
   387  func mustGetPath(t test.Failer, obj object.K8sObject, path string) any {
   388  	t.Helper()
   389  	got, f, err := tpath.Find(obj.UnstructuredObject().UnstructuredContent(), util.PathFromString(path))
   390  	if err != nil {
   391  		t.Fatal(err)
   392  	}
   393  	if !f {
   394  		t.Fatalf("couldn't find path %v", path)
   395  	}
   396  	return got
   397  }
   398  
   399  func mustFindObject(t test.Failer, objs object.K8sObjects, name, kind string) object.K8sObject {
   400  	t.Helper()
   401  	o := findObject(objs, name, kind)
   402  	if o == nil {
   403  		t.Fatalf("expected %v/%v", name, kind)
   404  		return object.K8sObject{}
   405  	}
   406  	return *o
   407  }
   408  
   409  func findObject(objs object.K8sObjects, name, kind string) *object.K8sObject {
   410  	for _, o := range objs {
   411  		if o.Kind == kind && o.Name == name {
   412  			return o
   413  		}
   414  	}
   415  	return nil
   416  }
   417  
   418  // mustGetValueAtPath returns the value at the given path in the unstructured tree t. Fails if the path is not found
   419  // in the tree.
   420  func mustGetValueAtPath(g *WithT, t map[string]any, path string) any {
   421  	got, f, err := tpath.GetPathContext(t, util.PathFromString(path), false)
   422  	g.Expect(err).Should(BeNil(), "path %s should exist (%s)", path, err)
   423  	g.Expect(f).Should(BeTrue(), "path %s should exist", path)
   424  	return got.Node
   425  }
   426  
   427  // toMap transforms a comma separated key:value list (e.g. "a:aval, b:bval") to a map.
   428  func toMap(s string) map[string]any {
   429  	out := make(map[string]any)
   430  	for _, l := range strings.Split(s, ",") {
   431  		l = strings.TrimSpace(l)
   432  		kv := strings.Split(l, ":")
   433  		if len(kv) != 2 {
   434  			panic("bad key:value in " + s)
   435  		}
   436  		out[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
   437  	}
   438  	if len(out) == 0 {
   439  		return nil
   440  	}
   441  	return out
   442  }
   443  
   444  // endpointSubsetAddressVal returns a map having subset address type for an endpoint.
   445  func endpointSubsetAddressVal(hostname, ip, nodeName string) map[string]any {
   446  	out := make(map[string]any)
   447  	if hostname != "" {
   448  		out["hostname"] = hostname
   449  	}
   450  	if ip != "" {
   451  		out["ip"] = ip
   452  	}
   453  	if nodeName != "" {
   454  		out["nodeName"] = nodeName
   455  	}
   456  	return out
   457  }
   458  
   459  // portVal returns a map having service port type. A value of -1 for port or targetPort leaves those keys unset.
   460  func portVal(name string, port, targetPort int64) map[string]any {
   461  	out := make(map[string]any)
   462  	if name != "" {
   463  		out["name"] = name
   464  	}
   465  	if port != -1 {
   466  		out["port"] = port
   467  	}
   468  	if targetPort != -1 {
   469  		out["targetPort"] = targetPort
   470  	}
   471  	return out
   472  }
   473  
   474  // checkRoleBindingsReferenceRoles fails if any RoleBinding in objs references a Role that isn't found in objs.
   475  func checkRoleBindingsReferenceRoles(g *WithT, objs *ObjectSet) {
   476  	for _, o := range objs.kind(name2.RoleBindingStr).objSlice {
   477  		ou := o.Unstructured()
   478  		rrname := mustGetValueAtPath(g, ou, "roleRef.name")
   479  		mustGetRole(g, objs, rrname.(string))
   480  	}
   481  }
   482  
   483  // checkClusterRoleBindingsReferenceRoles fails if any RoleBinding in objs references a Role that isn't found in objs.
   484  func checkClusterRoleBindingsReferenceRoles(g *WithT, objs *ObjectSet) {
   485  	for _, o := range objs.kind(name2.ClusterRoleBindingStr).objSlice {
   486  		ou := o.Unstructured()
   487  		rrname := mustGetValueAtPath(g, ou, "roleRef.name")
   488  		mustGetClusterRole(g, objs, rrname.(string))
   489  	}
   490  }