open-cluster-management.io/governance-policy-propagator@v0.13.0/test/utils/utils.go (about)

     1  // Copyright (c) 2021 Red Hat, Inc.
     2  // Copyright Contributors to the Open Cluster Management project
     3  
     4  package utils
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"os"
    10  	"os/exec"
    11  	"regexp"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/ghodss/yaml"
    16  	. "github.com/onsi/ginkgo/v2"
    17  	. "github.com/onsi/gomega"
    18  	corev1 "k8s.io/api/core/v1"
    19  	"k8s.io/apimachinery/pkg/api/errors"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    22  	"k8s.io/apimachinery/pkg/runtime/schema"
    23  	"k8s.io/client-go/dynamic"
    24  	"k8s.io/client-go/kubernetes"
    25  	clusterv1beta1 "open-cluster-management.io/api/cluster/v1beta1"
    26  	appsv1 "open-cluster-management.io/multicloud-operators-subscription/pkg/apis/apps/placementrule/v1"
    27  
    28  	"open-cluster-management.io/governance-policy-propagator/controllers/propagator"
    29  )
    30  
    31  // GeneratePlrStatus generate plr status with given clusters
    32  func GeneratePlrStatus(clusters ...string) *appsv1.PlacementRuleStatus {
    33  	plrDecision := []appsv1.PlacementDecision{}
    34  	for _, cluster := range clusters {
    35  		plrDecision = append(plrDecision, appsv1.PlacementDecision{
    36  			ClusterName:      cluster,
    37  			ClusterNamespace: cluster,
    38  		})
    39  	}
    40  
    41  	return &appsv1.PlacementRuleStatus{Decisions: plrDecision}
    42  }
    43  
    44  // GeneratePldStatus generate pld status with given clusters
    45  func GeneratePldStatus(
    46  	_ string, _ string, clusters ...string,
    47  ) *clusterv1beta1.PlacementDecisionStatus {
    48  	plrDecision := []clusterv1beta1.ClusterDecision{}
    49  	for _, cluster := range clusters {
    50  		plrDecision = append(plrDecision, clusterv1beta1.ClusterDecision{
    51  			ClusterName: cluster,
    52  			Reason:      "test",
    53  		})
    54  	}
    55  
    56  	return &clusterv1beta1.PlacementDecisionStatus{Decisions: plrDecision}
    57  }
    58  
    59  func RemovePolicyTemplateDBAnnotations(plc *unstructured.Unstructured) error {
    60  	// Remove the database annotation since this can be an inconsistent value
    61  	templates, _, _ := unstructured.NestedSlice(plc.Object, "spec", "policy-templates")
    62  
    63  	updated := false
    64  
    65  	for i, template := range templates {
    66  		template := template.(map[string]interface{})
    67  
    68  		annotations, ok, _ := unstructured.NestedMap(
    69  			template, "objectDefinition", "metadata", "annotations",
    70  		)
    71  		if !ok {
    72  			continue
    73  		}
    74  
    75  		annotationVal, ok := annotations[propagator.PolicyIDAnnotation].(string)
    76  		if !ok {
    77  			continue
    78  		}
    79  
    80  		if annotationVal != "" {
    81  			delete(annotations, propagator.PolicyIDAnnotation)
    82  
    83  			if len(annotations) == 0 {
    84  				unstructured.RemoveNestedField(template, "objectDefinition", "metadata", "annotations")
    85  			} else {
    86  				err := unstructured.SetNestedField(
    87  					template, annotations, "objectDefinition", "metadata", "annotations",
    88  				)
    89  				if err != nil {
    90  					return err
    91  				}
    92  			}
    93  
    94  			templates[i] = template
    95  			updated = true
    96  		}
    97  	}
    98  
    99  	if updated {
   100  		err := unstructured.SetNestedField(plc.Object, templates, "spec", "policy-templates")
   101  		if err != nil {
   102  			return err
   103  		}
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  // Pause sleep for given seconds
   110  func Pause(s uint) {
   111  	if s < 1 {
   112  		s = 1
   113  	}
   114  
   115  	time.Sleep(time.Duration(float64(s)) * time.Second)
   116  }
   117  
   118  // ParseYaml read given yaml file and unmarshal it to &unstructured.Unstructured{}
   119  func ParseYaml(file string) *unstructured.Unstructured {
   120  	yamlFile, err := os.ReadFile(file)
   121  	Expect(err).ToNot(HaveOccurred())
   122  
   123  	yamlPlc := &unstructured.Unstructured{}
   124  	err = yaml.Unmarshal(yamlFile, yamlPlc)
   125  	Expect(err).ToNot(HaveOccurred())
   126  
   127  	return yamlPlc
   128  }
   129  
   130  // GetClusterLevelWithTimeout keeps polling to get the object for timeout seconds until wantFound is met
   131  // (true for found, false for not found)
   132  func GetClusterLevelWithTimeout(
   133  	clientHubDynamic dynamic.Interface,
   134  	gvr schema.GroupVersionResource,
   135  	name string,
   136  	wantFound bool,
   137  	timeout int,
   138  ) *unstructured.Unstructured {
   139  	if timeout < 1 {
   140  		timeout = 1
   141  	}
   142  
   143  	var obj *unstructured.Unstructured
   144  
   145  	EventuallyWithOffset(1, func() error {
   146  		var err error
   147  		namespace := clientHubDynamic.Resource(gvr)
   148  
   149  		obj, err = namespace.Get(context.TODO(), name, metav1.GetOptions{})
   150  		if wantFound && err != nil {
   151  			return err
   152  		}
   153  
   154  		if !wantFound && err == nil {
   155  			return fmt.Errorf("expected to return IsNotFound error")
   156  		}
   157  
   158  		if !wantFound && err != nil && !errors.IsNotFound(err) {
   159  			return err
   160  		}
   161  
   162  		return nil
   163  	}, timeout, 1).ShouldNot(HaveOccurred())
   164  
   165  	if wantFound {
   166  		return obj
   167  	}
   168  
   169  	return nil
   170  }
   171  
   172  // GetWithTimeout keeps polling to get the object for timeout seconds until wantFound is met
   173  // (true for found, false for not found)
   174  func GetWithTimeout(
   175  	clientHubDynamic dynamic.Interface,
   176  	gvr schema.GroupVersionResource,
   177  	name, namespace string,
   178  	wantFound bool,
   179  	timeout int,
   180  ) *unstructured.Unstructured {
   181  	if timeout < 1 {
   182  		timeout = 1
   183  	}
   184  
   185  	var obj *unstructured.Unstructured
   186  
   187  	EventuallyWithOffset(1, func() error {
   188  		var err error
   189  		namespace := clientHubDynamic.Resource(gvr).Namespace(namespace)
   190  
   191  		obj, err = namespace.Get(context.TODO(), name, metav1.GetOptions{})
   192  		if wantFound && err != nil {
   193  			return err
   194  		}
   195  
   196  		if !wantFound && err == nil {
   197  			return fmt.Errorf("expected to return IsNotFound error")
   198  		}
   199  
   200  		if !wantFound && err != nil && !errors.IsNotFound(err) {
   201  			return err
   202  		}
   203  
   204  		return nil
   205  	}, timeout, 1).ShouldNot(HaveOccurred())
   206  
   207  	if wantFound {
   208  		return obj
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  // ListWithTimeout keeps polling to list the object for timeout seconds until wantFound is met
   215  // (true for found, false for not found)
   216  func ListWithTimeout(
   217  	clientHubDynamic dynamic.Interface,
   218  	gvr schema.GroupVersionResource,
   219  	opts metav1.ListOptions,
   220  	size int,
   221  	wantFound bool,
   222  	timeout int,
   223  ) *unstructured.UnstructuredList {
   224  	if timeout < 1 {
   225  		timeout = 1
   226  	}
   227  
   228  	var list *unstructured.UnstructuredList
   229  
   230  	EventuallyWithOffset(1, func() error {
   231  		var err error
   232  		list, err = clientHubDynamic.Resource(gvr).List(context.TODO(), opts)
   233  		if err != nil {
   234  			return err
   235  		}
   236  
   237  		if len(list.Items) != size {
   238  			return fmt.Errorf("list size doesn't match, expected %d actual %d", size, len(list.Items))
   239  		}
   240  
   241  		return nil
   242  	}, timeout, 1).ShouldNot(HaveOccurred())
   243  
   244  	if wantFound {
   245  		return list
   246  	}
   247  
   248  	return nil
   249  }
   250  
   251  // ListWithTimeoutByNamespace keeps polling to list the object for timeout seconds until wantFound is met
   252  // (true for found, false for not found)
   253  func ListWithTimeoutByNamespace(
   254  	clientHubDynamic dynamic.Interface,
   255  	gvr schema.GroupVersionResource,
   256  	opts metav1.ListOptions,
   257  	ns string,
   258  	size int,
   259  	wantFound bool,
   260  	timeout int,
   261  ) *unstructured.UnstructuredList {
   262  	if timeout < 1 {
   263  		timeout = 1
   264  	}
   265  
   266  	var list *unstructured.UnstructuredList
   267  
   268  	EventuallyWithOffset(1, func() error {
   269  		var err error
   270  		list, err = clientHubDynamic.Resource(gvr).Namespace(ns).List(context.TODO(), opts)
   271  		if err != nil {
   272  			return err
   273  		}
   274  
   275  		if len(list.Items) != size {
   276  			return fmt.Errorf("list size doesn't match, expected %d actual %d", size, len(list.Items))
   277  		}
   278  
   279  		return nil
   280  	}, timeout, 1).ShouldNot(HaveOccurred())
   281  
   282  	if wantFound {
   283  		return list
   284  	}
   285  
   286  	return nil
   287  }
   288  
   289  // Kubectl execute kubectl cli
   290  func Kubectl(args ...string) {
   291  	cmd := exec.Command("kubectl", args...)
   292  
   293  	err := cmd.Start()
   294  	if err != nil {
   295  		Fail(fmt.Sprintf("Error: %v", err))
   296  	}
   297  }
   298  
   299  // KubectlWithOutput execute kubectl cli and return output and error
   300  func KubectlWithOutput(args ...string) (string, error) {
   301  	kubectlCmd := exec.Command("kubectl", args...)
   302  
   303  	output, err := kubectlCmd.CombinedOutput()
   304  	if err != nil {
   305  		// Reformat error to include kubectl command and stderr output
   306  		err = fmt.Errorf(
   307  			"error running command '%s':\n %s: %s",
   308  			strings.Join(kubectlCmd.Args, " "),
   309  			output,
   310  			err.Error(),
   311  		)
   312  	}
   313  
   314  	return string(output), err
   315  }
   316  
   317  // GetMetrics execs into the propagator pod and curls the metrics endpoint, filters
   318  // the response with the given patterns, and returns the value(s) for the matching
   319  // metric(s).
   320  func GetMetrics(metricPatterns ...string) []string {
   321  	propPodInfo, err := KubectlWithOutput("get", "pod", "-n=open-cluster-management",
   322  		"-l=name=governance-policy-propagator", "--no-headers")
   323  	if err != nil {
   324  		return []string{err.Error()}
   325  	}
   326  
   327  	var cmd *exec.Cmd
   328  
   329  	metricFilter := " | grep " + strings.Join(metricPatterns, " | grep ")
   330  	metricsCmd := `curl localhost:8383/metrics` + metricFilter
   331  
   332  	// The pod name is "No" when the response is "No resources found"
   333  	propPodName := strings.Split(propPodInfo, " ")[0]
   334  	if propPodName == "No" {
   335  		// A missing pod could mean the controller is running locally
   336  		cmd = exec.Command("bash", "-c", metricsCmd)
   337  	} else {
   338  		cmd = exec.Command("kubectl", "exec", "-n=open-cluster-management", propPodName, "-c",
   339  			"governance-policy-propagator", "--", "bash", "-c", metricsCmd)
   340  	}
   341  
   342  	matchingMetricsRaw, err := cmd.Output()
   343  	if err != nil {
   344  		if err.Error() == "exit status 1" {
   345  			return []string{} // exit 1 indicates that grep couldn't find a match.
   346  		}
   347  
   348  		return []string{err.Error()}
   349  	}
   350  
   351  	matchingMetrics := strings.Split(strings.TrimSpace(string(matchingMetricsRaw)), "\n")
   352  	values := make([]string, len(matchingMetrics))
   353  
   354  	for i, metric := range matchingMetrics {
   355  		fields := strings.Fields(metric)
   356  		if len(fields) > 0 {
   357  			values[i] = fields[len(fields)-1]
   358  		}
   359  	}
   360  
   361  	return values
   362  }
   363  
   364  func GetMatchingEvents(
   365  	client kubernetes.Interface, namespace, objName, reasonRegex, msgRegex string, timeout int,
   366  ) []corev1.Event {
   367  	var eventList *corev1.EventList
   368  
   369  	EventuallyWithOffset(1, func() error {
   370  		var err error
   371  		eventList, err = client.CoreV1().Events(namespace).List(context.TODO(), metav1.ListOptions{})
   372  
   373  		return err
   374  	}, timeout, 1).ShouldNot(HaveOccurred())
   375  
   376  	matchingEvents := make([]corev1.Event, 0)
   377  	msgMatcher := regexp.MustCompile(msgRegex)
   378  	reasonMatcher := regexp.MustCompile(reasonRegex)
   379  
   380  	for _, event := range eventList.Items {
   381  		if event.InvolvedObject.Name == objName && reasonMatcher.MatchString(event.Reason) &&
   382  			msgMatcher.MatchString(event.Message) {
   383  			matchingEvents = append(matchingEvents, event)
   384  		}
   385  	}
   386  
   387  	return matchingEvents
   388  }
   389  
   390  // MetricsLines execs into the propagator pod and curls the metrics endpoint, and returns lines
   391  // that match the pattern.
   392  func MetricsLines(pattern string) (string, error) {
   393  	propPodInfo, err := KubectlWithOutput("get", "pod", "-n=open-cluster-management",
   394  		"-l=name=governance-policy-propagator", "--no-headers")
   395  	if err != nil {
   396  		return "", err
   397  	}
   398  
   399  	var cmd *exec.Cmd
   400  
   401  	metricsCmd := fmt.Sprintf(`curl localhost:8383/metrics | grep %q`, pattern)
   402  
   403  	// The pod name is "No" when the response is "No resources found"
   404  	propPodName := strings.Split(propPodInfo, " ")[0]
   405  	if propPodName == "No" {
   406  		// A missing pod could mean the controller is running locally
   407  		cmd = exec.Command("bash", "-c", metricsCmd)
   408  	} else {
   409  		cmd = exec.Command("kubectl", "exec", "-n=open-cluster-management", propPodName, "-c",
   410  			"governance-policy-propagator", "--", "bash", "-c", metricsCmd)
   411  	}
   412  
   413  	matchingMetricsRaw, err := cmd.Output()
   414  	if err != nil {
   415  		if err.Error() == "exit status 1" {
   416  			return "", nil // exit 1 indicates that grep couldn't find a match.
   417  		}
   418  
   419  		return "", err
   420  	}
   421  
   422  	return string(matchingMetricsRaw), nil
   423  }