open-cluster-management.io/governance-policy-propagator@v0.13.0/test/e2e/case20_compliance_api_controller_test.go (about) 1 // Copyright Contributors to the Open Cluster Management project 2 3 package e2e 4 5 import ( 6 "bytes" 7 "context" 8 "database/sql" 9 "encoding/json" 10 "fmt" 11 "io" 12 "math/rand" 13 "net/http" 14 "time" 15 16 "github.com/google/uuid" 17 . "github.com/onsi/ginkgo/v2" 18 . "github.com/onsi/gomega" 19 corev1 "k8s.io/api/core/v1" 20 rbacv1 "k8s.io/api/rbac/v1" 21 k8serrors "k8s.io/apimachinery/pkg/api/errors" 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 25 "open-cluster-management.io/governance-policy-propagator/controllers/complianceeventsapi" 26 "open-cluster-management.io/governance-policy-propagator/controllers/propagator" 27 "open-cluster-management.io/governance-policy-propagator/test/utils" 28 ) 29 30 var _ = Describe("Test governance-policy-database secret changes, DB annotations, and events", Serial, Ordered, func() { 31 const ( 32 case20PolicyName string = "case20-policy" 33 case20PolicyYAML string = "../resources/case20_compliance_api_controller/policy.yaml" 34 ) 35 36 seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) 37 nsName := fmt.Sprintf("case20-%d", seededRand.Int31()) 38 39 createCase20Policy := func(ctx context.Context) { 40 By("Creating " + case20PolicyName) 41 utils.Kubectl("apply", "-f", case20PolicyYAML, "-n", nsName, "--kubeconfig="+kubeconfigHub) 42 plc := utils.GetWithTimeout( 43 clientHubDynamic, gvrPolicy, case20PolicyName, nsName, true, defaultTimeoutSeconds, 44 ) 45 ExpectWithOffset(1, plc).NotTo(BeNil()) 46 47 By("Patching the placement with decision of cluster local-cluster") 48 pld := utils.GetWithTimeout( 49 clientHubDynamic, 50 gvrPlacementDecision, 51 case20PolicyName, 52 nsName, 53 true, 54 defaultTimeoutSeconds, 55 ) 56 pld.Object["status"] = utils.GeneratePldStatus(pld.GetName(), pld.GetNamespace(), "local-cluster") 57 _, err := clientHubDynamic.Resource(gvrPlacementDecision).Namespace(nsName).UpdateStatus( 58 ctx, pld, metav1.UpdateOptions{}, 59 ) 60 ExpectWithOffset(1, err).ToNot(HaveOccurred()) 61 62 By("Waiting for the replicated policy") 63 replicatedPolicy := utils.GetWithTimeout( 64 clientHubDynamic, gvrPolicy, case20PolicyName, nsName, true, defaultTimeoutSeconds, 65 ) 66 ExpectWithOffset(1, replicatedPolicy).NotTo(BeNil()) 67 } 68 69 waitForDisabledEvent := func(ctx context.Context, after time.Time) { 70 afterStr := after.Format(time.RFC3339Nano) 71 72 By("Waiting for the disabled compliance event after " + afterStr) 73 EventuallyWithOffset(1, func(g Gomega) { 74 endpoint := fmt.Sprintf( 75 "https://localhost:%d/api/v1/compliance-events?cluster.name=local-cluster&event.compliance=Disabled"+ 76 "&event.timestamp_after=%s&policy.name=%s", 77 complianceAPIPort, 78 afterStr, 79 case20PolicyName, 80 ) 81 82 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) 83 g.Expect(err).ToNot(HaveOccurred()) 84 85 req.Header.Set("Authorization", "Bearer "+clientToken) 86 87 resp, err := httpClient.Do(req) 88 if err != nil { 89 return 90 } 91 92 defer resp.Body.Close() 93 94 g.Expect(resp.StatusCode).To(Equal(http.StatusOK)) 95 96 body, err := io.ReadAll(resp.Body) 97 g.Expect(err).ToNot(HaveOccurred()) 98 99 result := map[string]any{} 100 101 err = json.Unmarshal(body, &result) 102 g.Expect(err).ToNot(HaveOccurred()) 103 104 metadata, ok := result["metadata"].(map[string]interface{}) 105 g.Expect(ok).To(BeTrue(), "The metadata key was the wrong type") 106 107 g.Expect(metadata["total"]).To(BeEquivalentTo(1)) 108 }, defaultTimeoutSeconds*2, 1).Should(Succeed()) 109 } 110 111 BeforeAll(func(ctx context.Context) { 112 Expect(clientToken).ToNot(BeEmpty(), "Ensure you use the service account kubeconfig (kubeconfig_hub)") 113 114 By("Creating a random namespace to avoid a cache hit") 115 ns := &corev1.Namespace{ 116 ObjectMeta: metav1.ObjectMeta{ 117 Name: nsName, 118 }, 119 } 120 _, err := clientHub.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}) 121 Expect(err).ToNot(HaveOccurred()) 122 }) 123 124 AfterEach(func(ctx context.Context) { 125 restoreDBConnection(ctx) 126 }) 127 128 AfterAll(func(ctx context.Context) { 129 err := clientHub.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{}) 130 Expect(err).ToNot(HaveOccurred()) 131 }) 132 133 It("Adds missing database IDs once database connection is restored", func(ctx context.Context) { 134 By("Updating the connection to be invalid") 135 bringDownDBConnection(ctx) 136 137 createCase20Policy(ctx) 138 139 By("Getting the replicated policy") 140 replicatedPolicy := utils.GetWithTimeout( 141 clientHubDynamic, gvrPolicy, case20PolicyName, nsName, true, defaultTimeoutSeconds, 142 ) 143 Expect(replicatedPolicy).NotTo(BeNil()) 144 145 annotations := replicatedPolicy.GetAnnotations() 146 Expect(annotations[propagator.ParentPolicyIDAnnotation]).To(BeEmpty()) 147 Expect(annotations[propagator.PolicyIDAnnotation]).To(BeEmpty()) 148 149 By("Restoring the database connection") 150 Eventually(func(g Gomega) { 151 namespacedSecret := clientHub.CoreV1().Secrets("open-cluster-management") 152 secret, err := namespacedSecret.Get( 153 ctx, complianceeventsapi.DBSecretName, metav1.GetOptions{}, 154 ) 155 g.Expect(err).ToNot(HaveOccurred()) 156 157 delete(secret.Data, "port") 158 159 _, err = namespacedSecret.Update(ctx, secret, metav1.UpdateOptions{}) 160 g.Expect(err).ToNot(HaveOccurred()) 161 }, defaultTimeoutSeconds, 1).Should(Succeed()) 162 163 By("Waiting for the replicated policy to have the database ID annotations") 164 Eventually(func(g Gomega) { 165 replicatedPolicy = utils.GetWithTimeout( 166 clientHubDynamic, gvrPolicy, nsName+"."+case20PolicyName, "local-cluster", true, defaultTimeoutSeconds, 167 ) 168 g.Expect(replicatedPolicy).NotTo(BeNil()) 169 170 annotations = replicatedPolicy.GetAnnotations() 171 g.Expect(annotations[propagator.ParentPolicyIDAnnotation]).ToNot(BeEmpty()) 172 173 templates, _, _ := unstructured.NestedSlice(replicatedPolicy.Object, "spec", "policy-templates") 174 g.Expect(templates).To(HaveLen(1)) 175 176 policyID, _, _ := unstructured.NestedString( 177 templates[0].(map[string]interface{}), 178 "objectDefinition", 179 "metadata", 180 "annotations", 181 propagator.PolicyIDAnnotation, 182 ) 183 g.Expect(policyID).ToNot(BeEmpty()) 184 }, defaultTimeoutSeconds, 1).Should(Succeed()) 185 }) 186 187 It("Creates a disabled event for local-cluster", func(ctx context.Context) { 188 now := time.Now().UTC().Add(-1 * time.Second) 189 190 By("Deleting " + case20PolicyName) 191 utils.Kubectl("delete", "-f", case20PolicyYAML, "-n", nsName, "--kubeconfig="+kubeconfigHub) 192 plc := utils.GetWithTimeout( 193 clientHubDynamic, gvrPolicy, case20PolicyName, nsName, false, defaultTimeoutSeconds, 194 ) 195 Expect(plc).To(BeNil()) 196 197 waitForDisabledEvent(ctx, now) 198 }) 199 200 It("Creates a disabled event for local-cluster when the database is down and restored", func(ctx context.Context) { 201 createCase20Policy(ctx) 202 203 bringDownDBConnection(ctx) 204 205 now := time.Now().UTC().Add(-1 * time.Second) 206 207 By("Deleting " + case20PolicyName) 208 utils.Kubectl("delete", "-f", case20PolicyYAML, "-n", nsName, "--kubeconfig="+kubeconfigHub) 209 plc := utils.GetWithTimeout( 210 clientHubDynamic, gvrPolicy, case20PolicyName, nsName, false, defaultTimeoutSeconds, 211 ) 212 Expect(plc).To(BeNil()) 213 214 By("Waiting for the replicated policy to be deleted") 215 replicatedPolicy := utils.GetWithTimeout( 216 clientHubDynamic, gvrPolicy, case20PolicyName, nsName, false, defaultTimeoutSeconds, 217 ) 218 Expect(replicatedPolicy).To(BeNil()) 219 220 restoreDBConnection(ctx) 221 222 waitForDisabledEvent(ctx, now) 223 }) 224 }) 225 226 func bringDownDBConnection(ctx context.Context) { 227 By("Setting the port to 12345") 228 EventuallyWithOffset(1, func(g Gomega) { 229 namespacedSecret := clientHub.CoreV1().Secrets("open-cluster-management") 230 secret, err := namespacedSecret.Get( 231 ctx, complianceeventsapi.DBSecretName, metav1.GetOptions{}, 232 ) 233 g.Expect(err).ToNot(HaveOccurred()) 234 235 secret.StringData = map[string]string{"port": "12345"} 236 237 _, err = namespacedSecret.Update(ctx, secret, metav1.UpdateOptions{}) 238 g.Expect(err).ToNot(HaveOccurred()) 239 }, defaultTimeoutSeconds, 1).Should(Succeed()) 240 241 By("Waiting for the database connection to be down") 242 EventuallyWithOffset(1, func(g Gomega) { 243 req, err := http.NewRequestWithContext( 244 ctx, http.MethodGet, fmt.Sprintf("https://localhost:%d/api/v1/compliance-events/1", complianceAPIPort), nil, 245 ) 246 g.Expect(err).ToNot(HaveOccurred()) 247 248 req.Header.Set("Authorization", "Bearer "+clientToken) 249 250 resp, err := httpClient.Do(req) 251 if err != nil { 252 return 253 } 254 255 defer resp.Body.Close() 256 257 g.Expect(resp.StatusCode).To(Equal(http.StatusInternalServerError)) 258 259 body, err := io.ReadAll(resp.Body) 260 g.Expect(err).ToNot(HaveOccurred()) 261 262 respJSON := map[string]any{} 263 264 err = json.Unmarshal(body, &respJSON) 265 g.Expect(err).ToNot(HaveOccurred()) 266 267 g.Expect(respJSON["message"]).To(Equal("The database is unavailable")) 268 }, defaultTimeoutSeconds, 1).Should(Succeed()) 269 } 270 271 func restoreDBConnection(ctx context.Context) { 272 By("Restoring the database connection") 273 EventuallyWithOffset(1, func(g Gomega) { 274 namespacedSecret := clientHub.CoreV1().Secrets("open-cluster-management") 275 secret, err := namespacedSecret.Get( 276 ctx, complianceeventsapi.DBSecretName, metav1.GetOptions{}, 277 ) 278 g.Expect(err).ToNot(HaveOccurred()) 279 280 if secret.Data["port"] == nil { 281 return 282 } 283 284 delete(secret.Data, "port") 285 286 _, err = namespacedSecret.Update(ctx, secret, metav1.UpdateOptions{}) 287 g.Expect(err).ToNot(HaveOccurred()) 288 }, defaultTimeoutSeconds, 1).Should(Succeed()) 289 290 By("Waiting for the database connection to be up") 291 EventuallyWithOffset(1, func(g Gomega) { 292 req, err := http.NewRequestWithContext( 293 ctx, 294 http.MethodGet, 295 fmt.Sprintf("https://localhost:%d/api/v1/compliance-events?per_page=1", complianceAPIPort), 296 nil, 297 ) 298 g.Expect(err).ToNot(HaveOccurred()) 299 300 req.Header.Set("Authorization", "Bearer "+clientToken) 301 302 resp, err := httpClient.Do(req) 303 if err != nil { 304 return 305 } 306 307 defer resp.Body.Close() 308 309 g.Expect(resp.StatusCode).To(Equal(http.StatusOK)) 310 }, defaultTimeoutSeconds, 1).Should(Succeed()) 311 } 312 313 var _ = Describe("Test compliance events API authentication and authorization", Serial, Ordered, func() { 314 eventsEndpoint := fmt.Sprintf("https://localhost:%d/api/v1/compliance-events", complianceAPIPort) 315 const saName string = "compliance-api-user" 316 var token string 317 318 getSamplePostRequest := func(clusterName string) *bytes.Buffer { 319 payload := []byte(fmt.Sprintf(`{ 320 "cluster": { 321 "name": "%s", 322 "cluster_id": "%s" 323 }, 324 "parent_policy": { 325 "name": "parent-policy", 326 "namespace": "%s" 327 }, 328 "policy": { 329 "apiGroup": "policy.open-cluster-management.io", 330 "kind": "ConfigurationPolicy", 331 "name": "etcd-encryption", 332 "spec": {"uid": "%s"} 333 }, 334 "event": { 335 "compliance": "NonCompliant", 336 "message": "configmaps [etcd] not found in namespace default", 337 "timestamp": "2023-02-02T02:02:02.222Z" 338 } 339 }`, clusterName, uuid.New().String(), uuid.New().String(), uuid.New().String())) 340 341 return bytes.NewBuffer(payload) 342 } 343 344 BeforeAll(func(ctx context.Context) { 345 By("Creating the service account " + saName + " in the namespace" + testNamespace) 346 _, err := clientHub.CoreV1().ServiceAccounts(testNamespace).Create( 347 ctx, 348 &corev1.ServiceAccount{ 349 ObjectMeta: metav1.ObjectMeta{ 350 Name: saName, 351 Namespace: testNamespace, 352 }, 353 }, 354 metav1.CreateOptions{}, 355 ) 356 Expect(err).ToNot(HaveOccurred()) 357 358 _, err = clientHub.CoreV1().Secrets(testNamespace).Create( 359 ctx, 360 &corev1.Secret{ 361 ObjectMeta: metav1.ObjectMeta{ 362 Name: saName, 363 Namespace: testNamespace, 364 Annotations: map[string]string{ 365 corev1.ServiceAccountNameKey: saName, 366 }, 367 }, 368 Type: corev1.SecretTypeServiceAccountToken, 369 }, 370 metav1.CreateOptions{}, 371 ) 372 Expect(err).ToNot(HaveOccurred()) 373 374 By("Granting the service account " + saName + " permission to the cluster namespace " + testNamespace) 375 _, err = clientHub.RbacV1().Roles(testNamespace).Create( 376 ctx, 377 &rbacv1.Role{ 378 ObjectMeta: metav1.ObjectMeta{ 379 Name: saName, 380 Namespace: testNamespace, 381 }, 382 Rules: []rbacv1.PolicyRule{ 383 { 384 APIGroups: []string{"policy.open-cluster-management.io"}, 385 Resources: []string{"policies/status"}, 386 Verbs: []string{"patch"}, 387 }, 388 }, 389 }, 390 metav1.CreateOptions{}, 391 ) 392 Expect(err).ToNot(HaveOccurred()) 393 394 _, err = clientHub.RbacV1().RoleBindings(testNamespace).Create( 395 ctx, 396 &rbacv1.RoleBinding{ 397 ObjectMeta: metav1.ObjectMeta{ 398 Name: saName, 399 Namespace: testNamespace, 400 }, 401 Subjects: []rbacv1.Subject{ 402 { 403 Kind: "ServiceAccount", 404 Name: saName, 405 Namespace: testNamespace, 406 }, 407 }, 408 RoleRef: rbacv1.RoleRef{ 409 APIGroup: "rbac.authorization.k8s.io", 410 Kind: "Role", 411 Name: saName, 412 }, 413 }, 414 metav1.CreateOptions{}, 415 ) 416 Expect(err).ToNot(HaveOccurred()) 417 418 Eventually(func(g Gomega) { 419 secret, err := clientHub.CoreV1().Secrets(testNamespace).Get(ctx, saName, metav1.GetOptions{}) 420 g.Expect(err).ToNot(HaveOccurred()) 421 422 g.Expect(secret.Data["token"]).ToNot(BeNil()) 423 424 token = string(secret.Data["token"]) 425 }, defaultTimeoutSeconds, 1).Should(Succeed()) 426 }) 427 428 AfterAll(func(ctx context.Context) { 429 By("Deleting the service account") 430 err := clientHub.CoreV1().ServiceAccounts(testNamespace).Delete(ctx, saName, metav1.DeleteOptions{}) 431 if !k8serrors.IsNotFound(err) { 432 Expect(err).ToNot(HaveOccurred()) 433 } 434 435 err = clientHub.CoreV1().Secrets(testNamespace).Delete(ctx, saName, metav1.DeleteOptions{}) 436 if !k8serrors.IsNotFound(err) { 437 Expect(err).ToNot(HaveOccurred()) 438 } 439 440 err = clientHub.RbacV1().Roles(testNamespace).Delete(ctx, saName, metav1.DeleteOptions{}) 441 if !k8serrors.IsNotFound(err) { 442 Expect(err).ToNot(HaveOccurred()) 443 } 444 445 err = clientHub.RbacV1().RoleBindings(testNamespace).Delete(ctx, saName, metav1.DeleteOptions{}) 446 if !k8serrors.IsNotFound(err) { 447 Expect(err).ToNot(HaveOccurred()) 448 } 449 450 By("Deleting all database records") 451 connectionURL := "postgresql://grc:grc@localhost:5432/ocm-compliance-history?sslmode=disable" 452 db, err := sql.Open("postgres", connectionURL) 453 DeferCleanup(func() { 454 Expect(db.Close()).To(Succeed()) 455 }) 456 457 Expect(err).ToNot(HaveOccurred()) 458 459 _, err = db.ExecContext(ctx, "DELETE FROM compliance_events") 460 Expect(err).ToNot(HaveOccurred()) 461 _, err = db.ExecContext(ctx, "DELETE FROM clusters") 462 Expect(err).ToNot(HaveOccurred()) 463 _, err = db.ExecContext(ctx, "DELETE FROM parent_policies") 464 Expect(err).ToNot(HaveOccurred()) 465 _, err = db.ExecContext(ctx, "DELETE FROM policies") 466 Expect(err).ToNot(HaveOccurred()) 467 }) 468 469 It("Rejects recording the compliance event without authentication", func(ctx context.Context) { 470 payload := getSamplePostRequest("cluster") 471 472 req, err := http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) 473 Expect(err).ToNot(HaveOccurred()) 474 req.Header.Set("Content-Type", "application/json") 475 476 resp, err := httpClient.Do(req) 477 Expect(err).ToNot(HaveOccurred()) 478 479 if resp != nil { 480 defer resp.Body.Close() 481 } 482 483 Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized)) 484 }) 485 486 It("Rejects recording the compliance event for the wrong namespace", func(ctx context.Context) { 487 payload := getSamplePostRequest("cluster") 488 489 req, err := http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) 490 Expect(err).ToNot(HaveOccurred()) 491 req.Header.Set("Content-Type", "application/json") 492 req.Header.Set("Authorization", "Bearer "+token) 493 494 resp, err := httpClient.Do(req) 495 Expect(err).ToNot(HaveOccurred()) 496 497 if resp != nil { 498 defer resp.Body.Close() 499 } 500 501 Expect(resp.StatusCode).To(Equal(http.StatusForbidden)) 502 }) 503 504 It("Allows recording the compliance event", func(ctx context.Context) { 505 payload := getSamplePostRequest(testNamespace) 506 507 req, err := http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) 508 Expect(err).ToNot(HaveOccurred()) 509 req.Header.Set("Content-Type", "application/json") 510 req.Header.Set("Authorization", "Bearer "+token) 511 512 resp, err := httpClient.Do(req) 513 Expect(err).ToNot(HaveOccurred()) 514 515 if resp != nil { 516 defer resp.Body.Close() 517 } 518 519 Expect(resp.StatusCode).To(Equal(http.StatusCreated)) 520 }) 521 522 It("Clears its database ID cache when the database loses data", func(ctx context.Context) { 523 By("Creating a compliance event") 524 payloadStr := getSamplePostRequest(testNamespace).String() 525 payload := bytes.NewBufferString(payloadStr) 526 527 req, err := http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) 528 Expect(err).ToNot(HaveOccurred()) 529 req.Header.Set("Content-Type", "application/json") 530 req.Header.Set("Authorization", "Bearer "+token) 531 532 resp, err := httpClient.Do(req) 533 Expect(err).ToNot(HaveOccurred()) 534 535 if resp != nil { 536 defer resp.Body.Close() 537 } 538 539 Expect(resp.StatusCode).To(Equal(http.StatusCreated)) 540 541 By("Deleting all compliance events and policy references") 542 connectionURL := "postgresql://grc:grc@localhost:5432/ocm-compliance-history?sslmode=disable" 543 db, err := sql.Open("postgres", connectionURL) 544 DeferCleanup(func() { 545 Expect(db.Close()).To(Succeed()) 546 }) 547 548 Expect(err).ToNot(HaveOccurred()) 549 550 _, err = db.ExecContext(ctx, "DELETE FROM compliance_events") 551 Expect(err).ToNot(HaveOccurred()) 552 _, err = db.ExecContext(ctx, "DELETE FROM parent_policies") 553 Expect(err).ToNot(HaveOccurred()) 554 _, err = db.ExecContext(ctx, "DELETE FROM policies") 555 Expect(err).ToNot(HaveOccurred()) 556 557 By("Verifying an internal error is returned the first time an invalid ID is provided") 558 payload = bytes.NewBufferString(payloadStr) 559 req, err = http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) 560 Expect(err).ToNot(HaveOccurred()) 561 req.Header.Set("Content-Type", "application/json") 562 req.Header.Set("Authorization", "Bearer "+token) 563 564 resp, err = httpClient.Do(req) 565 Expect(err).ToNot(HaveOccurred()) 566 567 if resp != nil { 568 defer resp.Body.Close() 569 } 570 571 body, err := io.ReadAll(resp.Body) 572 Expect(err).ToNot(HaveOccurred()) 573 574 Expect(resp.StatusCode).To(Equal(http.StatusInternalServerError), fmt.Sprintf("Got response %s", string(body))) 575 576 By("Verifying a success after the cache is cleared") 577 payload = bytes.NewBufferString(payloadStr) 578 req, err = http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) 579 Expect(err).ToNot(HaveOccurred()) 580 req.Header.Set("Content-Type", "application/json") 581 req.Header.Set("Authorization", "Bearer "+token) 582 583 resp, err = httpClient.Do(req) 584 Expect(err).ToNot(HaveOccurred()) 585 586 if resp != nil { 587 defer resp.Body.Close() 588 } 589 590 Expect(resp.StatusCode).To(Equal(http.StatusCreated)) 591 }) 592 })