github.com/operator-framework/operator-lifecycle-manager@v0.30.0/test/e2e/operator_test.go (about) 1 package e2e 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "time" 8 9 . "github.com/onsi/ginkgo/v2" 10 . "github.com/onsi/gomega" 11 "github.com/onsi/gomega/format" 12 gomegatypes "github.com/onsi/gomega/types" 13 "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" 14 "github.com/stretchr/testify/require" 15 corev1 "k8s.io/api/core/v1" 16 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 17 apierrors "k8s.io/apimachinery/pkg/api/errors" 18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/apimachinery/pkg/runtime" 20 "k8s.io/apimachinery/pkg/types" 21 "k8s.io/apimachinery/pkg/watch" 22 "k8s.io/client-go/tools/reference" 23 controllerclient "sigs.k8s.io/controller-runtime/pkg/client" 24 25 operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" 26 operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" 27 clientv1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/typed/operators/v1" 28 "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/decorators" 29 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/testobj" 30 "github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx" 31 ) 32 33 // Describes test specs for the Operator resource. 34 var _ = Describe("Operator API", func() { 35 var ( 36 clientCtx context.Context 37 scheme *runtime.Scheme 38 listOpts metav1.ListOptions 39 operatorClient clientv1.OperatorInterface 40 client controllerclient.Client 41 operatorFactory decorators.OperatorFactory 42 ) 43 44 BeforeEach(func() { 45 // Setup common utilities 46 clientCtx = context.Background() 47 scheme = ctx.Ctx().Scheme() 48 listOpts = metav1.ListOptions{} 49 operatorClient = ctx.Ctx().OperatorClient().OperatorsV1().Operators() 50 client = ctx.Ctx().Client() 51 52 var err error 53 operatorFactory, err = decorators.NewSchemedOperatorFactory(scheme) 54 Expect(err).ToNot(HaveOccurred()) 55 }) 56 57 // Ensures that an Operator resource can select its components by label and surface them correctly in its status. 58 // 59 // Steps: 60 // 1. Create an Operator resource, o 61 // 2. Ensure o's status eventually contains its component label selector 62 // 3. Create namespaces ns-a and ns-b 63 // 4. Label ns-a with o's component label 64 // 5. Ensure o's status.components.refs field eventually contains a reference to ns-a 65 // 6. Create ServiceAccounts sa-a and sa-b in namespaces ns-a and ns-b respectively 66 // 7. Label sa-a and sa-b with o's component label 67 // 8. Ensure o's status.components.refs field eventually contains references to sa-a and sa-b 68 // 9. Remove the component label from sa-b 69 // 10. Ensure the reference to sa-b is eventually removed from o's status.components.refs field 70 // 11. Delete o 71 // 12. Ensure o is re-created 72 // 13. Delete ns-a 73 // 14. Ensure the reference to ns-a is eventually removed from o's status.components.refs field 74 // 15. Delete o 75 // 16. Ensure o is not re-created 76 // issue: https://github.com/operator-framework/operator-lifecycle-manager/issues/2628 77 It("should surface components in its status", func() { 78 o := &operatorsv1.Operator{} 79 o.SetName(genName("o-")) 80 By(fmt.Sprintf("Creating an Operator resource %s", o.GetName())) 81 82 Consistently(o).ShouldNot(ContainCopiedCSVReferences()) 83 84 Eventually(func() error { 85 return client.Create(clientCtx, o) 86 }).Should(Succeed()) 87 88 defer func() { 89 Eventually(func() error { 90 if env := os.Getenv("SKIP_CLEANUP"); env != "" { 91 fmt.Printf("Skipping cleanup of operator %s...\n", o.GetName()) 92 return nil 93 } 94 err := client.Delete(clientCtx, o) 95 if apierrors.IsNotFound(err) { 96 return nil 97 } 98 99 return err 100 }).Should(Succeed()) 101 }() 102 103 By("eventually having a status that contains its component label selector") 104 w, err := operatorClient.Watch(clientCtx, listOpts) 105 Expect(err).ToNot(HaveOccurred()) 106 defer w.Stop() 107 108 deadline, cancel := context.WithTimeout(clientCtx, 1*time.Minute) 109 defer cancel() 110 111 expectedKey := "operators.coreos.com/" + o.GetName() 112 awaitPredicates(deadline, w, operatorPredicate(func(op *operatorsv1.Operator) bool { 113 if op.Status.Components == nil || op.Status.Components.LabelSelector == nil { 114 return false 115 } 116 117 for _, requirement := range op.Status.Components.LabelSelector.MatchExpressions { 118 if requirement.Key == expectedKey && requirement.Operator == metav1.LabelSelectorOpExists { 119 return true 120 } 121 } 122 123 return false 124 })) 125 defer w.Stop() 126 127 nsA := &corev1.Namespace{} 128 nsA.SetName(genName("ns-a-")) 129 nsB := &corev1.Namespace{} 130 nsB.SetName(genName("ns-b-")) 131 By(fmt.Sprintf("Create namespaces ns-a: (%s) and ns-b: (%s)", nsA.GetName(), nsB.GetName())) 132 133 for _, ns := range []*corev1.Namespace{nsA, nsB} { 134 Eventually(func() error { 135 return client.Create(clientCtx, ns) 136 }).Should(Succeed()) 137 138 defer func(n *corev1.Namespace) { 139 if env := os.Getenv("SKIP_CLEANUP"); env != "" { 140 fmt.Printf("Skipping cleanup of namespace %s...\n", n.GetName()) 141 return 142 } 143 Eventually(func() error { 144 err := client.Delete(clientCtx, n) 145 if apierrors.IsNotFound(err) { 146 return nil 147 } 148 return err 149 }).Should(Succeed()) 150 }(ns) 151 } 152 153 By(fmt.Sprintf("Label ns-a (%s) with o's (%s) component label (%s)", nsA.GetName(), o.GetName(), expectedKey)) 154 setComponentLabel := func(m metav1.Object) error { 155 m.SetLabels(map[string]string{ 156 install.OLMManagedLabelKey: install.OLMManagedLabelValue, 157 expectedKey: "", 158 }) 159 return nil 160 } 161 Eventually(Apply(nsA, setComponentLabel)).Should(Succeed()) 162 163 By("Ensure o's status.components.refs field eventually contains a reference to ns-a") 164 By("eventually listing a single component reference") 165 componentRefEventuallyExists(w, true, getReference(scheme, nsA)) 166 167 saA := &corev1.ServiceAccount{} 168 saA.SetName(genName("sa-a-")) 169 saA.SetNamespace(nsA.GetName()) 170 saB := &corev1.ServiceAccount{} 171 saB.SetName(genName("sa-b-")) 172 saB.SetNamespace(nsB.GetName()) 173 By(fmt.Sprintf("Create ServiceAccounts sa-a (%s/%s) and sa-b (%s/%s) in namespaces ns-a and ns-b respectively", saA.GetNamespace(), saA.GetName(), saB.GetNamespace(), saB.GetName())) 174 175 for _, sa := range []*corev1.ServiceAccount{saA, saB} { 176 Eventually(func() error { 177 return client.Create(clientCtx, sa) 178 }).Should(Succeed()) 179 defer func(sa *corev1.ServiceAccount) { 180 Eventually(func() error { 181 if env := os.Getenv("SKIP_CLEANUP"); env != "" { 182 fmt.Printf("Skipping cleanup of serviceaccount %s/%s...\n", sa.GetNamespace(), sa.GetName()) 183 return nil 184 } 185 err := client.Delete(clientCtx, sa) 186 if apierrors.IsNotFound(err) { 187 return nil 188 } 189 return err 190 }).Should(Succeed()) 191 }(sa) 192 } 193 194 By("Label sa-a and sa-b with o's component label") 195 Eventually(Apply(saA, setComponentLabel)).Should(Succeed()) 196 Eventually(Apply(saB, setComponentLabel)).Should(Succeed()) 197 198 By("Ensure o's status.components.refs field eventually contains references to sa-a and sa-b") 199 By("eventually listing multiple component references") 200 componentRefEventuallyExists(w, true, getReference(scheme, saA)) 201 componentRefEventuallyExists(w, true, getReference(scheme, saB)) 202 203 By("Remove the component label from sa-b") 204 Eventually(Apply(saB, func(m metav1.Object) error { 205 m.SetLabels(nil) 206 return nil 207 })).Should(Succeed()) 208 209 By("Ensure the reference to sa-b is eventually removed from o's status.components.refs field") 210 By("removing a component's reference when it no longer bears the component label") 211 componentRefEventuallyExists(w, false, getReference(scheme, saB)) 212 213 By("Delete o") 214 Eventually(func() error { 215 err := client.Delete(clientCtx, o) 216 if err != nil && !apierrors.IsNotFound(err) { 217 return err 218 } 219 return nil 220 }).Should(Succeed()) 221 222 By("Ensure that o is eventually recreated (because some of its components still exist).") 223 By("recreating the Operator when any components still exist") 224 Eventually(func() error { 225 return client.Get(clientCtx, types.NamespacedName{Name: o.GetName()}, o) 226 }).Should(Succeed()) 227 228 By("Delete ns-a") 229 Eventually(func() error { 230 err := client.Delete(clientCtx, nsA) 231 if apierrors.IsNotFound(err) { 232 return nil 233 } 234 return err 235 }).Should(Succeed()) 236 237 By("Ensure the reference to ns-a is eventually removed from o's status.components.refs field") 238 By("removing a component's reference when it no longer exists") 239 componentRefEventuallyExists(w, false, getReference(scheme, nsA)) 240 241 By("Delete o") 242 Eventually(func() error { 243 err := client.Delete(clientCtx, o) 244 if apierrors.IsNotFound(err) { 245 return nil 246 } 247 return err 248 }).Should(Succeed()) 249 250 By("Ensure that o is consistently not found") 251 By("verifying the Operator is permanently deleted if it has no components") 252 Consistently(func() error { 253 err := client.Get(clientCtx, types.NamespacedName{Name: o.GetName()}, o) 254 if apierrors.IsNotFound(err) { 255 return nil 256 } 257 return err 258 }).Should(Succeed()) 259 }) 260 261 Context("when a subscription to a package exists", func() { 262 var ( 263 ns *corev1.Namespace 264 sub *operatorsv1alpha1.Subscription 265 ip *operatorsv1alpha1.InstallPlan 266 operatorName types.NamespacedName 267 ) 268 269 BeforeEach(func() { 270 By("Subscribe to a package and await a successful install") 271 ns = &corev1.Namespace{} 272 ns.SetName(genName("ns-")) 273 Eventually(func() error { 274 return client.Create(clientCtx, ns) 275 }).Should(Succeed()) 276 By(fmt.Sprintf("created namespace %s", ns.Name)) 277 278 By("Default to AllNamespaces") 279 og := &operatorsv1.OperatorGroup{} 280 og.SetNamespace(ns.GetName()) 281 og.SetName(genName("og-")) 282 Eventually(func() error { 283 return client.Create(clientCtx, og) 284 }).Should(Succeed()) 285 By(fmt.Sprintf("created operator group %s/%s", og.Namespace, og.Name)) 286 287 cs := &operatorsv1alpha1.CatalogSource{ 288 Spec: operatorsv1alpha1.CatalogSourceSpec{ 289 SourceType: operatorsv1alpha1.SourceTypeGrpc, 290 Image: "quay.io/operator-framework/ci-index:latest", 291 GrpcPodConfig: &operatorsv1alpha1.GrpcPodConfig{ 292 SecurityContextConfig: operatorsv1alpha1.Restricted, 293 }, 294 }, 295 } 296 cs.SetNamespace(ns.GetName()) 297 cs.SetName(genName("cs-")) 298 Eventually(func() error { 299 return client.Create(clientCtx, cs) 300 }).Should(Succeed()) 301 By(fmt.Sprintf("created catalog source %s/%s", cs.Namespace, cs.Name)) 302 303 By("Wait for the CatalogSource to be ready") 304 _, err := fetchCatalogSourceOnStatus(newCRClient(), cs.GetName(), cs.GetNamespace(), catalogSourceRegistryPodSynced()) 305 Expect(err).ToNot(HaveOccurred()) 306 307 sub = &operatorsv1alpha1.Subscription{ 308 Spec: &operatorsv1alpha1.SubscriptionSpec{ 309 CatalogSource: cs.GetName(), 310 CatalogSourceNamespace: cs.GetNamespace(), 311 Package: "kiali", 312 Channel: "stable", 313 InstallPlanApproval: operatorsv1alpha1.ApprovalAutomatic, 314 }, 315 } 316 sub.SetNamespace(cs.GetNamespace()) 317 sub.SetName(genName("sub-")) 318 Eventually(func() error { 319 return client.Create(clientCtx, sub) 320 }).Should(Succeed()) 321 By(fmt.Sprintf("created subscription %s/%s", sub.Namespace, sub.Name)) 322 323 _, err = fetchSubscription(newCRClient(), sub.Namespace, sub.Name, subscriptionStateAtLatestChecker()) 324 require.NoError(GinkgoT(), err) 325 326 subscriptionWithInstallPLan, err := fetchSubscription(newCRClient(), sub.Namespace, sub.Name, subscriptionHasInstallPlanChecker()) 327 require.NoError(GinkgoT(), err) 328 require.NotNil(GinkgoT(), subscriptionWithInstallPLan) 329 ipRef := subscriptionWithInstallPLan.Status.InstallPlanRef 330 331 ip, err = fetchInstallPlan(GinkgoT(), newCRClient(), ipRef.Name, ipRef.Namespace, buildInstallPlanPhaseCheckFunc(operatorsv1alpha1.InstallPlanPhaseComplete)) 332 Expect(err).To(BeNil()) 333 334 operator, err := operatorFactory.NewPackageOperator(sub.Spec.Package, sub.GetNamespace()) 335 Expect(err).ToNot(HaveOccurred()) 336 operatorName = testobj.NamespacedName(operator) 337 By(fmt.Sprintf("waiting for operator %s/%s to exist", operator.Namespace, operator.Name)) 338 }) 339 340 AfterEach(func() { 341 Eventually(func() error { 342 if env := os.Getenv("SKIP_CLEANUP"); env != "" { 343 fmt.Printf("Skipping cleanup of namespace %s...\n", ns.Name) 344 return nil 345 } 346 err := client.Delete(clientCtx, ns) 347 if apierrors.IsNotFound(err) { 348 return nil 349 } 350 return err 351 }).Should(Succeed()) 352 }) 353 354 It("should automatically adopt components", func() { 355 Consistently(func() (*operatorsv1.Operator, error) { 356 o := &operatorsv1.Operator{} 357 err := client.Get(clientCtx, operatorName, o) 358 return o, err 359 }).ShouldNot(ContainCopiedCSVReferences()) 360 361 Eventually(func() (*operatorsv1.Operator, error) { 362 o := &operatorsv1.Operator{} 363 err := client.Get(clientCtx, operatorName, o) 364 return o, err 365 }).Should(ReferenceComponents([]*corev1.ObjectReference{ 366 getReference(scheme, sub), 367 getReference(scheme, ip), 368 getReference(scheme, testobj.WithNamespacedName( 369 &types.NamespacedName{Namespace: sub.GetNamespace(), Name: "kiali-operator.v1.4.2"}, 370 &operatorsv1alpha1.ClusterServiceVersion{}, 371 )), 372 getReference(scheme, testobj.WithNamespacedName( 373 &types.NamespacedName{Namespace: sub.GetNamespace(), Name: "kiali-operator"}, 374 &corev1.ServiceAccount{}, 375 )), 376 getReference(scheme, testobj.WithName("kialis.kiali.io", &apiextensionsv1.CustomResourceDefinition{})), 377 getReference(scheme, testobj.WithName("monitoringdashboards.monitoring.kiali.io", &apiextensionsv1.CustomResourceDefinition{})), 378 })) 379 }) 380 381 Context("when a namespace is added", func() { 382 383 var newNs *corev1.Namespace 384 385 BeforeEach(func() { 386 By("Subscribe to a package and await a successful install") 387 newNs = &corev1.Namespace{} 388 newNs.SetName(genName("ns-")) 389 Eventually(func() error { 390 return client.Create(clientCtx, newNs) 391 }).Should(Succeed()) 392 }) 393 394 AfterEach(func() { 395 Eventually(func() error { 396 err := client.Delete(clientCtx, newNs) 397 if apierrors.IsNotFound(err) { 398 return nil 399 } 400 return err 401 }).Should(Succeed()) 402 }) 403 404 It("should not adopt copied csvs", func() { 405 Consistently(func() (*operatorsv1.Operator, error) { 406 o := &operatorsv1.Operator{} 407 err := client.Get(clientCtx, operatorName, o) 408 return o, err 409 }).ShouldNot(ContainCopiedCSVReferences()) 410 }) 411 }) 412 }) 413 }) 414 415 func getReference(scheme *runtime.Scheme, obj runtime.Object) *corev1.ObjectReference { 416 ref, err := reference.GetReference(scheme, obj) 417 if err != nil { 418 panic(fmt.Sprintf("unable to get object reference: %s", err)) 419 } 420 ref.UID = "" 421 ref.ResourceVersion = "" 422 423 return ref 424 } 425 426 func componentRefEventuallyExists(w watch.Interface, exists bool, ref *corev1.ObjectReference) { 427 deadline, cancel := context.WithTimeout(context.Background(), 1*time.Minute) 428 defer cancel() 429 430 awaitPredicates(deadline, w, operatorPredicate(func(op *operatorsv1.Operator) bool { 431 if op.Status.Components == nil { 432 return false 433 } 434 435 for _, r := range op.Status.Components.Refs { 436 if r.APIVersion == ref.APIVersion && r.Kind == ref.Kind && r.Namespace == ref.Namespace && r.Name == ref.Name { 437 return exists 438 } 439 } 440 441 return !exists 442 })) 443 } 444 445 func ContainCopiedCSVReferences() gomegatypes.GomegaMatcher { 446 return &copiedCSVRefMatcher{} 447 } 448 449 type copiedCSVRefMatcher struct { 450 } 451 452 func (matcher *copiedCSVRefMatcher) Match(actual interface{}) (success bool, err error) { 453 if actual == nil { 454 return false, nil 455 } 456 operator, ok := actual.(*operatorsv1.Operator) 457 if !ok { 458 return false, fmt.Errorf("copiedCSVRefMatcher matcher expects an *Operator") 459 } 460 if operator.Status.Components == nil { 461 return false, nil 462 } 463 for _, ref := range operator.Status.Components.Refs { 464 if ref.Kind != operatorsv1alpha1.ClusterServiceVersionKind { 465 continue 466 } 467 for _, c := range ref.Conditions { 468 if c.Reason == string(operatorsv1alpha1.CSVReasonCopied) { 469 return true, nil 470 } 471 } 472 } 473 return false, nil 474 } 475 476 func (matcher *copiedCSVRefMatcher) FailureMessage(actual interface{}) (message string) { 477 operator, ok := actual.(*operatorsv1.Operator) 478 if !ok { 479 return "copiedCSVRefMatcher matcher expects an *Operator" 480 } 481 return fmt.Sprintf("Expected\n\t%#v\nto contain copied CSVs in components\n\t%#v\n", operator, operator.Status.Components) 482 } 483 484 func (matcher *copiedCSVRefMatcher) NegatedFailureMessage(actual interface{}) (message string) { 485 operator, ok := actual.(*operatorsv1.Operator) 486 if !ok { 487 return "copiedCSVRefMatcher matcher expects an *Operator" 488 } 489 return fmt.Sprintf("Expected\n\t%#v\nto not contain copied CSVs in components\n\t%#v\n", operator, operator.Status.Components) 490 } 491 492 func operatorPredicate(fn func(*operatorsv1.Operator) bool) predicateFunc { 493 return func(event watch.Event) bool { 494 o, ok := event.Object.(*operatorsv1.Operator) 495 if !ok { 496 panic(fmt.Sprintf("unexpected event object type %T in deployment", event.Object)) 497 } 498 499 return fn(o) 500 } 501 } 502 503 type OperatorMatcher struct { 504 matches func(*operatorsv1.Operator) (bool, error) 505 name string 506 } 507 508 func (o OperatorMatcher) Match(actual interface{}) (bool, error) { 509 operator, ok := actual.(*operatorsv1.Operator) 510 if !ok { 511 return false, fmt.Errorf("OperatorMatcher expects Operator (got %T)", actual) 512 } 513 514 return o.matches(operator) 515 } 516 517 func (o OperatorMatcher) String() string { 518 return o.name 519 } 520 521 func (o OperatorMatcher) FailureMessage(actual interface{}) string { 522 return format.Message(actual, "to satisfy", o) 523 } 524 525 func (o OperatorMatcher) NegatedFailureMessage(actual interface{}) string { 526 return format.Message(actual, "not to satisfy", o) 527 } 528 529 func ReferenceComponents(refs []*corev1.ObjectReference) gomegatypes.GomegaMatcher { 530 return &OperatorMatcher{ 531 matches: func(operator *operatorsv1.Operator) (bool, error) { 532 actual := map[corev1.ObjectReference]struct{}{} 533 for _, ref := range operator.Status.Components.Refs { 534 actual[*ref.ObjectReference] = struct{}{} 535 } 536 537 for _, ref := range refs { 538 if _, ok := actual[*ref]; !ok { 539 return false, nil 540 } 541 } 542 543 return true, nil 544 }, 545 name: fmt.Sprintf("ReferenceComponents(%v)", refs), 546 } 547 }