operator/cmd/mesh/test-util.go (415 lines of code) (raw):

// Copyright Istio 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 mesh import ( "fmt" "os" "reflect" "regexp" "strings" "testing" ) import ( "github.com/onsi/gomega" "github.com/onsi/gomega/types" "istio.io/pkg/log" labels2 "k8s.io/apimachinery/pkg/labels" ) import ( name2 "github.com/apache/dubbo-go-pixiu/operator/pkg/name" "github.com/apache/dubbo-go-pixiu/operator/pkg/object" "github.com/apache/dubbo-go-pixiu/operator/pkg/tpath" "github.com/apache/dubbo-go-pixiu/operator/pkg/util" "github.com/apache/dubbo-go-pixiu/pkg/test" ) // PathValue is a path/value type. type PathValue struct { path string value interface{} } // String implements the Stringer interface. func (pv *PathValue) String() string { return fmt.Sprintf("%s:%v", pv.path, pv.value) } // ObjectSet is a set of objects maintained both as a slice (for ordering) and map (for speed). type ObjectSet struct { objSlice object.K8sObjects objMap map[string]*object.K8sObject keySlice []string } // NewObjectSet creates a new ObjectSet from objs and returns a pointer to it. func NewObjectSet(objs object.K8sObjects) *ObjectSet { ret := &ObjectSet{} for _, o := range objs { ret.append(o) } return ret } // parseObjectSetFromManifest parses an ObjectSet from the given manifest. func parseObjectSetFromManifest(manifest string) (*ObjectSet, error) { objSlice, err := object.ParseK8sObjectsFromYAMLManifest(manifest) return NewObjectSet(objSlice), err } // append appends an object to o. func (o *ObjectSet) append(obj *object.K8sObject) { h := obj.Hash() o.objSlice = append(o.objSlice, obj) if o.objMap == nil { o.objMap = make(map[string]*object.K8sObject) } o.objMap[h] = obj o.keySlice = append(o.keySlice, h) } // size reports the length of o. func (o *ObjectSet) size() int { return len(o.keySlice) } // nameMatches returns a subset of o where objects names match the given regex. func (o *ObjectSet) nameMatches(nameRegex string) *ObjectSet { ret := &ObjectSet{} for k, v := range o.objMap { _, _, objName := object.FromHash(k) m, err := regexp.MatchString(nameRegex, objName) if err != nil { log.Error(err.Error()) continue } if m { ret.append(v) } } return ret } // nameEquals returns the object in o whose name matches "name", or nil if no object name matches. func (o *ObjectSet) nameEquals(name string) *object.K8sObject { for k, v := range o.objMap { _, _, objName := object.FromHash(k) if objName == name { return v } } return nil } // kind returns a subset of o where kind matches the given value. func (o *ObjectSet) kind(kind string) *ObjectSet { ret := &ObjectSet{} for k, v := range o.objMap { objKind, _, _ := object.FromHash(k) if objKind == kind { ret.append(v) } } return ret } // namespace returns a subset of o where namespace matches the given value or fails if it's not found in objs. func (o *ObjectSet) namespace(namespace string) *ObjectSet { ret := &ObjectSet{} for k, v := range o.objMap { _, objNamespace, _ := object.FromHash(k) if objNamespace == namespace { ret.append(v) } } return ret } // labels returns a subset of o where the object's labels match all the given labels. func (o *ObjectSet) labels(labels ...string) *ObjectSet { ret := &ObjectSet{} for _, obj := range o.objMap { hasAll := true for _, l := range labels { lkv := strings.Split(l, "=") if len(lkv) != 2 { panic("label must have format key=value") } if !hasLabel(obj, lkv[0], lkv[1]) { hasAll = false break } } if hasAll { ret.append(obj) } } return ret } // HasLabel reports whether 0 has the given label. func hasLabel(o *object.K8sObject, label, value string) bool { got, found, err := tpath.Find(o.UnstructuredObject().UnstructuredContent(), util.PathFromString("metadata.labels")) if err != nil { log.Errorf("bad path: %s", err) return false } if !found { return false } return got.(map[string]interface{})[label] == value } // mustGetService returns the service with the given name or fails if it's not found in objs. func mustGetService(g *gomega.WithT, objs *ObjectSet, name string) *object.K8sObject { obj := objs.kind(name2.ServiceStr).nameEquals(name) g.Expect(obj).Should(gomega.Not(gomega.BeNil())) return obj } // mustGetDeployment returns the deployment with the given name or fails if it's not found in objs. func mustGetDeployment(g *gomega.WithT, objs *ObjectSet, deploymentName string) *object.K8sObject { obj := objs.kind(name2.DeploymentStr).nameEquals(deploymentName) g.Expect(obj).Should(gomega.Not(gomega.BeNil())) return obj } // mustGetClusterRole returns the clusterRole with the given name or fails if it's not found in objs. func mustGetClusterRole(g *gomega.WithT, objs *ObjectSet, name string) *object.K8sObject { obj := objs.kind(name2.ClusterRoleStr).nameEquals(name) g.Expect(obj).Should(gomega.Not(gomega.BeNil())) return obj } // mustGetRole returns the role with the given name or fails if it's not found in objs. func mustGetRole(g *gomega.WithT, objs *ObjectSet, name string) *object.K8sObject { obj := objs.kind(name2.RoleStr).nameEquals(name) g.Expect(obj).Should(gomega.Not(gomega.BeNil())) return obj } // mustGetContainer returns the container tree with the given name in the deployment with the given name. func mustGetContainer(g *gomega.WithT, objs *ObjectSet, deploymentName, containerName string) map[string]interface{} { obj := mustGetDeployment(g, objs, deploymentName) container := obj.Container(containerName) g.Expect(container).Should(gomega.Not(gomega.BeNil()), fmt.Sprintf("Expected to get container %s in deployment %s", containerName, deploymentName)) return container } // mustGetEndpoint returns the endpoint tree with the given name in the deployment with the given name. func mustGetEndpoint(g *gomega.WithT, objs *ObjectSet, endpointName string) *object.K8sObject { obj := objs.kind(name2.EndpointStr).nameEquals(endpointName) if obj == nil { return nil } g.Expect(obj).Should(gomega.Not(gomega.BeNil())) return obj } // mustGetMutatingWebhookConfiguration returns the mutatingWebhookConfiguration with the given name or fails if it's not found in objs. func mustGetMutatingWebhookConfiguration(g *gomega.WithT, objs *ObjectSet, mutatingWebhookConfigurationName string) *object.K8sObject { obj := objs.kind(name2.MutatingWebhookConfigurationStr).nameEquals(mutatingWebhookConfigurationName) g.Expect(obj).Should(gomega.Not(gomega.BeNil())) return obj } // HavePathValueEqual matches map[string]interface{} tree against a PathValue. func HavePathValueEqual(expected interface{}) types.GomegaMatcher { return &HavePathValueEqualMatcher{ expected: expected, } } // HavePathValueEqualMatcher is a matcher type for HavePathValueEqual. type HavePathValueEqualMatcher struct { expected interface{} } // Match implements the Matcher interface. func (m *HavePathValueEqualMatcher) Match(actual interface{}) (bool, error) { pv := m.expected.(PathValue) node := actual.(map[string]interface{}) got, f, err := tpath.GetPathContext(node, util.PathFromString(pv.path), false) if err != nil || !f { return false, err } if reflect.TypeOf(got.Node) != reflect.TypeOf(pv.value) { return false, fmt.Errorf("comparison types don't match: got %v(%T), want %v(%T)", got.Node, got.Node, pv.value, pv.value) } if !reflect.DeepEqual(got.Node, pv.value) { return false, fmt.Errorf("values don't match: got %v, want %v", got.Node, pv.value) } return true, nil } // FailureMessage implements the Matcher interface. func (m *HavePathValueEqualMatcher) FailureMessage(actual interface{}) string { pv := m.expected.(PathValue) node := actual.(map[string]interface{}) return fmt.Sprintf("Expected the following parseObjectSetFromManifest to have path=value %s=%v\n\n%v", pv.path, pv.value, util.ToYAML(node)) } // NegatedFailureMessage implements the Matcher interface. func (m *HavePathValueEqualMatcher) NegatedFailureMessage(actual interface{}) string { pv := m.expected.(PathValue) node := actual.(map[string]interface{}) return fmt.Sprintf("Expected the following parseObjectSetFromManifest not to have path=value %s=%v\n\n%v", pv.path, pv.value, util.ToYAML(node)) } // HavePathValueMatchRegex matches map[string]interface{} tree against a PathValue. func HavePathValueMatchRegex(expected interface{}) types.GomegaMatcher { return &HavePathValueMatchRegexMatcher{ expected: expected, } } // HavePathValueMatchRegexMatcher is a matcher type for HavePathValueMatchRegex. type HavePathValueMatchRegexMatcher struct { expected interface{} } // Match implements the Matcher interface. func (m *HavePathValueMatchRegexMatcher) Match(actual interface{}) (bool, error) { pv := m.expected.(PathValue) node := actual.(map[string]interface{}) got, f, err := tpath.GetPathContext(node, util.PathFromString(pv.path), false) if err != nil || !f { return false, err } if reflect.TypeOf(got.Node).Kind() != reflect.String || reflect.TypeOf(pv.value).Kind() != reflect.String { return false, fmt.Errorf("comparison types must both be string: got %v(%T), want %v(%T)", got.Node, got.Node, pv.value, pv.value) } gotS := got.Node.(string) wantS := pv.value.(string) ok, err := regexp.MatchString(wantS, gotS) if err != nil { return false, err } if !ok { return false, fmt.Errorf("values don't match: got %v, want %v", got.Node, pv.value) } return true, nil } // FailureMessage implements the Matcher interface. func (m *HavePathValueMatchRegexMatcher) FailureMessage(actual interface{}) string { pv := m.expected.(PathValue) node := actual.(map[string]interface{}) return fmt.Sprintf("Expected the following parseObjectSetFromManifest to regex match path=value %s=%v\n\n%v", pv.path, pv.value, util.ToYAML(node)) } // NegatedFailureMessage implements the Matcher interface. func (m *HavePathValueMatchRegexMatcher) NegatedFailureMessage(actual interface{}) string { pv := m.expected.(PathValue) node := actual.(map[string]interface{}) 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)) } // HavePathValueContain matches map[string]interface{} tree against a PathValue. func HavePathValueContain(expected interface{}) types.GomegaMatcher { return &HavePathValueContainMatcher{ expected: expected, } } // HavePathValueContainMatcher is a matcher type for HavePathValueContain. type HavePathValueContainMatcher struct { expected interface{} } // Match implements the Matcher interface. func (m *HavePathValueContainMatcher) Match(actual interface{}) (bool, error) { pv := m.expected.(PathValue) node := actual.(map[string]interface{}) got, f, err := tpath.GetPathContext(node, util.PathFromString(pv.path), false) if err != nil || !f { return false, err } if reflect.TypeOf(got.Node) != reflect.TypeOf(pv.value) { return false, fmt.Errorf("comparison types don't match: got %T, want %T", got.Node, pv.value) } gotValStr := util.ToYAML(got.Node) subsetValStr := util.ToYAML(pv.value) overlay, err := util.OverlayYAML(gotValStr, subsetValStr) if err != nil { return false, err } if overlay != gotValStr { return false, fmt.Errorf("actual value:\n\n%s\ndoesn't contain expected subset:\n\n%s", gotValStr, subsetValStr) } return true, nil } // FailureMessage implements the Matcher interface. func (m *HavePathValueContainMatcher) FailureMessage(actual interface{}) string { pv := m.expected.(PathValue) node := actual.(map[string]interface{}) 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)) } // NegatedFailureMessage implements the Matcher interface. func (m *HavePathValueContainMatcher) NegatedFailureMessage(actual interface{}) string { pv := m.expected.(PathValue) node := actual.(map[string]interface{}) 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)) } func mustSelect(t test.Failer, selector map[string]string, labels map[string]string) { t.Helper() kselector := labels2.Set(selector).AsSelectorPreValidated() if !kselector.Matches(labels2.Set(labels)) { t.Fatalf("%v does not select %v", selector, labels) } } func mustNotSelect(t test.Failer, selector map[string]string, labels map[string]string) { t.Helper() kselector := labels2.Set(selector).AsSelectorPreValidated() if kselector.Matches(labels2.Set(labels)) { t.Fatalf("%v selects %v when it should not", selector, labels) } } func mustGetLabels(t test.Failer, obj object.K8sObject, path string) map[string]string { t.Helper() got := mustGetPath(t, obj, path) conv, ok := got.(map[string]interface{}) if !ok { t.Fatalf("could not convert %v", got) } ret := map[string]string{} for k, v := range conv { sv, ok := v.(string) if !ok { t.Fatalf("could not convert to string %v", v) } ret[k] = sv } return ret } func mustGetPath(t test.Failer, obj object.K8sObject, path string) interface{} { t.Helper() got, f, err := tpath.Find(obj.UnstructuredObject().UnstructuredContent(), util.PathFromString(path)) if err != nil { t.Fatal(err) } if !f { t.Fatalf("couldn't find path %v", path) } return got } func mustFindObject(t test.Failer, objs object.K8sObjects, name, kind string) object.K8sObject { t.Helper() o := findObject(objs, name, kind) if o == nil { t.Fatalf("expected %v/%v", name, kind) return object.K8sObject{} } return *o } func findObject(objs object.K8sObjects, name, kind string) *object.K8sObject { for _, o := range objs { if o.Kind == kind && o.Name == name { return o } } return nil } // mustGetValueAtPath returns the value at the given path in the unstructured tree t. Fails if the path is not found // in the tree. func mustGetValueAtPath(g *gomega.WithT, t map[string]interface{}, path string) interface{} { got, f, err := tpath.GetPathContext(t, util.PathFromString(path), false) g.Expect(err).Should(gomega.BeNil(), "path %s should exist (%s)", path, err) g.Expect(f).Should(gomega.BeTrue(), "path %s should exist", path) return got.Node } func createTempDirOrFail(t *testing.T, prefix string) string { dir, err := os.MkdirTemp("", prefix) if err != nil { t.Fatal(err) } return dir } func removeDirOrFail(t *testing.T, path string) { err := os.RemoveAll(path) if err != nil { t.Fatal(err) } } // toMap transforms a comma separated key:value list (e.g. "a:aval, b:bval") to a map. func toMap(s string) map[string]interface{} { out := make(map[string]interface{}) for _, l := range strings.Split(s, ",") { l = strings.TrimSpace(l) kv := strings.Split(l, ":") if len(kv) != 2 { panic("bad key:value in " + s) } out[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) } if len(out) == 0 { return nil } return out } // endpointSubsetAddressVal returns a map having subset address type for an endpint. func endpointSubsetAddressVal(hostname, ip, nodeName string) map[string]interface{} { out := make(map[string]interface{}) if hostname != "" { out["hostname"] = hostname } if ip != "" { out["ip"] = ip } if nodeName != "" { out["nodeName"] = nodeName } return out } // portVal returns a map having service port type. A value of -1 for port or targetPort leaves those keys unset. func portVal(name string, port, targetPort int64) map[string]interface{} { out := make(map[string]interface{}) if name != "" { out["name"] = name } if port != -1 { out["port"] = port } if targetPort != -1 { out["targetPort"] = targetPort } return out } // checkRoleBindingsReferenceRoles fails if any RoleBinding in objs references a Role that isn't found in objs. func checkRoleBindingsReferenceRoles(g *gomega.WithT, objs *ObjectSet) { for _, o := range objs.kind(name2.RoleBindingStr).objSlice { ou := o.Unstructured() rrname := mustGetValueAtPath(g, ou, "roleRef.name") mustGetRole(g, objs, rrname.(string)) } } // checkClusterRoleBindingsReferenceRoles fails if any RoleBinding in objs references a Role that isn't found in objs. func checkClusterRoleBindingsReferenceRoles(g *gomega.WithT, objs *ObjectSet) { for _, o := range objs.kind(name2.ClusterRoleBindingStr).objSlice { ou := o.Unstructured() rrname := mustGetValueAtPath(g, ou, "roleRef.name") mustGetClusterRole(g, objs, rrname.(string)) } }