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 := &registryReconcilerFactory{
   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  }