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 }