blob: fcb95b42f39d2bcc84495fae5776e04909813e93 [file] [log] [blame]
// 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))
}
}