diff --git a/deploy/chart/templates/0000_30_05-catalogsource.crd.yaml b/deploy/chart/templates/0000_30_05-catalogsource.crd.yaml index e18f6be65b..c549ed5329 100644 --- a/deploy/chart/templates/0000_30_05-catalogsource.crd.yaml +++ b/deploy/chart/templates/0000_30_05-catalogsource.crd.yaml @@ -56,11 +56,16 @@ spec: enum: - internal # deprecated - configmap + - grpc configMap: type: string description: The name of a ConfigMap that holds the entries for an in-memory catalog. + image: + type: string + description: An image that serves a grpc registry. Only valid for `grpc` sourceType. + displayName: type: string description: Pretty name for display diff --git a/pkg/api/apis/operators/v1alpha1/catalogsource_types.go b/pkg/api/apis/operators/v1alpha1/catalogsource_types.go index 8f06410c40..6b316c6a35 100644 --- a/pkg/api/apis/operators/v1alpha1/catalogsource_types.go +++ b/pkg/api/apis/operators/v1alpha1/catalogsource_types.go @@ -20,11 +20,13 @@ type SourceType string const ( SourceTypeInternal SourceType = "internal" SourceTypeConfigmap SourceType = "configmap" + SourceTypeGrpc SourceType = "grpc" ) type CatalogSourceSpec struct { SourceType SourceType `json:"sourceType"` ConfigMap string `json:"configMap,omitempty"` + Image string `json:"image,omitempty"` Secrets []string `json:"secrets,omitempty"` // Metadata diff --git a/pkg/controller/operators/catalog/operator.go b/pkg/controller/operators/catalog/operator.go index f523921add..21d3f2059e 100644 --- a/pkg/controller/operators/catalog/operator.go +++ b/pkg/controller/operators/catalog/operator.go @@ -29,7 +29,7 @@ import ( "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions" olmerrors "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/errors" - "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" @@ -54,16 +54,16 @@ var timeNow = func() metav1.Time { return metav1.NewTime(time.Now().UTC()) } // resolving dependencies in a catalog. type Operator struct { *queueinformer.Operator - client versioned.Interface - lister operatorlister.OperatorLister - namespace string - sources map[resolver.CatalogKey]resolver.SourceRef - sourcesLock sync.RWMutex - sourcesLastUpdate metav1.Time - resolver resolver.Resolver - subQueue workqueue.RateLimitingInterface - catSrcQueue workqueue.RateLimitingInterface - configmapRegistryReconciler registry.RegistryReconciler + client versioned.Interface + lister operatorlister.OperatorLister + namespace string + sources map[resolver.CatalogKey]resolver.SourceRef + sourcesLock sync.RWMutex + sourcesLastUpdate metav1.Time + resolver resolver.Resolver + subQueue workqueue.RateLimitingInterface + catSrcQueue workqueue.RateLimitingInterface + reconciler reconciler.ReconcilerReconciler } // NewOperator creates a new Catalog Operator. @@ -207,10 +207,10 @@ func NewOperator(kubeconfigPath string, logger *logrus.Logger, wakeupInterval ti op.lister.CoreV1().RegisterPodLister(namespace, podInformer.Lister()) op.lister.CoreV1().RegisterConfigMapLister(namespace, configMapInformer.Lister()) } - op.configmapRegistryReconciler = ®istry.ConfigMapRegistryReconciler{ - Image: configmapRegistryImage, - OpClient: op.OpClient, - Lister: op.lister, + op.reconciler = &reconciler.RegistryReconcilerReconciler{ + ConfigMapServerImage: configmapRegistryImage, + OpClient: op.OpClient, + Lister: op.lister, } return op, nil } @@ -281,15 +281,29 @@ func (o *Operator) handleDeletion(obj interface{}) { } func (o *Operator) handleCatSrcDeletion(obj interface{}) { - if catsrc, ok := obj.(*v1alpha1.CatalogSource); ok { - sourceKey := resolver.CatalogKey{Name: catsrc.GetName(), Namespace: catsrc.GetNamespace()} - func() { - o.sourcesLock.Lock() - defer o.sourcesLock.Unlock() - delete(o.sources, sourceKey) - }() - o.Log.WithField("source", sourceKey).Info("removed client for deleted catalogsource") + catsrc, ok := obj.(metav1.Object) + if !ok { + if !ok { + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + utilruntime.HandleError(fmt.Errorf("Couldn't get object from tombstone %#v", obj)) + return + } + + catsrc, ok = tombstone.Obj.(metav1.Object) + if !ok { + utilruntime.HandleError(fmt.Errorf("Tombstone contained object that is not a Namespace %#v", obj)) + return + } + } } + sourceKey := resolver.CatalogKey{Name: catsrc.GetName(), Namespace: catsrc.GetNamespace()} + func() { + o.sourcesLock.Lock() + defer o.sourcesLock.Unlock() + delete(o.sources, sourceKey) + }() + o.Log.WithField("source", sourceKey).Info("removed client for deleted catalogsource") } func (o *Operator) syncCatalogSources(obj interface{}) (syncError error) { @@ -303,53 +317,45 @@ func (o *Operator) syncCatalogSources(obj interface{}) (syncError error) { "source": catsrc.GetName(), }) - if catsrc.Spec.SourceType == v1alpha1.SourceTypeInternal || catsrc.Spec.SourceType == v1alpha1.SourceTypeConfigmap { - return o.syncConfigMapSource(logger, catsrc) - } - - logger.WithField("sourceType", catsrc.Spec.SourceType).Warn("unknown source type") - - // TODO: write status about invalid source type - - return nil -} - -func (o *Operator) syncConfigMapSource(logger *logrus.Entry, catsrc *v1alpha1.CatalogSource) (syncError error) { - // Get the catalog source's config map - configMap, err := o.lister.CoreV1().ConfigMapLister().ConfigMaps(catsrc.GetNamespace()).Get(catsrc.Spec.ConfigMap) - if err != nil { - return fmt.Errorf("failed to get catalog config map %s: %s", catsrc.Spec.ConfigMap, err) - } - out := catsrc.DeepCopy() sourceKey := resolver.CatalogKey{Name: catsrc.GetName(), Namespace: catsrc.GetNamespace()} - if catsrc.Status.ConfigMapResource == nil || catsrc.Status.ConfigMapResource.UID != configMap.GetUID() || catsrc.Status.ConfigMapResource.ResourceVersion != configMap.GetResourceVersion() { - // configmap ref nonexistent or updated, write out the new configmap ref to status and exit - out.Status.ConfigMapResource = &v1alpha1.ConfigMapResourceReference{ - Name: configMap.GetName(), - Namespace: configMap.GetNamespace(), - UID: configMap.GetUID(), - ResourceVersion: configMap.GetResourceVersion(), + if catsrc.Spec.SourceType == v1alpha1.SourceTypeInternal || catsrc.Spec.SourceType == v1alpha1.SourceTypeConfigmap { + // Get the catalog source's config map + configMap, err := o.lister.CoreV1().ConfigMapLister().ConfigMaps(catsrc.GetNamespace()).Get(catsrc.Spec.ConfigMap) + if err != nil { + return fmt.Errorf("failed to get catalog config map %s: %s", catsrc.Spec.ConfigMap, err) } - out.Status.LastSync = timeNow() - // update status - if _, err = o.client.OperatorsV1alpha1().CatalogSources(out.GetNamespace()).UpdateStatus(out); err != nil { - return err - } + if catsrc.Status.ConfigMapResource == nil || catsrc.Status.ConfigMapResource.UID != configMap.GetUID() || catsrc.Status.ConfigMapResource.ResourceVersion != configMap.GetResourceVersion() { + // configmap ref nonexistent or updated, write out the new configmap ref to status and exit + out.Status.ConfigMapResource = &v1alpha1.ConfigMapResourceReference{ + Name: configMap.GetName(), + Namespace: configMap.GetNamespace(), + UID: configMap.GetUID(), + ResourceVersion: configMap.GetResourceVersion(), + } + out.Status.LastSync = timeNow() - o.sourcesLastUpdate = timeNow() + // update status + if _, err = o.client.OperatorsV1alpha1().CatalogSources(out.GetNamespace()).UpdateStatus(out); err != nil { + return err + } - return nil + o.sourcesLastUpdate = timeNow() + + return nil + } } - // configmap ref is up to date, continue parsing - if catsrc.Status.RegistryServiceStatus == nil || catsrc.Status.RegistryServiceStatus.CreatedAt.Before(&catsrc.Status.LastSync) { - // if registry pod hasn't been created or hasn't been updated since the last configmap update, recreate it + reconciler := o.reconciler.ReconcilerForSourceType(catsrc.Spec.SourceType) + if reconciler == nil { + return fmt.Errorf("no reconciler for source type %s", catsrc.Spec.SourceType) + } - out := catsrc.DeepCopy() - if err := o.configmapRegistryReconciler.EnsureRegistryServer(out); err != nil { + // if registry pod hasn't been created or hasn't been updated since the last configmap update, recreate it + if catsrc.Status.RegistryServiceStatus == nil || catsrc.Status.RegistryServiceStatus.CreatedAt.Before(&catsrc.Status.LastSync) { + if err := reconciler.EnsureRegistryServer(out); err != nil { logger.WithError(err).Warn("couldn't ensure registry server") return err } @@ -359,7 +365,7 @@ func (o *Operator) syncConfigMapSource(logger *logrus.Entry, catsrc *v1alpha1.Ca } // update status - if _, err = o.client.OperatorsV1alpha1().CatalogSources(out.GetNamespace()).UpdateStatus(out); err != nil { + if _, err := o.client.OperatorsV1alpha1().CatalogSources(out.GetNamespace()).UpdateStatus(out); err != nil { return err } @@ -368,6 +374,7 @@ func (o *Operator) syncConfigMapSource(logger *logrus.Entry, catsrc *v1alpha1.Ca return nil } + // update operator's view of sources sourcesUpdated := false func() { o.sourcesLock.Lock() @@ -389,13 +396,14 @@ func (o *Operator) syncConfigMapSource(logger *logrus.Entry, catsrc *v1alpha1.Ca } }() - if sourcesUpdated { - // record that we've done work here onto the status - out := catsrc.DeepCopy() - out.Status.LastSync = timeNow() - if _, err = o.client.OperatorsV1alpha1().CatalogSources(out.GetNamespace()).UpdateStatus(out); err != nil { - return err - } + if !sourcesUpdated { + return nil + } + + // record that we've done work here onto the status + out.Status.LastSync = timeNow() + if _, err := o.client.OperatorsV1alpha1().CatalogSources(out.GetNamespace()).UpdateStatus(out); err != nil { + return err } // Sync any dependent Subscriptions diff --git a/pkg/controller/operators/catalog/operator_test.go b/pkg/controller/operators/catalog/operator_test.go index 0d1d0cdf59..73aa3b4f64 100644 --- a/pkg/controller/operators/catalog/operator_test.go +++ b/pkg/controller/operators/catalog/operator_test.go @@ -24,7 +24,7 @@ import ( "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/fake" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions" olmerrors "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/errors" - "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver" "github.com/operator-framework/operator-lifecycle-manager/pkg/fakes" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" @@ -146,7 +146,7 @@ func TestSyncCatalogSources(t *testing.T) { Data: fakeConfigMapData(), }, expectedStatus: nil, - expectedError: nil, + expectedError: fmt.Errorf("no reconciler for source type nope"), }, { testName: "CatalogSourceWithBackingConfigMap", @@ -420,10 +420,10 @@ func NewFakeOperator(clientObjs []runtime.Object, k8sObjs []runtime.Object, extO resolver: &fakes.FakeResolver{}, } - op.configmapRegistryReconciler = ®istry.ConfigMapRegistryReconciler{ - Image: "test:pod", - OpClient: op.OpClient, - Lister: lister, + op.reconciler = &reconciler.RegistryReconcilerReconciler{ + ConfigMapServerImage: "test:pod", + OpClient: op.OpClient, + Lister: lister, } var hasSyncedCheckFns []cache.InformerSynced diff --git a/pkg/controller/operators/catalog/subscriptions_test.go b/pkg/controller/operators/catalog/subscriptions_test.go index ebc061a1f0..d8a0659b0b 100644 --- a/pkg/controller/operators/catalog/subscriptions_test.go +++ b/pkg/controller/operators/catalog/subscriptions_test.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver" "github.com/operator-framework/operator-lifecycle-manager/pkg/fakes" ) @@ -66,11 +67,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.ClusterServiceVersionKind, - Name: "csv.v.1", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.ClusterServiceVersionKind, + Name: "csv.v.1", + Manifest: "{}", }, }, }, @@ -150,11 +151,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.ClusterServiceVersionKind, - Name: "csv.v.1", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.ClusterServiceVersionKind, + Name: "csv.v.1", + Manifest: "{}", }, }, }, @@ -195,11 +196,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.ClusterServiceVersionKind, - Name: "csv.v.2", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.ClusterServiceVersionKind, + Name: "csv.v.2", + Manifest: "{}", }, }, }, @@ -279,11 +280,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.ClusterServiceVersionKind, - Name: "csv.v.2", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.ClusterServiceVersionKind, + Name: "csv.v.2", + Manifest: "{}", }, }, }, @@ -324,11 +325,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.ClusterServiceVersionKind, - Name: "csv.v.2", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.ClusterServiceVersionKind, + Name: "csv.v.2", + Manifest: "{}", }, }, { @@ -336,11 +337,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.ClusterServiceVersionKind, - Name: "dep.v.1", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.ClusterServiceVersionKind, + Name: "dep.v.1", + Manifest: "{}", }, }, { @@ -348,11 +349,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.SubscriptionKind, - Name: "sub-dep", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.SubscriptionKind, + Name: "sub-dep", + Manifest: "{}", }, }, }, @@ -433,11 +434,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.ClusterServiceVersionKind, - Name: "csv.v.2", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.ClusterServiceVersionKind, + Name: "csv.v.2", + Manifest: "{}", }, }, { @@ -445,11 +446,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.ClusterServiceVersionKind, - Name: "dep.v.1", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.ClusterServiceVersionKind, + Name: "dep.v.1", + Manifest: "{}", }, }, { @@ -457,11 +458,11 @@ func TestSyncSubscriptions(t *testing.T) { Resource: v1alpha1.StepResource{ CatalogSource: "src", CatalogSourceNamespace: testNamespace, - Group: v1alpha1.GroupName, - Version: v1alpha1.GroupVersion, - Kind: v1alpha1.SubscriptionKind, - Name: "sub-dep", - Manifest: "{}", + Group: v1alpha1.GroupName, + Version: v1alpha1.GroupVersion, + Kind: v1alpha1.SubscriptionKind, + Name: "sub-dep", + Manifest: "{}", }, }, }, @@ -477,11 +478,16 @@ func TestSyncSubscriptions(t *testing.T) { o, _, err := NewFakeOperator(tt.fields.existingOLMObjs, tt.fields.existingObjects, nil, nil, testNamespace, stopCh) require.NoError(t, err) - o.configmapRegistryReconciler = &fakes.FakeRegistryReconciler{ - EnsureRegistryServerStub: func(source *v1alpha1.CatalogSource) error { - return nil + o.reconciler = &fakes.FakeReconcilerReconciler{ + ReconcilerForSourceTypeStub: func(sourceType v1alpha1.SourceType) reconciler.RegistryReconciler { + return &fakes.FakeRegistryReconciler{ + EnsureRegistryServerStub: func(source *v1alpha1.CatalogSource) error { + return nil + }, + } }, } + o.sourcesLastUpdate = tt.fields.sourcesLastUpdate o.resolver = &fakes.FakeResolver{ ResolveStepsStub: func(string, resolver.SourceQuerier) ([]*v1alpha1.Step, []*v1alpha1.Subscription, error) { diff --git a/pkg/controller/registry/reconciler.go b/pkg/controller/registry/reconciler/configmap.go similarity index 73% rename from pkg/controller/registry/reconciler.go rename to pkg/controller/registry/reconciler/configmap.go index 67cad72d00..cacd572622 100644 --- a/pkg/controller/registry/reconciler.go +++ b/pkg/controller/registry/reconciler/configmap.go @@ -1,5 +1,5 @@ -//go:generate counterfeiter -o ../../fakes/fake_reconciler.go . RegistryReconciler -package registry +//go:generate counterfeiter -o ../../../fakes/fake_reconciler.go . RegistryReconciler +package reconciler import ( "fmt" @@ -20,33 +20,36 @@ import ( var timeNow = func() metav1.Time { return metav1.NewTime(time.Now().UTC()) } -// catalogSourceDecorator wraps CatalogSource to add additional methods -type catalogSourceDecorator struct { +// configMapCatalogSourceDecorator wraps CatalogSource to add additional methods +type configMapCatalogSourceDecorator struct { *v1alpha1.CatalogSource } -func (s *catalogSourceDecorator) serviceAccountName() string { +func (s *configMapCatalogSourceDecorator) serviceAccountName() string { return s.GetName() + "-configmap-server" } -func (s *catalogSourceDecorator) roleName() string { +func (s *configMapCatalogSourceDecorator) roleName() string { return s.GetName() + "-configmap-reader" } -func (s *catalogSourceDecorator) Selector() labels.Selector { +func (s *configMapCatalogSourceDecorator) Selector() labels.Selector { return labels.SelectorFromValidatedSet(map[string]string{ "olm.catalogSource": s.GetName(), }) } -func (s *catalogSourceDecorator) Labels() map[string]string { - return map[string]string{ - "olm.catalogSource": s.GetName(), - "olm.configMapResourceVersion": s.Status.ConfigMapResource.ResourceVersion, +func (s *configMapCatalogSourceDecorator) Labels() map[string]string { + labels := map[string]string{ + "olm.catalogSource": s.GetName(), + } + if s.Spec.SourceType == v1alpha1.SourceTypeInternal || s.Spec.SourceType == v1alpha1.SourceTypeConfigmap { + labels["olm.configMapResourceVersion"] = s.Status.ConfigMapResource.ResourceVersion } + return labels } -func (s *catalogSourceDecorator) ConfigMapChanges(configMap *v1.ConfigMap) bool { +func (s *configMapCatalogSourceDecorator) ConfigMapChanges(configMap *v1.ConfigMap) bool { if s.Status.ConfigMapResource == nil { return true } @@ -56,7 +59,7 @@ func (s *catalogSourceDecorator) ConfigMapChanges(configMap *v1.ConfigMap) bool return true } -func (s *catalogSourceDecorator) Service() *v1.Service { +func (s *configMapCatalogSourceDecorator) Service() *v1.Service { svc := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: s.GetName(), @@ -77,7 +80,7 @@ func (s *catalogSourceDecorator) Service() *v1.Service { return svc } -func (s *catalogSourceDecorator) Pod(image string) *v1.Pod { +func (s *configMapCatalogSourceDecorator) Pod(image string) *v1.Pod { pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ GenerateName: s.GetName() + "-", @@ -121,7 +124,7 @@ func (s *catalogSourceDecorator) Pod(image string) *v1.Pod { return pod } -func (s *catalogSourceDecorator) ServiceAccount() *v1.ServiceAccount { +func (s *configMapCatalogSourceDecorator) ServiceAccount() *v1.ServiceAccount { sa := &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: s.serviceAccountName(), @@ -132,7 +135,7 @@ func (s *catalogSourceDecorator) ServiceAccount() *v1.ServiceAccount { return sa } -func (s *catalogSourceDecorator) Role() *rbacv1.Role { +func (s *configMapCatalogSourceDecorator) Role() *rbacv1.Role { role := &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ Name: s.roleName(), @@ -151,7 +154,7 @@ func (s *catalogSourceDecorator) Role() *rbacv1.Role { return role } -func (s *catalogSourceDecorator) RoleBinding() *rbacv1.RoleBinding { +func (s *configMapCatalogSourceDecorator) RoleBinding() *rbacv1.RoleBinding { rb := &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: s.GetName() + "-server-configmap-reader", @@ -174,10 +177,6 @@ func (s *catalogSourceDecorator) RoleBinding() *rbacv1.RoleBinding { return rb } -type RegistryReconciler interface { - EnsureRegistryServer(catalogSource *v1alpha1.CatalogSource) error -} - type ConfigMapRegistryReconciler struct { Lister operatorlister.OperatorLister OpClient operatorclient.ClientInterface @@ -186,7 +185,7 @@ type ConfigMapRegistryReconciler struct { var _ RegistryReconciler = &ConfigMapRegistryReconciler{} -func (c *ConfigMapRegistryReconciler) currentService(source catalogSourceDecorator) *v1.Service { +func (c *ConfigMapRegistryReconciler) currentService(source configMapCatalogSourceDecorator) *v1.Service { serviceName := source.Service().GetName() service, err := c.Lister.CoreV1().ServiceLister().Services(source.GetNamespace()).Get(serviceName) if err != nil { @@ -196,7 +195,7 @@ func (c *ConfigMapRegistryReconciler) currentService(source catalogSourceDecorat return service } -func (c *ConfigMapRegistryReconciler) currentServiceAccount(source catalogSourceDecorator) *v1.ServiceAccount { +func (c *ConfigMapRegistryReconciler) currentServiceAccount(source configMapCatalogSourceDecorator) *v1.ServiceAccount { serviceAccountName := source.ServiceAccount().GetName() serviceAccount, err := c.Lister.CoreV1().ServiceAccountLister().ServiceAccounts(source.GetNamespace()).Get(serviceAccountName) if err != nil { @@ -206,7 +205,7 @@ func (c *ConfigMapRegistryReconciler) currentServiceAccount(source catalogSource return serviceAccount } -func (c *ConfigMapRegistryReconciler) currentRole(source catalogSourceDecorator) *rbacv1.Role { +func (c *ConfigMapRegistryReconciler) currentRole(source configMapCatalogSourceDecorator) *rbacv1.Role { roleName := source.Role().GetName() role, err := c.Lister.RbacV1().RoleLister().Roles(source.GetNamespace()).Get(roleName) if err != nil { @@ -216,7 +215,7 @@ func (c *ConfigMapRegistryReconciler) currentRole(source catalogSourceDecorator) return role } -func (c *ConfigMapRegistryReconciler) currentRoleBinding(source catalogSourceDecorator) *rbacv1.RoleBinding { +func (c *ConfigMapRegistryReconciler) currentRoleBinding(source configMapCatalogSourceDecorator) *rbacv1.RoleBinding { roleBindingName := source.RoleBinding().GetName() roleBinding, err := c.Lister.RbacV1().RoleBindingLister().RoleBindings(source.GetNamespace()).Get(roleBindingName) if err != nil { @@ -226,7 +225,7 @@ func (c *ConfigMapRegistryReconciler) currentRoleBinding(source catalogSourceDec return roleBinding } -func (c *ConfigMapRegistryReconciler) currentPods(source catalogSourceDecorator, image string) []*v1.Pod { +func (c *ConfigMapRegistryReconciler) currentPods(source configMapCatalogSourceDecorator, image string) []*v1.Pod { podName := source.Pod(image).GetName() pods, err := c.Lister.CoreV1().PodLister().Pods(source.GetNamespace()).List(source.Selector()) if err != nil { @@ -239,7 +238,7 @@ func (c *ConfigMapRegistryReconciler) currentPods(source catalogSourceDecorator, return pods } -func (c *ConfigMapRegistryReconciler) currentPodsWithCorrectResourceVersion(source catalogSourceDecorator, image string) []*v1.Pod { +func (c *ConfigMapRegistryReconciler) currentPodsWithCorrectResourceVersion(source configMapCatalogSourceDecorator, image string) []*v1.Pod { podName := source.Pod(image).GetName() pods, err := c.Lister.CoreV1().PodLister().Pods(source.GetNamespace()).List(labels.SelectorFromValidatedSet(source.Labels())) if err != nil { @@ -254,29 +253,44 @@ func (c *ConfigMapRegistryReconciler) currentPodsWithCorrectResourceVersion(sour // Ensure that all components of registry server are up to date. func (c *ConfigMapRegistryReconciler) EnsureRegistryServer(catalogSource *v1alpha1.CatalogSource) error { - source := catalogSourceDecorator{catalogSource} + source := configMapCatalogSourceDecorator{catalogSource} - // fetch configmap first, exit early if we can't find it - configMap, err := c.Lister.CoreV1().ConfigMapLister().ConfigMaps(source.GetNamespace()).Get(source.Spec.ConfigMap) - if err != nil { - return fmt.Errorf("unable to get configmap %s/%s from cache", source.GetNamespace(), source.Spec.ConfigMap) + image := c.Image + if source.Spec.SourceType == "grpc" { + image = source.Spec.Image } - - if source.ConfigMapChanges(configMap) { - catalogSource.Status.ConfigMapResource = &v1alpha1.ConfigMapResourceReference{ - Name: configMap.GetName(), - Namespace: configMap.GetNamespace(), - UID: configMap.GetUID(), - ResourceVersion: configMap.GetResourceVersion(), - } + if image == "" { + return fmt.Errorf("no image for registry") } // if service status is nil, we force create every object to ensure they're created the first time overwrite := source.Status.RegistryServiceStatus == nil + overwritePod := overwrite - // recreate the pod if there are configmap changes; this causes the db to be rebuilt - // recreate the pod if no existing pod is serving the latest configmap - overwritePod := overwrite || source.ConfigMapChanges(configMap) || len(c.currentPodsWithCorrectResourceVersion(source, c.Image)) == 0 + if source.Spec.SourceType == v1alpha1.SourceTypeConfigmap || source.Spec.SourceType == v1alpha1.SourceTypeInternal { + // fetch configmap first, exit early if we can't find it + configMap, err := c.Lister.CoreV1().ConfigMapLister().ConfigMaps(source.GetNamespace()).Get(source.Spec.ConfigMap) + if err != nil { + return fmt.Errorf("unable to get configmap %s/%s from cache", source.GetNamespace(), source.Spec.ConfigMap) + } + + if source.ConfigMapChanges(configMap) { + catalogSource.Status.ConfigMapResource = &v1alpha1.ConfigMapResourceReference{ + Name: configMap.GetName(), + Namespace: configMap.GetNamespace(), + UID: configMap.GetUID(), + ResourceVersion: configMap.GetResourceVersion(), + } + + // recreate the pod if there are configmap changes; this causes the db to be rebuilt + overwritePod = true + } + + // recreate the pod if no existing pod is serving the latest image + if len(c.currentPodsWithCorrectResourceVersion(source, image)) == 0 { + overwritePod = true + } + } //TODO: if any of these error out, we should write a status back (possibly set RegistryServiceStatus to nil so they get recreated) if err := c.ensureServiceAccount(source, overwrite); err != nil { @@ -289,7 +303,7 @@ func (c *ConfigMapRegistryReconciler) EnsureRegistryServer(catalogSource *v1alph return errors.Wrapf(err, "error ensuring rolebinding: %s", source.RoleBinding().GetName()) } if err := c.ensurePod(source, overwritePod); err != nil { - return errors.Wrapf(err, "error ensuring pod: %s", source.Pod(c.Image).GetName()) + return errors.Wrapf(err, "error ensuring pod: %s", source.Pod(image).GetName()) } if err := c.ensureService(source, overwrite); err != nil { return errors.Wrapf(err, "error ensuring service: %s", source.Service().GetName()) @@ -308,7 +322,7 @@ func (c *ConfigMapRegistryReconciler) EnsureRegistryServer(catalogSource *v1alph return nil } -func (c *ConfigMapRegistryReconciler) ensureServiceAccount(source catalogSourceDecorator, overwrite bool) error { +func (c *ConfigMapRegistryReconciler) ensureServiceAccount(source configMapCatalogSourceDecorator, overwrite bool) error { serviceAccount := source.ServiceAccount() if c.currentServiceAccount(source) != nil { if !overwrite { @@ -322,7 +336,7 @@ func (c *ConfigMapRegistryReconciler) ensureServiceAccount(source catalogSourceD return err } -func (c *ConfigMapRegistryReconciler) ensureRole(source catalogSourceDecorator, overwrite bool) error { +func (c *ConfigMapRegistryReconciler) ensureRole(source configMapCatalogSourceDecorator, overwrite bool) error { role := source.Role() if c.currentRole(source) != nil { if !overwrite { @@ -336,7 +350,7 @@ func (c *ConfigMapRegistryReconciler) ensureRole(source catalogSourceDecorator, return err } -func (c *ConfigMapRegistryReconciler) ensureRoleBinding(source catalogSourceDecorator, overwrite bool) error { +func (c *ConfigMapRegistryReconciler) ensureRoleBinding(source configMapCatalogSourceDecorator, overwrite bool) error { roleBinding := source.RoleBinding() if c.currentRoleBinding(source) != nil { if !overwrite { @@ -350,7 +364,7 @@ func (c *ConfigMapRegistryReconciler) ensureRoleBinding(source catalogSourceDeco return err } -func (c *ConfigMapRegistryReconciler) ensurePod(source catalogSourceDecorator, overwrite bool) error { +func (c *ConfigMapRegistryReconciler) ensurePod(source configMapCatalogSourceDecorator, overwrite bool) error { pod := source.Pod(c.Image) currentPods := c.currentPods(source, c.Image) if len(currentPods) > 0 { @@ -370,7 +384,7 @@ func (c *ConfigMapRegistryReconciler) ensurePod(source catalogSourceDecorator, o return errors.Wrapf(err, "error creating new pod: %s", pod.GetGenerateName()) } -func (c *ConfigMapRegistryReconciler) ensureService(source catalogSourceDecorator, overwrite bool) error { +func (c *ConfigMapRegistryReconciler) ensureService(source configMapCatalogSourceDecorator, overwrite bool) error { service := source.Service() if c.currentService(source) != nil { if !overwrite { diff --git a/pkg/controller/registry/reconciler_test.go b/pkg/controller/registry/reconciler/configmap_test.go similarity index 92% rename from pkg/controller/registry/reconciler_test.go rename to pkg/controller/registry/reconciler/configmap_test.go index 91de074753..3b78e07d52 100644 --- a/pkg/controller/registry/reconciler_test.go +++ b/pkg/controller/registry/reconciler/configmap_test.go @@ -1,4 +1,4 @@ -package registry +package reconciler import ( "fmt" @@ -7,6 +7,7 @@ import ( "github.com/ghodss/yaml" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" "github.com/stretchr/testify/require" @@ -25,7 +26,7 @@ const ( testNamespace = "testns" ) -func reconciler(t *testing.T, k8sObjs []runtime.Object, stopc <-chan struct{}) (*ConfigMapRegistryReconciler, operatorclient.ClientInterface) { +func cmReconciler(t *testing.T, k8sObjs []runtime.Object, stopc <-chan struct{}) (*ConfigMapRegistryReconciler, operatorclient.ClientInterface) { opClientFake := operatorclient.NewClient(k8sfake.NewSimpleClientset(k8sObjs...), nil, nil) // Creates registry pods in response to configmaps @@ -110,10 +111,10 @@ func validConfigMap() *corev1.ConfigMap { func TestValidConfigMap(t *testing.T) { cm := validConfigMap() require.NotNil(t, cm) - require.Contains(t, cm.Data[ConfigMapCRDName], "fake") + require.Contains(t, cm.Data[registry.ConfigMapCRDName], "fake") } -func validCatalogSource(configMap *corev1.ConfigMap) *v1alpha1.CatalogSource { +func validConfigMapCatalogSource(configMap *corev1.ConfigMap) *v1alpha1.CatalogSource { return &v1alpha1.CatalogSource{ ObjectMeta: metav1.ObjectMeta{ Name: "cool-catalog", @@ -122,7 +123,7 @@ func validCatalogSource(configMap *corev1.ConfigMap) *v1alpha1.CatalogSource { }, Spec: v1alpha1.CatalogSourceSpec{ ConfigMap: "cool-configmap", - SourceType: "nope", + SourceType: v1alpha1.SourceTypeConfigmap, }, Status: v1alpha1.CatalogSourceStatus{ ConfigMapResource: &v1alpha1.ConfigMapResourceReference{ @@ -136,7 +137,7 @@ func validCatalogSource(configMap *corev1.ConfigMap) *v1alpha1.CatalogSource { } func objectsForCatalogSource(catsrc *v1alpha1.CatalogSource) []runtime.Object { - decorated := catalogSourceDecorator{catsrc} + decorated := configMapCatalogSourceDecorator{catsrc} objs := []runtime.Object{ decorated.Pod(registryImageName), decorated.Service(), @@ -179,7 +180,7 @@ func TestConfigMapRegistryReconciler(t *testing.T) { timeNow = func() metav1.Time { return nowTime } validConfigMap := validConfigMap() - validCatalogSource := validCatalogSource(validConfigMap) + validCatalogSource := validConfigMapCatalogSource(validConfigMap) outdatedCatalogSource := validCatalogSource.DeepCopy() outdatedCatalogSource.Status.ConfigMapResource.ResourceVersion = "old" type cluster struct { @@ -202,7 +203,11 @@ func TestConfigMapRegistryReconciler(t *testing.T) { testName: "NoConfigMap", in: in{ cluster: cluster{}, - catsrc: &v1alpha1.CatalogSource{}, + catsrc: &v1alpha1.CatalogSource{ + Spec: v1alpha1.CatalogSourceSpec{ + SourceType: v1alpha1.SourceTypeConfigmap, + }, + }, }, out: out{ err: fmt.Errorf("unable to get configmap / from cache"), @@ -340,7 +345,7 @@ func TestConfigMapRegistryReconciler(t *testing.T) { stopc := make(chan struct{}) defer close(stopc) - rec, client := reconciler(t, tt.in.cluster.k8sObjs, stopc) + rec, client := cmReconciler(t, tt.in.cluster.k8sObjs, stopc) err := rec.EnsureRegistryServer(tt.in.catsrc) @@ -352,7 +357,7 @@ func TestConfigMapRegistryReconciler(t *testing.T) { } // if no error, the reconciler should create the same set of kube objects every time - decorated := catalogSourceDecorator{tt.in.catsrc} + decorated := configMapCatalogSourceDecorator{tt.in.catsrc} pod := decorated.Pod(registryImageName) outPod, err := client.KubernetesInterface().CoreV1().Pods(pod.GetNamespace()).Get(pod.GetName(), metav1.GetOptions{}) diff --git a/pkg/controller/registry/reconciler/grpc.go b/pkg/controller/registry/reconciler/grpc.go new file mode 100644 index 0000000000..4c43f9677f --- /dev/null +++ b/pkg/controller/registry/reconciler/grpc.go @@ -0,0 +1,203 @@ +package reconciler + +import ( + "fmt" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// grpcCatalogSourceDecorator wraps CatalogSource to add additional methods +type grpcCatalogSourceDecorator struct { + *v1alpha1.CatalogSource +} + +func (s *grpcCatalogSourceDecorator) Selector() labels.Selector { + return labels.SelectorFromValidatedSet(map[string]string{ + "olm.catalogSource": s.GetName(), + }) +} + +func (s *grpcCatalogSourceDecorator) Labels() map[string]string { + return map[string]string{ + "olm.catalogSource": s.GetName(), + } +} + +func (s *grpcCatalogSourceDecorator) Service() *v1.Service { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.GetName(), + Namespace: s.GetNamespace(), + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "grpc", + Port: 50051, + TargetPort: intstr.FromInt(50051), + }, + }, + Selector: s.Labels(), + }, + } + ownerutil.AddOwner(svc, s.CatalogSource, false, false) + return svc +} + +func (s *grpcCatalogSourceDecorator) Pod() *v1.Pod { + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: s.GetName() + "-", + Namespace: s.GetNamespace(), + Labels: s.Labels(), + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "registry-server", + Image: s.Spec.Image, + Ports: []v1.ContainerPort{ + { + Name: "grpc", + ContainerPort: 50051, + }, + }, + ReadinessProbe: &v1.Probe{ + Handler: v1.Handler{ + Exec: &v1.ExecAction{ + Command: []string{"grpc_health_probe", "-addr=localhost:50051"}, + }, + }, + InitialDelaySeconds: 5, + }, + LivenessProbe: &v1.Probe{ + Handler: v1.Handler{ + Exec: &v1.ExecAction{ + Command: []string{"grpc_health_probe", "-addr=localhost:50051"}, + }, + }, + InitialDelaySeconds: 10, + }, + }, + }, + }, + } + ownerutil.AddOwner(pod, s.CatalogSource, false, false) + return pod +} + +type GrpcRegistryReconciler struct { + Lister operatorlister.OperatorLister + OpClient operatorclient.ClientInterface +} + +var _ RegistryReconciler = &GrpcRegistryReconciler{} + +func (c *GrpcRegistryReconciler) currentService(source grpcCatalogSourceDecorator) *v1.Service { + serviceName := source.Service().GetName() + service, err := c.Lister.CoreV1().ServiceLister().Services(source.GetNamespace()).Get(serviceName) + if err != nil { + logrus.WithField("service", serviceName).Warn("couldn't find service in cache") + return nil + } + return service +} + +func (c *GrpcRegistryReconciler) currentPods(source grpcCatalogSourceDecorator) []*v1.Pod { + pods, err := c.Lister.CoreV1().PodLister().Pods(source.GetNamespace()).List(source.Selector()) + if err != nil { + logrus.WithError(err).Warn("couldn't find pod in cache") + return nil + } + if len(pods) > 1 { + logrus.WithField("selector", source.Selector()).Warn("multiple pods found for selector") + } + return pods +} + +func (c *GrpcRegistryReconciler) currentPodsWithCorrectImage(source grpcCatalogSourceDecorator) []*v1.Pod { + pods, err := c.Lister.CoreV1().PodLister().Pods(source.GetNamespace()).List(labels.SelectorFromValidatedSet(source.Labels())) + if err != nil { + logrus.WithError(err).Warn("couldn't find pod in cache") + return nil + } + found := []*v1.Pod{} + for _, p := range pods { + if p.Spec.Containers[0].Image == source.Spec.Image { + found = append(found, p) + } + } + return found +} + +// Ensure that all components of registry server are up to date. +func (c *GrpcRegistryReconciler) EnsureRegistryServer(catalogSource *v1alpha1.CatalogSource) error { + source := grpcCatalogSourceDecorator{catalogSource} + + // if service status is nil, we force create every object to ensure they're created the first time + overwrite := source.Status.RegistryServiceStatus == nil + // recreate the pod if no existing pod is serving the latest image + overwritePod := overwrite || len(c.currentPodsWithCorrectImage(source)) == 0 + + //TODO: if any of these error out, we should write a status back (possibly set RegistryServiceStatus to nil so they get recreated) + if err := c.ensurePod(source, overwritePod); err != nil { + return errors.Wrapf(err, "error ensuring pod: %s", source.Pod().GetName()) + } + if err := c.ensureService(source, overwrite); err != nil { + return errors.Wrapf(err, "error ensuring service: %s", source.Service().GetName()) + } + + if overwritePod { + catalogSource.Status.RegistryServiceStatus = &v1alpha1.RegistryServiceStatus{ + CreatedAt: timeNow(), + Protocol: "grpc", + ServiceName: source.Service().GetName(), + ServiceNamespace: source.GetNamespace(), + Port: fmt.Sprintf("%d", source.Service().Spec.Ports[0].Port), + } + catalogSource.Status.LastSync = timeNow() + } + return nil +} + +func (c *GrpcRegistryReconciler) ensurePod(source grpcCatalogSourceDecorator, overwrite bool) error { + currentPods := c.currentPods(source) + if len(currentPods) > 0 { + if !overwrite { + return nil + } + for _, p := range currentPods { + if err := c.OpClient.KubernetesInterface().CoreV1().Pods(source.GetNamespace()).Delete(p.GetName(), metav1.NewDeleteOptions(0)); err != nil { + return errors.Wrapf(err, "error deleting old pod: %s", p.GetName()) + } + } + } + _, err := c.OpClient.KubernetesInterface().CoreV1().Pods(source.GetNamespace()).Create(source.Pod()) + if err == nil { + return nil + } + return errors.Wrapf(err, "error creating new pod: %s", source.Pod().GetGenerateName()) +} + +func (c *GrpcRegistryReconciler) ensureService(source grpcCatalogSourceDecorator, overwrite bool) error { + service := source.Service() + if c.currentService(source) != nil { + if !overwrite { + return nil + } + if err := c.OpClient.DeleteService(service.GetNamespace(), service.GetName(), metav1.NewDeleteOptions(0)); err != nil { + return err + } + } + _, err := c.OpClient.CreateService(service) + return err +} diff --git a/pkg/controller/registry/reconciler/grpc_test.go b/pkg/controller/registry/reconciler/grpc_test.go new file mode 100644 index 0000000000..b6ea2f47ad --- /dev/null +++ b/pkg/controller/registry/reconciler/grpc_test.go @@ -0,0 +1,257 @@ +package reconciler + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/informers" + k8sfake "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/tools/cache" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" +) + +func grpcReconciler(t *testing.T, k8sObjs []runtime.Object, stopc <-chan struct{}) (*GrpcRegistryReconciler, operatorclient.ClientInterface) { + opClientFake := operatorclient.NewClient(k8sfake.NewSimpleClientset(k8sObjs...), nil, nil) + + // Creates registry pods in response to configmaps + informerFactory := informers.NewSharedInformerFactory(opClientFake.KubernetesInterface(), 5*time.Second) + roleInformer := informerFactory.Rbac().V1().Roles() + roleBindingInformer := informerFactory.Rbac().V1().RoleBindings() + serviceAccountInformer := informerFactory.Core().V1().ServiceAccounts() + serviceInformer := informerFactory.Core().V1().Services() + podInformer := informerFactory.Core().V1().Pods() + configMapInformer := informerFactory.Core().V1().ConfigMaps() + + registryInformers := []cache.SharedIndexInformer{ + roleInformer.Informer(), + roleBindingInformer.Informer(), + serviceAccountInformer.Informer(), + serviceInformer.Informer(), + podInformer.Informer(), + configMapInformer.Informer(), + } + + lister := operatorlister.NewLister() + lister.RbacV1().RegisterRoleLister(testNamespace, roleInformer.Lister()) + lister.RbacV1().RegisterRoleBindingLister(testNamespace, roleBindingInformer.Lister()) + lister.CoreV1().RegisterServiceAccountLister(testNamespace, serviceAccountInformer.Lister()) + lister.CoreV1().RegisterServiceLister(testNamespace, serviceInformer.Lister()) + lister.CoreV1().RegisterPodLister(testNamespace, podInformer.Lister()) + lister.CoreV1().RegisterConfigMapLister(testNamespace, configMapInformer.Lister()) + + rec := &GrpcRegistryReconciler{ + OpClient: opClientFake, + Lister: lister, + } + + var hasSyncedCheckFns []cache.InformerSynced + for _, informer := range registryInformers { + hasSyncedCheckFns = append(hasSyncedCheckFns, informer.HasSynced) + go informer.Run(stopc) + } + + require.True(t, cache.WaitForCacheSync(stopc, hasSyncedCheckFns...), "caches failed to sync") + + return rec, opClientFake +} + +func validGrpcCatalogSource(image string) *v1alpha1.CatalogSource { + return &v1alpha1.CatalogSource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "img-catalog", + Namespace: testNamespace, + UID: types.UID("catalog-uid"), + }, + Spec: v1alpha1.CatalogSourceSpec{ + Image: image, + SourceType: v1alpha1.SourceTypeGrpc, + }, + } +} + +func TestGrpcRegistryReconciler(t *testing.T) { + nowTime := metav1.Date(2018, time.January, 26, 20, 40, 0, 0, time.UTC) + timeNow = func() metav1.Time { return nowTime } + + validConfigMap := validConfigMap() + validCatalogSource := validConfigMapCatalogSource(validConfigMap) + outdatedCatalogSource := validCatalogSource.DeepCopy() + outdatedCatalogSource.Status.ConfigMapResource.ResourceVersion = "old" + type cluster struct { + k8sObjs []runtime.Object + } + type in struct { + cluster cluster + catsrc *v1alpha1.CatalogSource + } + type out struct { + status *v1alpha1.RegistryServiceStatus + err error + } + tests := []struct { + testName string + in in + out out + }{ + { + testName: "Grpc/NoExistingRegistry/CreateSuccessful", + in: in{ + catsrc: validGrpcCatalogSource("test-img"), + }, + out: out{ + status: &v1alpha1.RegistryServiceStatus{ + CreatedAt: timeNow(), + Protocol: "grpc", + ServiceName: "img-catalog", + ServiceNamespace: testNamespace, + Port: "50051", + }, + }, + }, + { + testName: "Grpc/ExistingRegistry/BadServiceAccount", + in: in{ + cluster: cluster{ + k8sObjs: modifyObjName(objectsForCatalogSource(validGrpcCatalogSource("test-img")), "ServiceAccount", "badName"), + }, + catsrc: validGrpcCatalogSource("test-img"), + }, + out: out{ + status: &v1alpha1.RegistryServiceStatus{ + CreatedAt: timeNow(), + Protocol: "grpc", + ServiceName: "img-catalog", + ServiceNamespace: testNamespace, + Port: "50051", + }, + }, + }, + { + testName: "Grpc/ExistingRegistry/BadService", + in: in{ + cluster: cluster{ + k8sObjs: modifyObjName(objectsForCatalogSource(validGrpcCatalogSource("test-img")), "Service", "badName"), + }, + catsrc: validGrpcCatalogSource("test-img"), + }, + out: out{ + status: &v1alpha1.RegistryServiceStatus{ + CreatedAt: timeNow(), + Protocol: "grpc", + ServiceName: "img-catalog", + ServiceNamespace: testNamespace, + Port: "50051", + }, + }, + }, + { + testName: "Grpc/ExistingRegistry/BadPod", + in: in{ + cluster: cluster{ + k8sObjs: modifyObjName(objectsForCatalogSource(validGrpcCatalogSource("test-img")), "Pod", "badName"), + }, + catsrc: validGrpcCatalogSource("test-img"), + }, + out: out{ + status: &v1alpha1.RegistryServiceStatus{ + CreatedAt: timeNow(), + Protocol: "grpc", + ServiceName: "img-catalog", + ServiceNamespace: testNamespace, + Port: "50051", + }, + }, + }, + { + testName: "Grpc/ExistingRegistry/BadRole", + in: in{ + cluster: cluster{ + k8sObjs: modifyObjName(objectsForCatalogSource(validGrpcCatalogSource("test-img")), "Role", "badName"), + }, + catsrc: validGrpcCatalogSource("test-img"), + }, + out: out{ + status: &v1alpha1.RegistryServiceStatus{ + CreatedAt: timeNow(), + Protocol: "grpc", + ServiceName: "img-catalog", + ServiceNamespace: testNamespace, + Port: "50051", + }, + }, + }, + { + testName: "Grpc/ExistingRegistry/BadRoleBinding", + in: in{ + cluster: cluster{ + k8sObjs: modifyObjName(objectsForCatalogSource(validGrpcCatalogSource("test-img")), "RoleBinding", "badName"), + }, + catsrc: validGrpcCatalogSource("test-img"), + }, + out: out{ + status: &v1alpha1.RegistryServiceStatus{ + CreatedAt: timeNow(), + Protocol: "grpc", + ServiceName: "img-catalog", + ServiceNamespace: testNamespace, + Port: "50051", + }, + }, + }, + { + testName: "Grpc/ExistingRegistry/OldPod", + in: in{ + cluster: cluster{ + k8sObjs: objectsForCatalogSource(validGrpcCatalogSource("old-img")), + }, + catsrc: validGrpcCatalogSource("new-img"), + }, + out: out{ + status: &v1alpha1.RegistryServiceStatus{ + CreatedAt: timeNow(), + Protocol: "grpc", + ServiceName: "img-catalog", + ServiceNamespace: testNamespace, + Port: "50051", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + stopc := make(chan struct{}) + defer close(stopc) + + rec, client := grpcReconciler(t, tt.in.cluster.k8sObjs, stopc) + + err := rec.EnsureRegistryServer(tt.in.catsrc) + + require.Equal(t, tt.out.err, err) + require.Equal(t, tt.out.status, tt.in.catsrc.Status.RegistryServiceStatus) + + if tt.out.err != nil { + return + } + + // if no error, the reconciler should create the same set of kube objects every time + decorated := grpcCatalogSourceDecorator{tt.in.catsrc} + + pod := decorated.Pod() + outPod, err := client.KubernetesInterface().CoreV1().Pods(pod.GetNamespace()).Get(pod.GetName(), metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, pod, outPod) + + service := decorated.Service() + outService, err := client.KubernetesInterface().CoreV1().Services(service.GetNamespace()).Get(service.GetName(), metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, service, outService) + }) + } +} diff --git a/pkg/controller/registry/reconciler/reconciler.go b/pkg/controller/registry/reconciler/reconciler.go new file mode 100644 index 0000000000..b42f9971e4 --- /dev/null +++ b/pkg/controller/registry/reconciler/reconciler.go @@ -0,0 +1,39 @@ +//go:generate counterfeiter -o ../../../fakes/fake_reconciler_reconciler.go . ReconcilerReconciler +package reconciler + +import ( + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorlister" +) + +type RegistryReconciler interface { + EnsureRegistryServer(catalogSource *v1alpha1.CatalogSource) error +} + +type ReconcilerReconciler interface { + ReconcilerForSourceType(sourceType v1alpha1.SourceType) RegistryReconciler +} + +type RegistryReconcilerReconciler struct { + Lister operatorlister.OperatorLister + OpClient operatorclient.ClientInterface + ConfigMapServerImage string +} + +func (r *RegistryReconcilerReconciler) ReconcilerForSourceType(sourceType v1alpha1.SourceType) RegistryReconciler { + if sourceType == v1alpha1.SourceTypeInternal || sourceType == v1alpha1.SourceTypeConfigmap { + return &ConfigMapRegistryReconciler{ + Lister: r.Lister, + OpClient: r.OpClient, + Image: r.ConfigMapServerImage, + } + } + if sourceType == v1alpha1.SourceTypeGrpc { + return &GrpcRegistryReconciler{ + Lister: r.Lister, + OpClient: r.OpClient, + } + } + return nil +} diff --git a/pkg/fakes/fake_reconciler.go b/pkg/fakes/fake_reconciler.go index bf910f1732..d8c890723c 100644 --- a/pkg/fakes/fake_reconciler.go +++ b/pkg/fakes/fake_reconciler.go @@ -5,7 +5,7 @@ import ( sync "sync" v1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" - registry "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry" + reconciler "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler" ) type FakeRegistryReconciler struct { @@ -108,4 +108,4 @@ func (fake *FakeRegistryReconciler) recordInvocation(key string, args []interfac fake.invocations[key] = append(fake.invocations[key], args) } -var _ registry.RegistryReconciler = new(FakeRegistryReconciler) +var _ reconciler.RegistryReconciler = new(FakeRegistryReconciler) diff --git a/pkg/fakes/fake_reconciler_reconciler.go b/pkg/fakes/fake_reconciler_reconciler.go new file mode 100644 index 0000000000..7cf95f91e7 --- /dev/null +++ b/pkg/fakes/fake_reconciler_reconciler.go @@ -0,0 +1,111 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + sync "sync" + + v1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + reconciler "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler" +) + +type FakeReconcilerReconciler struct { + ReconcilerForSourceTypeStub func(v1alpha1.SourceType) reconciler.RegistryReconciler + reconcilerForSourceTypeMutex sync.RWMutex + reconcilerForSourceTypeArgsForCall []struct { + arg1 v1alpha1.SourceType + } + reconcilerForSourceTypeReturns struct { + result1 reconciler.RegistryReconciler + } + reconcilerForSourceTypeReturnsOnCall map[int]struct { + result1 reconciler.RegistryReconciler + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeReconcilerReconciler) ReconcilerForSourceType(arg1 v1alpha1.SourceType) reconciler.RegistryReconciler { + fake.reconcilerForSourceTypeMutex.Lock() + ret, specificReturn := fake.reconcilerForSourceTypeReturnsOnCall[len(fake.reconcilerForSourceTypeArgsForCall)] + fake.reconcilerForSourceTypeArgsForCall = append(fake.reconcilerForSourceTypeArgsForCall, struct { + arg1 v1alpha1.SourceType + }{arg1}) + fake.recordInvocation("ReconcilerForSourceType", []interface{}{arg1}) + fake.reconcilerForSourceTypeMutex.Unlock() + if fake.ReconcilerForSourceTypeStub != nil { + return fake.ReconcilerForSourceTypeStub(arg1) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.reconcilerForSourceTypeReturns + return fakeReturns.result1 +} + +func (fake *FakeReconcilerReconciler) ReconcilerForSourceTypeCallCount() int { + fake.reconcilerForSourceTypeMutex.RLock() + defer fake.reconcilerForSourceTypeMutex.RUnlock() + return len(fake.reconcilerForSourceTypeArgsForCall) +} + +func (fake *FakeReconcilerReconciler) ReconcilerForSourceTypeCalls(stub func(v1alpha1.SourceType) reconciler.RegistryReconciler) { + fake.reconcilerForSourceTypeMutex.Lock() + defer fake.reconcilerForSourceTypeMutex.Unlock() + fake.ReconcilerForSourceTypeStub = stub +} + +func (fake *FakeReconcilerReconciler) ReconcilerForSourceTypeArgsForCall(i int) v1alpha1.SourceType { + fake.reconcilerForSourceTypeMutex.RLock() + defer fake.reconcilerForSourceTypeMutex.RUnlock() + argsForCall := fake.reconcilerForSourceTypeArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeReconcilerReconciler) ReconcilerForSourceTypeReturns(result1 reconciler.RegistryReconciler) { + fake.reconcilerForSourceTypeMutex.Lock() + defer fake.reconcilerForSourceTypeMutex.Unlock() + fake.ReconcilerForSourceTypeStub = nil + fake.reconcilerForSourceTypeReturns = struct { + result1 reconciler.RegistryReconciler + }{result1} +} + +func (fake *FakeReconcilerReconciler) ReconcilerForSourceTypeReturnsOnCall(i int, result1 reconciler.RegistryReconciler) { + fake.reconcilerForSourceTypeMutex.Lock() + defer fake.reconcilerForSourceTypeMutex.Unlock() + fake.ReconcilerForSourceTypeStub = nil + if fake.reconcilerForSourceTypeReturnsOnCall == nil { + fake.reconcilerForSourceTypeReturnsOnCall = make(map[int]struct { + result1 reconciler.RegistryReconciler + }) + } + fake.reconcilerForSourceTypeReturnsOnCall[i] = struct { + result1 reconciler.RegistryReconciler + }{result1} +} + +func (fake *FakeReconcilerReconciler) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.reconcilerForSourceTypeMutex.RLock() + defer fake.reconcilerForSourceTypeMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeReconcilerReconciler) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ reconciler.ReconcilerReconciler = new(FakeReconcilerReconciler)