github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/registry/reconciler/configmap_test.go (about) 1 package reconciler 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "testing" 8 "time" 9 10 "github.com/ghodss/yaml" 11 k8slabels "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/labels" 12 "github.com/sirupsen/logrus" 13 "github.com/stretchr/testify/require" 14 corev1 "k8s.io/api/core/v1" 15 rbacv1 "k8s.io/api/rbac/v1" 16 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 17 "k8s.io/apimachinery/pkg/api/meta" 18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/apimachinery/pkg/labels" 20 "k8s.io/apimachinery/pkg/runtime" 21 "k8s.io/apimachinery/pkg/types" 22 "k8s.io/client-go/informers" 23 "k8s.io/client-go/tools/cache" 24 25 "github.com/operator-framework/api/pkg/operators/v1alpha1" 26 "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry" 27 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/clientfake" 28 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" 29 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" 30 ) 31 32 const ( 33 registryImageName = "test:image" 34 runAsUser = 1001 35 testNamespace = "testns" 36 ) 37 38 type fakeReconcilerConfig struct { 39 now nowFunc 40 k8sObjs []runtime.Object 41 k8sClientOptions []clientfake.Option 42 configMapServerImage string 43 } 44 45 type fakeReconcilerOption func(*fakeReconcilerConfig) 46 47 func withNow(now nowFunc) fakeReconcilerOption { 48 return func(config *fakeReconcilerConfig) { 49 config.now = now 50 } 51 } 52 53 func withK8sObjs(k8sObjs ...runtime.Object) fakeReconcilerOption { 54 return func(config *fakeReconcilerConfig) { 55 config.k8sObjs = k8sObjs 56 } 57 } 58 59 func withK8sClientOptions(options ...clientfake.Option) fakeReconcilerOption { 60 return func(config *fakeReconcilerConfig) { 61 config.k8sClientOptions = options 62 } 63 } 64 65 func fakeReconcilerFactory(t *testing.T, stopc <-chan struct{}, options ...fakeReconcilerOption) (RegistryReconcilerFactory, operatorclient.ClientInterface) { 66 config := &fakeReconcilerConfig{ 67 now: metav1.Now, 68 configMapServerImage: registryImageName, 69 } 70 71 // Apply all config options 72 for _, option := range options { 73 option(config) 74 } 75 76 opClientFake := operatorclient.NewClient(clientfake.NewReactionForwardingClientsetDecorator(config.k8sObjs, config.k8sClientOptions...), nil, nil) 77 78 // Creates registry pods in response to configmaps 79 informerFactory := informers.NewSharedInformerFactory(opClientFake.KubernetesInterface(), 5*time.Second) 80 roleInformer := informerFactory.Rbac().V1().Roles() 81 roleBindingInformer := informerFactory.Rbac().V1().RoleBindings() 82 serviceAccountInformer := informerFactory.Core().V1().ServiceAccounts() 83 serviceInformer := informerFactory.Core().V1().Services() 84 podInformer := informerFactory.Core().V1().Pods() 85 configMapInformer := informerFactory.Core().V1().ConfigMaps() 86 87 registryInformers := []cache.SharedIndexInformer{ 88 roleInformer.Informer(), 89 roleBindingInformer.Informer(), 90 serviceAccountInformer.Informer(), 91 serviceInformer.Informer(), 92 podInformer.Informer(), 93 configMapInformer.Informer(), 94 } 95 96 lister := operatorlister.NewLister() 97 lister.RbacV1().RegisterRoleLister(testNamespace, roleInformer.Lister()) 98 lister.RbacV1().RegisterRoleBindingLister(testNamespace, roleBindingInformer.Lister()) 99 lister.CoreV1().RegisterServiceAccountLister(testNamespace, serviceAccountInformer.Lister()) 100 lister.CoreV1().RegisterServiceLister(testNamespace, serviceInformer.Lister()) 101 lister.CoreV1().RegisterPodLister(testNamespace, podInformer.Lister()) 102 lister.CoreV1().RegisterConfigMapLister(testNamespace, configMapInformer.Lister()) 103 104 rec := ®istryReconcilerFactory{ 105 now: config.now, 106 OpClient: opClientFake, 107 Lister: lister, 108 ConfigMapServerImage: config.configMapServerImage, 109 createPodAsUser: runAsUser, 110 } 111 112 var hasSyncedCheckFns []cache.InformerSynced 113 for _, informer := range registryInformers { 114 hasSyncedCheckFns = append(hasSyncedCheckFns, informer.HasSynced) 115 go informer.Run(stopc) 116 } 117 118 require.True(t, cache.WaitForCacheSync(stopc, hasSyncedCheckFns...), "caches failed to sync") 119 120 return rec, opClientFake 121 } 122 123 func crd(name string) v1beta1.CustomResourceDefinition { 124 return v1beta1.CustomResourceDefinition{ 125 ObjectMeta: metav1.ObjectMeta{ 126 Name: name, 127 }, 128 Spec: v1beta1.CustomResourceDefinitionSpec{ 129 Group: name + "group", 130 Versions: []v1beta1.CustomResourceDefinitionVersion{ 131 { 132 Name: "v1", 133 Served: true, 134 Storage: true, 135 }, 136 }, 137 Names: v1beta1.CustomResourceDefinitionNames{ 138 Kind: name, 139 }, 140 }, 141 } 142 } 143 144 func validConfigMap() *corev1.ConfigMap { 145 data := make(map[string]string) 146 dataYaml, _ := yaml.Marshal([]v1beta1.CustomResourceDefinition{crd("fake-crd")}) 147 data["customResourceDefinitions"] = string(dataYaml) 148 return &corev1.ConfigMap{ 149 ObjectMeta: metav1.ObjectMeta{ 150 Name: "cool-configmap", 151 Namespace: testNamespace, 152 UID: types.UID("configmap-uid"), 153 ResourceVersion: "resource-version", 154 }, 155 Data: data, 156 } 157 } 158 159 func TestValidConfigMap(t *testing.T) { 160 cm := validConfigMap() 161 require.NotNil(t, cm) 162 require.Contains(t, cm.Data[registry.ConfigMapCRDName], "fake") 163 } 164 165 func validConfigMapCatalogSource(configMap *corev1.ConfigMap) *v1alpha1.CatalogSource { 166 return &v1alpha1.CatalogSource{ 167 ObjectMeta: metav1.ObjectMeta{ 168 Name: "cool-catalog", 169 Namespace: testNamespace, 170 UID: types.UID("catalog-uid"), 171 Labels: map[string]string{"olm.catalogSource": "cool-catalog"}, 172 }, 173 Spec: v1alpha1.CatalogSourceSpec{ 174 ConfigMap: "cool-configmap", 175 SourceType: v1alpha1.SourceTypeConfigmap, 176 }, 177 Status: v1alpha1.CatalogSourceStatus{ 178 ConfigMapResource: &v1alpha1.ConfigMapResourceReference{ 179 Name: configMap.GetName(), 180 Namespace: configMap.GetNamespace(), 181 UID: configMap.GetUID(), 182 ResourceVersion: configMap.GetResourceVersion(), 183 }, 184 }, 185 } 186 } 187 188 func objectsForCatalogSource(t *testing.T, catsrc *v1alpha1.CatalogSource) []runtime.Object { 189 // the registry pod security context is derived from the defaultNamespace by default 190 // therefore a namespace resource must always be present 191 var objs = []runtime.Object{ 192 defaultNamespace(), 193 } 194 195 switch catsrc.Spec.SourceType { 196 case v1alpha1.SourceTypeInternal, v1alpha1.SourceTypeConfigmap: 197 decorated := configMapCatalogSourceDecorator{catsrc, runAsUser} 198 service, err := decorated.Service() 199 if err != nil { 200 t.Fatal(err) 201 } 202 serviceAccount := decorated.ServiceAccount() 203 pod, err := decorated.Pod(registryImageName, defaultPodSecurityConfig) 204 if err != nil { 205 t.Fatal(err) 206 } 207 objs = append(objs, 208 pod, 209 service, 210 serviceAccount, 211 ) 212 case v1alpha1.SourceTypeGrpc: 213 if catsrc.Spec.Image != "" { 214 decorated := grpcCatalogSourceDecorator{CatalogSource: catsrc, createPodAsUser: runAsUser, opmImage: ""} 215 serviceAccount := decorated.ServiceAccount() 216 service, err := decorated.Service() 217 if err != nil { 218 t.Fatal(err) 219 } 220 pod, err := decorated.Pod(serviceAccount, defaultPodSecurityConfig) 221 if err != nil { 222 t.Fatal(err) 223 } 224 objs = append(objs, 225 pod, 226 service, 227 serviceAccount, 228 ) 229 } 230 } 231 232 blockOwnerDeletion := false 233 isController := false 234 for _, o := range objs { 235 mo := o.(metav1.Object) 236 mo.SetOwnerReferences([]metav1.OwnerReference{{ 237 APIVersion: "operators.coreos.com/v1alpha1", 238 Kind: "CatalogSource", 239 Name: catsrc.GetName(), 240 UID: catsrc.GetUID(), 241 BlockOwnerDeletion: &blockOwnerDeletion, 242 Controller: &isController, 243 }}) 244 } 245 return objs 246 } 247 248 func modifyObjName(objs []runtime.Object, kind runtime.Object, newName string) []runtime.Object { 249 var out []runtime.Object 250 t := reflect.TypeOf(kind) 251 for _, obj := range objs { 252 o := obj.DeepCopyObject() 253 if reflect.TypeOf(o) == t { 254 if accessor, err := meta.Accessor(o); err == nil { 255 accessor.SetName(newName) 256 } 257 } 258 out = append(out, o) 259 } 260 return out 261 } 262 263 func setLabel(objs []runtime.Object, kind runtime.Object, label, value string) []runtime.Object { 264 var out []runtime.Object 265 t := reflect.TypeOf(kind) 266 for _, obj := range objs { 267 o := obj.DeepCopyObject() 268 if reflect.TypeOf(o) == t { 269 if accessor, err := meta.Accessor(o); err == nil { 270 k8slabels.AddLabel(accessor.GetLabels(), label, value) 271 } 272 } 273 out = append(out, o) 274 } 275 return out 276 } 277 278 func TestConfigMapRegistryReconciler(t *testing.T) { 279 now := func() metav1.Time { return metav1.Date(2018, time.January, 26, 20, 40, 0, 0, time.UTC) } 280 281 validConfigMap := validConfigMap() 282 validCatalogSource := validConfigMapCatalogSource(validConfigMap) 283 outdatedCatalogSource := validCatalogSource.DeepCopy() 284 outdatedCatalogSource.Status.ConfigMapResource.ResourceVersion = "old" 285 type cluster struct { 286 k8sObjs []runtime.Object 287 } 288 type in struct { 289 cluster cluster 290 catsrc *v1alpha1.CatalogSource 291 } 292 type out struct { 293 status *v1alpha1.RegistryServiceStatus 294 err error 295 } 296 tests := []struct { 297 testName string 298 in in 299 out out 300 }{ 301 { 302 testName: "NoConfigMap", 303 in: in{ 304 cluster: cluster{ 305 k8sObjs: []runtime.Object{ 306 validConfigMap, 307 defaultNamespace(), 308 }, 309 }, 310 catsrc: &v1alpha1.CatalogSource{ 311 ObjectMeta: metav1.ObjectMeta{ 312 Namespace: testNamespace, 313 }, 314 Spec: v1alpha1.CatalogSourceSpec{ 315 SourceType: v1alpha1.SourceTypeConfigmap, 316 ConfigMap: "test-cm", 317 }, 318 }, 319 }, 320 out: out{ 321 err: fmt.Errorf(`unable to find configmap testns/test-cm: configmaps "test-cm" not found`), 322 }, 323 }, 324 { 325 testName: "NoExistingRegistry/CreateSuccessful", 326 in: in{ 327 cluster: cluster{ 328 k8sObjs: []runtime.Object{ 329 validConfigMap, 330 defaultNamespace(), 331 }, 332 }, 333 catsrc: validCatalogSource, 334 }, 335 out: out{ 336 status: &v1alpha1.RegistryServiceStatus{ 337 CreatedAt: now(), 338 Protocol: "grpc", 339 ServiceName: "cool-catalog", 340 ServiceNamespace: testNamespace, 341 Port: "50051", 342 }, 343 }, 344 }, 345 { 346 testName: "ExistingRegistry/BadServiceAccount", 347 in: in{ 348 cluster: cluster{ 349 k8sObjs: append(modifyObjName(objectsForCatalogSource(t, validCatalogSource), &corev1.ServiceAccount{}, "badName"), validConfigMap), 350 }, 351 catsrc: validCatalogSource, 352 }, 353 out: out{ 354 status: &v1alpha1.RegistryServiceStatus{ 355 CreatedAt: now(), 356 Protocol: "grpc", 357 ServiceName: "cool-catalog", 358 ServiceNamespace: testNamespace, 359 Port: "50051", 360 }, 361 }, 362 }, 363 { 364 testName: "ExistingRegistry/BadService", 365 in: in{ 366 cluster: cluster{ 367 k8sObjs: append(modifyObjName(objectsForCatalogSource(t, validCatalogSource), &corev1.Service{}, "badName"), validConfigMap), 368 }, 369 catsrc: validCatalogSource, 370 }, 371 out: out{ 372 status: &v1alpha1.RegistryServiceStatus{ 373 CreatedAt: now(), 374 Protocol: "grpc", 375 ServiceName: "cool-catalog", 376 ServiceNamespace: testNamespace, 377 Port: "50051", 378 }, 379 }, 380 }, 381 { 382 testName: "ExistingRegistry/BadServiceWithWrongHash", 383 in: in{ 384 cluster: cluster{ 385 k8sObjs: append(setLabel(objectsForCatalogSource(t, validCatalogSource), &corev1.Service{}, ServiceHashLabelKey, "wrongHash"), validConfigMap), 386 }, 387 catsrc: validCatalogSource, 388 }, 389 out: out{ 390 status: &v1alpha1.RegistryServiceStatus{ 391 CreatedAt: now(), 392 Protocol: "grpc", 393 ServiceName: "cool-catalog", 394 ServiceNamespace: testNamespace, 395 Port: "50051", 396 }, 397 }, 398 }, 399 { 400 testName: "ExistingRegistry/BadPod", 401 in: in{ 402 cluster: cluster{ 403 k8sObjs: append(setLabel(objectsForCatalogSource(t, validCatalogSource), &corev1.Pod{}, CatalogSourceLabelKey, "badValue"), validConfigMap), 404 }, 405 catsrc: validCatalogSource, 406 }, 407 out: out{ 408 status: &v1alpha1.RegistryServiceStatus{ 409 CreatedAt: now(), 410 Protocol: "grpc", 411 ServiceName: "cool-catalog", 412 ServiceNamespace: testNamespace, 413 Port: "50051", 414 }, 415 }, 416 }, 417 { 418 testName: "ExistingRegistry/BadRole", 419 in: in{ 420 cluster: cluster{ 421 k8sObjs: append(modifyObjName(objectsForCatalogSource(t, validCatalogSource), &rbacv1.Role{}, "badName"), validConfigMap), 422 }, 423 catsrc: validCatalogSource, 424 }, 425 out: out{ 426 status: &v1alpha1.RegistryServiceStatus{ 427 CreatedAt: now(), 428 Protocol: "grpc", 429 ServiceName: "cool-catalog", 430 ServiceNamespace: testNamespace, 431 Port: "50051", 432 }, 433 }, 434 }, 435 { 436 testName: "ExistingRegistry/BadRoleBinding", 437 in: in{ 438 cluster: cluster{ 439 k8sObjs: append(modifyObjName(objectsForCatalogSource(t, validCatalogSource), &rbacv1.RoleBinding{}, "badName"), validConfigMap), 440 }, 441 catsrc: validCatalogSource, 442 }, 443 out: out{ 444 status: &v1alpha1.RegistryServiceStatus{ 445 CreatedAt: now(), 446 Protocol: "grpc", 447 ServiceName: "cool-catalog", 448 ServiceNamespace: testNamespace, 449 Port: "50051", 450 }, 451 }, 452 }, 453 { 454 testName: "ExistingRegistry/OldPod", 455 in: in{ 456 cluster: cluster{ 457 k8sObjs: append(objectsForCatalogSource(t, validCatalogSource), validConfigMap), 458 }, 459 catsrc: outdatedCatalogSource, 460 }, 461 out: out{ 462 status: &v1alpha1.RegistryServiceStatus{ 463 CreatedAt: now(), 464 Protocol: "grpc", 465 ServiceName: "cool-catalog", 466 ServiceNamespace: testNamespace, 467 Port: "50051", 468 }, 469 }, 470 }, 471 } 472 for _, tt := range tests { 473 t.Run(tt.testName, func(t *testing.T) { 474 stopc := make(chan struct{}) 475 defer close(stopc) 476 477 factory, client := fakeReconcilerFactory(t, stopc, withNow(now), withK8sObjs(tt.in.cluster.k8sObjs...), withK8sClientOptions(clientfake.WithNameGeneration(t))) 478 rec := factory.ReconcilerForSource(tt.in.catsrc) 479 480 err := rec.EnsureRegistryServer(logrus.NewEntry(logrus.New()), tt.in.catsrc) 481 482 if tt.out.err != nil { 483 require.EqualError(t, err, tt.out.err.Error()) 484 } else { 485 require.NoError(t, err) 486 } 487 require.Equal(t, tt.out.status, tt.in.catsrc.Status.RegistryServiceStatus) 488 489 if tt.out.err != nil { 490 return 491 } 492 493 // if no error, the reconciler should create the same set of kube objects every time 494 decorated := configMapCatalogSourceDecorator{tt.in.catsrc, runAsUser} 495 496 pod, err := decorated.Pod(registryImageName, defaultPodSecurityConfig) 497 require.NoError(t, err) 498 listOptions := metav1.ListOptions{LabelSelector: labels.SelectorFromSet(labels.Set{CatalogSourceLabelKey: tt.in.catsrc.GetName()}).String()} 499 outPods, err := client.KubernetesInterface().CoreV1().Pods(pod.GetNamespace()).List(context.TODO(), listOptions) 500 require.NoError(t, err) 501 require.Len(t, outPods.Items, 1) 502 outPod := outPods.Items[0] 503 require.Equal(t, pod.GetGenerateName(), outPod.GetGenerateName()) 504 require.Equal(t, pod.GetLabels(), outPod.GetLabels()) 505 require.Equal(t, pod.Spec, outPod.Spec) 506 507 service, err := decorated.Service() 508 require.NoError(t, err) 509 outService, err := client.KubernetesInterface().CoreV1().Services(service.GetNamespace()).Get(context.TODO(), service.GetName(), metav1.GetOptions{}) 510 require.NoError(t, err) 511 require.Equal(t, service, outService) 512 513 serviceAccount := decorated.ServiceAccount() 514 outServiceAccount, err := client.KubernetesInterface().CoreV1().ServiceAccounts(serviceAccount.GetNamespace()).Get(context.TODO(), serviceAccount.GetName(), metav1.GetOptions{}) 515 require.NoError(t, err) 516 require.Equal(t, serviceAccount, outServiceAccount) 517 518 role := decorated.Role() 519 outRole, err := client.KubernetesInterface().RbacV1().Roles(role.GetNamespace()).Get(context.TODO(), role.GetName(), metav1.GetOptions{}) 520 require.NoError(t, err) 521 require.Equal(t, role, outRole) 522 523 roleBinding := decorated.RoleBinding() 524 outRoleBinding, err := client.KubernetesInterface().RbacV1().RoleBindings(roleBinding.GetNamespace()).Get(context.TODO(), roleBinding.GetName(), metav1.GetOptions{}) 525 require.NoError(t, err) 526 require.Equal(t, roleBinding, outRoleBinding) 527 }) 528 } 529 } 530 531 func TestConfigMapRegistryChecker(t *testing.T) { 532 validConfigMap := validConfigMap() 533 validCatalogSource := validConfigMapCatalogSource(validConfigMap) 534 type cluster struct { 535 k8sObjs []runtime.Object 536 } 537 type in struct { 538 cluster cluster 539 catsrc *v1alpha1.CatalogSource 540 } 541 type out struct { 542 healthy bool 543 err error 544 } 545 tests := []struct { 546 testName string 547 in in 548 out out 549 }{ 550 { 551 testName: "ConfigMap/ExistingRegistry/DeadPod", 552 in: in{ 553 cluster: cluster{ 554 k8sObjs: append(withPodDeletedButNotRemoved(objectsForCatalogSource(t, validCatalogSource)), validConfigMap), 555 }, 556 catsrc: validCatalogSource, 557 }, 558 out: out{ 559 healthy: false, 560 }, 561 }, 562 } 563 for _, tt := range tests { 564 t.Run(tt.testName, func(t *testing.T) { 565 stopc := make(chan struct{}) 566 defer close(stopc) 567 568 factory, _ := fakeReconcilerFactory(t, stopc, withK8sObjs(tt.in.cluster.k8sObjs...)) 569 rec := factory.ReconcilerForSource(tt.in.catsrc) 570 571 healthy, err := rec.CheckRegistryServer(logrus.NewEntry(logrus.New()), tt.in.catsrc) 572 573 require.Equal(t, tt.out.err, err) 574 if tt.out.err != nil { 575 return 576 } 577 578 require.Equal(t, tt.out.healthy, healthy) 579 }) 580 } 581 }