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 }