Repository: redhat-developer/service-binding-operator
Pull request #162 (2019-08-31 15:03:22)

Adding Watches when a CSV is Created/Updated
Refactoring controller to accommodate watch over `ClusterServiceVersion` (`CSV`) objects in order to inspect and create controller's watch over described `GroupVersionKind` references.

Fixes: #142

### How to Simulate

To simulate this PR, you would have to create a new `CSV` object in the same namespace the operator is working at.

First, spin up operator locally, wait for bootstrap to be completed:

```
$ make local
```

In another shell clone [postgresql-operator](https://github.com/operator-backing-service-samples/postgresql-operator.git), and temporarily edit `manifests/stable/postgresql-operator.v0.0.6.clusterserviceversion.yaml` to use the same namespace than operator.

```
$ sed 's/namespace: placeholder/namespace: YOUR_NAMESPACE/g' manifests/stable/postgresql-operator.v0.0.6.clusterserviceversion.yaml > /var/tmp/example-csv.yaml
$ kubectl apply -f /var/tmp/example-csv.yaml --validate=false
```

On applying `postgresql-operator.v0.0.6`, you should see the operator print the following log line:

```
{"level":"info","ts":1567266681.516482,"logger":"olm","msg":"Extracting GVK from CRDDescription","CRDDescription.Name":"databases.postgresql.baiju.dev"}
2019-08-31T17:51:21.516+0200	INFO	servicebindingrequest.CSVToWatcherMapper	Adding watch for GVK	{"Obj.NamespacedName": "otaviof/postgresql-operator.v0.0.6", "GVK": "postgresql.baiju.dev/v1alpha1, Kind=Database"}
```

openshift-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **otaviof**
You can assign the PR to them by writing `/assign @otaviof` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=redhat-developer%2Fservice-binding-operator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/redhat-developer/service-binding-operator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["otaviof"]} -->
openshift-ci-robot: @otaviof: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/lint | 02b89c6140dfee4eb77f9d13bba7a5a3f34cb451 | [link](https://prow.svc.ci.openshift.org/view/gcs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/162/pull-ci-redhat-developer-service-binding-operator-master-lint/466) | `/test lint`

[Full PR test history](https://prow.svc.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=162). [Your PR dashboard](https://prow.svc.ci.openshift.org/pr?query=is:pr+state:open+author:otaviof). Please help us cut down on flakes by [linking to](https://github.com/kubernetes/community/blob/master/contributors/devel/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/redhat-developer/service-binding-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
codecov[bot]: # [Codecov](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@d4037e6`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `24.71%`.

[![Impacted file tree graph](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162/graphs/tree.svg?width=650&token=CICBeVWlrC&height=150&src=pr)](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##             master     #162   +/-   ##
=========================================
  Coverage          ?   59.84%           
=========================================
  Files             ?       11           
  Lines             ?      757           
  Branches          ?        0           
=========================================
  Hits              ?      453           
  Misses            ?      237           
  Partials          ?       67
```


| [Impacted Files](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/controller/servicebindingrequest/common.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162/diff?src=pr&el=tree#diff-cGtnL2NvbnRyb2xsZXIvc2VydmljZWJpbmRpbmdyZXF1ZXN0L2NvbW1vbi5nbw==) | `15.38% <ø> (ø)` | |
| [pkg/controller/servicebindingrequest/controller.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162/diff?src=pr&el=tree#diff-cGtnL2NvbnRyb2xsZXIvc2VydmljZWJpbmRpbmdyZXF1ZXN0L2NvbnRyb2xsZXIuZ28=) | `0% <0%> (ø)` | |
| [pkg/controller/servicebindingrequest/watch.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162/diff?src=pr&el=tree#diff-cGtnL2NvbnRyb2xsZXIvc2VydmljZWJpbmRpbmdyZXF1ZXN0L3dhdGNoLmdv) | `0% <0%> (ø)` | |
| [.../controller/servicebindingrequest/sbrcontroller.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162/diff?src=pr&el=tree#diff-cGtnL2NvbnRyb2xsZXIvc2VydmljZWJpbmRpbmdyZXF1ZXN0L3NicmNvbnRyb2xsZXIuZ28=) | `0% <0%> (ø)` | |
| [pkg/controller/servicebindingrequest/olm.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162/diff?src=pr&el=tree#diff-cGtnL2NvbnRyb2xsZXIvc2VydmljZWJpbmRpbmdyZXF1ZXN0L29sbS5nbw==) | `64.28% <67.18%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162?src=pr&el=footer). Last update [d4037e6...8592cab](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/162?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

openshift-ci-robot: @otaviof: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/e2e | 8592caba92869ecdc812d475ce40cec98462bad6 | [link](https://prow.svc.ci.openshift.org/view/gcs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/162/pull-ci-redhat-developer-service-binding-operator-master-e2e/491) | `/test e2e`

[Full PR test history](https://prow.svc.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=162). [Your PR dashboard](https://prow.svc.ci.openshift.org/pr?query=is:pr+state:open+author:otaviof). Please help us cut down on flakes by [linking to](https://github.com/kubernetes/community/blob/master/contributors/devel/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/redhat-developer/service-binding-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
openshift-ci-robot: @otaviof: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/e2e | 623f56f2429991e9197aa92f4381f4df97fcf869 | [link](https://prow.svc.ci.openshift.org/view/gcs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/162/pull-ci-redhat-developer-service-binding-operator-master-e2e/525) | `/test e2e`

[Full PR test history](https://prow.svc.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=162). [Your PR dashboard](https://prow.svc.ci.openshift.org/pr?query=is:pr+state:open+author:otaviof). Please help us cut down on flakes by [linking to](https://github.com/kubernetes/community/blob/master/contributors/devel/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/redhat-developer/service-binding-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
otaviof: /test e2e
sbose78: /lgtm
sbose78: /approve
openshift-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/redhat-developer/service-binding-operator/pull/162#issuecomment-528871682" title="Approved">sbose78</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=redhat-developer%2Fservice-binding-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/redhat-developer/service-binding-operator/blob/master/OWNERS)~~ [sbose78]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/pkg/controller/servicebindingrequest/annotations.go b/pkg/controller/servicebindingrequest/annotations.go
index 9d788e77..af46e79d 100644
--- a/pkg/controller/servicebindingrequest/annotations.go
+++ b/pkg/controller/servicebindingrequest/annotations.go
@@ -18,9 +18,9 @@ const (
 	sbrNameAnnotation      = "service-binding-operator.apps.openshift.io/binding-name"
 )
 
-// extractNamespacedName returns a types.NamespacedName if the required service binding request keys
+// extractSBRNamespacedName returns a types.NamespacedName if the required service binding request keys
 // are present in the given data
-func extractNamespacedName(data map[string]string) types.NamespacedName {
+func extractSBRNamespacedName(data map[string]string) types.NamespacedName {
 	namespacedName := types.NamespacedName{}
 	ns, exists := data[sbrNamespaceAnnotation]
 	if !exists {
@@ -40,40 +40,40 @@ func extractNamespacedName(data map[string]string) types.NamespacedName {
 // not present, it checks if the object is an actual SBR, returning the details when positive. An
 // error can be returned in the case the object can't be decoded.
 func GetSBRNamespacedNameFromObject(obj runtime.Object) (types.NamespacedName, error) {
-	namespacedName := types.NamespacedName{}
+	sbrNamespacedName := types.NamespacedName{}
 	data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
 	if err != nil {
-		return namespacedName, err
+		return sbrNamespacedName, err
 	}
 
 	u := &unstructured.Unstructured{Object: data}
 
-	namespacedName = extractNamespacedName(u.GetAnnotations())
+	sbrNamespacedName = extractSBRNamespacedName(u.GetAnnotations())
 	logger := log.WithValues(
 		"Resource.GVK", u.GroupVersionKind(),
 		"Resource.Namespace", u.GetNamespace(),
 		"Resource.Name", u.GetName(),
-		"Extracted.NamespacedName", namespacedName.String(),
+		"SBR.NamespacedName", sbrNamespacedName.String(),
 	)
-	if !IsSBRNamespacedNameEmpty(namespacedName) {
-		logger.Info("Not able to define SBR namespaced-name based on annotations!")
-		return namespacedName, nil
-	}
 
-	if u.GroupVersionKind() != v1alpha1.SchemeGroupVersion.WithKind(ServiceBindingRequestKind) {
-		logger.Info("Object is also not a SBR resource type.")
-		return namespacedName, nil
+	if IsNamespacedNameEmpty(sbrNamespacedName) {
+		logger.Info("SBR information not present in annotations, continue inspecting object")
+	} else {
+		// FIXME: Increase V level for tracing info to avoid flooding logs with this information.
+		logger.Info("SBR information found in annotations, returning it")
+		return sbrNamespacedName, nil
 	}
 
-	logger.Info("Creating namespaced-name for a actual SBR object.")
-	namespacedName.Namespace = u.GetNamespace()
-	namespacedName.Name = u.GetName()
-	return namespacedName, nil
-}
+	if u.GroupVersionKind() == v1alpha1.SchemeGroupVersion.WithKind(ServiceBindingRequestKind) {
+		logger.Info("Object is a SBR, returning its namespaced name")
+		sbrNamespacedName.Namespace = u.GetNamespace()
+		sbrNamespacedName.Name = u.GetName()
+		return sbrNamespacedName, nil
+	}
 
-// IsSBRNamespacedNameEmpty returns true if any of the fields from the given namespacedName is empty.
-func IsSBRNamespacedNameEmpty(namespacedName types.NamespacedName) bool {
-	return namespacedName.Namespace == "" || namespacedName.Name == ""
+	// FIXME: Increase V level for tracing info to avoid flooding logs with this information.
+	logger.Info("Object is not a SBR, returning an empty namespaced name")
+	return sbrNamespacedName, nil
 }
 
 // SetSBRAnnotations update existing annotations to include operator's. The annotations added are
diff --git a/pkg/controller/servicebindingrequest/common.go b/pkg/controller/servicebindingrequest/common.go
index 4fbc7fa5..cd853a2c 100644
--- a/pkg/controller/servicebindingrequest/common.go
+++ b/pkg/controller/servicebindingrequest/common.go
@@ -12,12 +12,15 @@ var (
 	log = logf.ZapLogger(true).WithName("servicebindingrequest")
 )
 
-// ServiceBindingRequestKind defines the name of the CRD Kind.
+// ServiceBindingRequestKind defines the name of the CRD kind.
 const ServiceBindingRequestKind = "ServiceBindingRequest"
 
 // DeploymentConfigKind defines the name of DeploymentConfig kind.
 const DeploymentConfigKind = "DeploymentConfig"
 
+// ClusterServiceVersionKind the name of ClusterServiceVersion kind.
+const ClusterServiceVersionKind = "ClusterServiceVersion"
+
 // RequeueOnNotFound inspect error, if not-found then returns Requeue, otherwise expose the error.
 func RequeueOnNotFound(err error, requeueAfter int64) (reconcile.Result, error) {
 	if errors.IsNotFound(err) {
diff --git a/pkg/controller/servicebindingrequest/controller.go b/pkg/controller/servicebindingrequest/controller.go
index 8c51e65c..5ab913c5 100644
--- a/pkg/controller/servicebindingrequest/controller.go
+++ b/pkg/controller/servicebindingrequest/controller.go
@@ -1,24 +1,10 @@
 package servicebindingrequest
 
 import (
-	"os"
-
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/dynamic"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
-	"sigs.k8s.io/controller-runtime/pkg/event"
-	"sigs.k8s.io/controller-runtime/pkg/handler"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
-	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
-	"sigs.k8s.io/controller-runtime/pkg/source"
-
-	"github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1"
-)
-
-var (
-	controllerName = "servicebindingrequest-controller"
 )
 
 // Add creates a new ServiceBindingRequest Controller and adds it to the Manager. The Manager will
@@ -43,82 +29,11 @@ func newReconciler(mgr manager.Manager, client dynamic.Interface) (reconcile.Rec
 // add adds a new Controller to mgr with r as the reconcile.Reconciler.
 func add(mgr manager.Manager, r reconcile.Reconciler, client dynamic.Interface) error {
 	opts := controller.Options{Reconciler: r}
-	c, err := controller.New(controllerName, mgr, opts)
+	c, err := NewSBRController(mgr, opts, client)
 	if err != nil {
 		return err
 	}
-
-	pred := predicate.Funcs{
-		UpdateFunc: func(e event.UpdateEvent) bool {
-			// ignore updates to CR status in which case metadata.Generation does not change
-			return e.MetaOld.GetGeneration() != e.MetaNew.GetGeneration()
-		},
-		DeleteFunc: func(e event.DeleteEvent) bool {
-			// evaluates to false if the object has been confirmed deleted
-			return !e.DeleteStateUnknown
-		},
-	}
-
-	// watching operator's main CRD -- ServiceBindingRequest
-	sbrGVK := v1alpha1.SchemeGroupVersion.WithKind(ServiceBindingRequestKind)
-	err = c.Watch(createSourceForGVK(sbrGVK), newEnqueueRequestsForSBR(), pred)
-	if err != nil {
-		return err
-	}
-	log.WithValues("GroupVersionKind", sbrGVK).Info("Watch added for ServiceBindingRequest")
-
-	// list of interesting GVKs to watch
-	gvks, err := getWatchingGVKs(client)
-	if err != nil {
-		return err
-	}
-
-	for _, gvk := range gvks {
-		err = c.Watch(createSourceForGVK(gvk), newEnqueueRequestsForSBR(), pred)
-		if err != nil {
-			return err
-		}
-		log.WithValues("GroupVersionKind", gvk).Info("Watch added")
-	}
-
-	return nil
-}
-
-// newEnqueueRequestsForSBR returns a handler.EventHandler configured to map any incoming object to a
-// ServiceBindingRequest if it contains the required configuration.
-func newEnqueueRequestsForSBR() handler.EventHandler {
-	return &handler.EnqueueRequestsFromMapFunc{ToRequests: &SBRRequestMapper{}}
-}
-
-// createSourceForGVK creates a *source.Kind for the given gvk.
-func createSourceForGVK(gvk schema.GroupVersionKind) *source.Kind {
-	return &source.Kind{Type: createUnstructuredWithGVK(gvk)}
-}
-
-// createUnstructuredWithGVK creates a *unstructured.Unstructured with the given gvk.
-func createUnstructuredWithGVK(gvk schema.GroupVersionKind) *unstructured.Unstructured {
-	u := &unstructured.Unstructured{}
-	u.SetGroupVersionKind(gvk)
-	return u
-}
-
-// getWatchingGVKs return a list of GVKs that this controller is interested in watching.
-func getWatchingGVKs(client dynamic.Interface) ([]schema.GroupVersionKind, error) {
-	// standard resources types
-	gvks := []schema.GroupVersionKind{
-		{Group: "", Version: "v1", Kind: "Secret"},
-		{Group: "", Version: "v1", Kind: "ConfigMap"},
-	}
-
-	olm := NewOLM(client, os.Getenv("WATCH_NAMESPACE"))
-	olmGVKs, err := olm.ListCSVOwnedCRDsAsGVKs()
-	if err != nil {
-		log.Error(err, "On listing owned CSV as GVKs")
-		return nil, err
-	}
-	log.WithValues("CSVOwnedGVK.Amount", len(olmGVKs)).
-		Info("Amount of GVK founds in CSV objects.")
-	return append(gvks, olmGVKs...), nil
+	return c.Watch()
 }
 
 // blank assignment to verify that ReconcileServiceBindingRequest implements reconcile.Reconciler
diff --git a/pkg/controller/servicebindingrequest/mapper.go b/pkg/controller/servicebindingrequest/mapper.go
index 1045ac0a..c2cfebb1 100644
--- a/pkg/controller/servicebindingrequest/mapper.go
+++ b/pkg/controller/servicebindingrequest/mapper.go
@@ -23,7 +23,7 @@ func (m *SBRRequestMapper) Map(obj handler.MapObject) []reconcile.Request {
 		logger.Error(err, "on inspecting object for annotations for SBR object")
 		return toReconcile
 	}
-	if IsSBRNamespacedNameEmpty(sbrNamespacedName) {
+	if IsNamespacedNameEmpty(sbrNamespacedName) {
 		log.Info("not able to extract SBR namespaced-name")
 		return toReconcile
 	}
diff --git a/pkg/controller/servicebindingrequest/namespacedname.go b/pkg/controller/servicebindingrequest/namespacedname.go
new file mode 100644
index 00000000..76e1a14c
--- /dev/null
+++ b/pkg/controller/servicebindingrequest/namespacedname.go
@@ -0,0 +1,8 @@
+package servicebindingrequest
+
+import "k8s.io/apimachinery/pkg/types"
+
+// IsNamespacedNameEmpty returns true if any of the fields from the given namespacedName is empty.
+func IsNamespacedNameEmpty(namespacedName types.NamespacedName) bool {
+	return namespacedName.Namespace == "" || namespacedName.Name == ""
+}
diff --git a/pkg/controller/servicebindingrequest/olm.go b/pkg/controller/servicebindingrequest/olm.go
index 57b0238a..3b8f58e3 100644
--- a/pkg/controller/servicebindingrequest/olm.go
+++ b/pkg/controller/servicebindingrequest/olm.go
@@ -9,12 +9,13 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/dynamic"
 	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
 )
 
 // OLM represents the actions this operator needs to take upon Operator-Lifecycle-Manager resources,
-// Like ClusterServiceVersions (CSV) and CRD-Descriptions.
+// like ClusterServiceVersions (CSV) and CRDDescriptions.
 type OLM struct {
 	client dynamic.Interface // kubernetes dynamic client
 	ns     string            // namespace
@@ -37,98 +38,124 @@ func (o *OLM) listCSVs() ([]unstructured.Unstructured, error) {
 	return csvs.Items, nil
 }
 
-// ListCSVOwnedCRDs return a unstructured list of CRD objects from "owned" section in CSVs.
-func (o *OLM) ListCSVOwnedCRDs() ([]*unstructured.Unstructured, error) {
-	crds := []*unstructured.Unstructured{}
-	csvs, err := o.listCSVs()
-	if err != nil {
-		return nil, err
-	}
-
-	// TODO: add right logger entries like in Planner;
+// extractOwnedCRDs from a list of CSV objects.
+func (o *OLM) extractOwnedCRDs(
+	csvs []unstructured.Unstructured,
+) ([]unstructured.Unstructured, error) {
+	crds := []unstructured.Unstructured{}
 	for _, csv := range csvs {
 		ownedPath := []string{"spec", "customresourcedefinitions", "owned"}
+		logger := o.logger.WithValues("OwnedPath", ownedPath, "CSV.Name", csv.GetName())
+
 		ownedCRDs, exists, err := unstructured.NestedSlice(csv.Object, ownedPath...)
 		if err != nil {
+			logger.Error(err, "on extracting nested slice")
 			return nil, err
 		}
 		if !exists {
 			continue
 		}
+
 		for _, crd := range ownedCRDs {
 			data := crd.(map[string]interface{})
-			crds = append(crds, &unstructured.Unstructured{Object: data})
+			crds = append(crds, unstructured.Unstructured{Object: data})
 		}
 	}
 
 	return crds, nil
 }
 
-type eachOwnedCRDFn func(crd *olmv1alpha1.CRDDescription)
-
-// loopCSVOwnedCRDs takes a function as parameter and excute this with every CRD object.
-func (o *OLM) loopCSVOwnedCRDs(fn eachOwnedCRDFn) error {
-	crds, err := o.ListCSVOwnedCRDs()
+// ListCSVOwnedCRDs return a unstructured list of CRD objects from "owned" section in CSVs.
+func (o *OLM) ListCSVOwnedCRDs() ([]unstructured.Unstructured, error) {
+	csvs, err := o.listCSVs()
 	if err != nil {
-		o.logger.Error(err, "during list CSV owned CRDs")
-		return err
+		o.logger.Error(err, "on listting CSVs")
+		return nil, err
 	}
+	return o.extractOwnedCRDs(csvs)
+}
 
-	for _, u := range crds {
-		crd := &olmv1alpha1.CRDDescription{}
-		err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, crd)
+// eachCRDDescriptionFn function to be called against each CRDDescription in a slice.
+type eachCRDDescriptionFn func(crdDescription *olmv1alpha1.CRDDescription)
+
+// loopCRDDescriptions takes a list of CRDDescriptions (extracted from "owned") and converts to a
+// actual type instance, before calling out for informed function. This method can return error in
+// case of issues to convert unstructured into CRDDescription.
+func (o *OLM) loopCRDDescriptions(
+	crdDescriptions []unstructured.Unstructured,
+	fn eachCRDDescriptionFn,
+) error {
+	for _, u := range crdDescriptions {
+		crdDescription := &olmv1alpha1.CRDDescription{}
+		err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, crdDescription)
 		if err != nil {
 			o.logger.Error(err, "on converting from unstructured to CRD")
 			return err
 		}
-		fn(crd)
+		logger := o.logger.WithValues("CRDDescription", crdDescription)
+		logger.Info("Inspecting CRDDescription...")
+		if crdDescription.Name == "" {
+			logger.Info("Skipping empty CRDDescription!")
+			continue
+		}
+		fn(crdDescription)
 	}
-
 	return nil
 }
 
 // SelectCRDByGVK return a single CRD based on a given GVK.
 func (o *OLM) SelectCRDByGVK(gvk schema.GroupVersionKind) (*olmv1alpha1.CRDDescription, error) {
 	logger := o.logger.WithValues("Selector.GVK", gvk)
-	crds := []*olmv1alpha1.CRDDescription{}
+	ownedCRDs, err := o.ListCSVOwnedCRDs()
+	if err != nil {
+		logger.Error(err, "on listing owned CRDs")
+		return nil, err
+	}
 
-	err := o.loopCSVOwnedCRDs(func(crd *olmv1alpha1.CRDDescription) {
+	crdDescriptions := []*olmv1alpha1.CRDDescription{}
+	err = o.loopCRDDescriptions(ownedCRDs, func(crdDescription *olmv1alpha1.CRDDescription) {
 		logger = logger.WithValues(
-			"CRDDescription.Name", crd.Name,
-			"CRDDescription.Version", crd.Version,
-			"CRDDescription.Kind", crd.Kind,
+			"CRDDescription.Name", crdDescription.Name,
+			"CRDDescription.Version", crdDescription.Version,
+			"CRDDescription.Kind", crdDescription.Kind,
 		)
 		logger.Info("Inspecting CRDDescription object...")
 		// checking for suffix since is expected to have object type as prefix
-		if !strings.EqualFold(strings.ToLower(crd.Kind), strings.ToLower(gvk.Kind)) {
+		if !strings.EqualFold(strings.ToLower(crdDescription.Kind), strings.ToLower(gvk.Kind)) {
 			return
 		}
-		if crd.Version != "" && strings.ToLower(gvk.Version) != strings.ToLower(crd.Version) {
+		// matching resource version, unless when not informed
+		if crdDescription.Version != "" &&
+			strings.ToLower(gvk.Version) != strings.ToLower(crdDescription.Version) {
 			return
 		}
 		logger.Info("CRDDescription object matches selector!")
-		crds = append(crds, crd)
+		crdDescriptions = append(crdDescriptions, crdDescription)
 	})
 	if err != nil {
 		return nil, err
 	}
 
-	if len(crds) == 0 {
+	if len(crdDescriptions) == 0 {
 		logger.Info("No CRD could be found for GVK.")
 		return nil, nil
 	}
-	return crds[0], nil
+	return crdDescriptions[0], nil
 }
 
-// ListCSVOwnedCRDsAsGVKs return the list of owned CRDs from all CSV objects as a list of GVKs.
-func (o *OLM) ListCSVOwnedCRDsAsGVKs() ([]schema.GroupVersionKind, error) {
+// extractGVKs loop owned objects and extract the GVK information from them.
+func (o *OLM) extractGVKs(
+	crdDescriptions []unstructured.Unstructured,
+) ([]schema.GroupVersionKind, error) {
 	gvks := []schema.GroupVersionKind{}
-	err := o.loopCSVOwnedCRDs(func(crd *olmv1alpha1.CRDDescription) {
-		_, gv := schema.ParseResourceArg(crd.Name)
+	err := o.loopCRDDescriptions(crdDescriptions, func(crdDescription *olmv1alpha1.CRDDescription) {
+		o.logger.WithValues("CRDDescription.Name", crdDescription.Name).
+			Info("Extracting GVK from CRDDescription")
+		_, gv := schema.ParseResourceArg(crdDescription.Name)
 		gvks = append(gvks, schema.GroupVersionKind{
 			Group:   gv.Group,
-			Version: crd.Version,
-			Kind:    crd.Kind,
+			Version: crdDescription.Version,
+			Kind:    crdDescription.Kind,
 		})
 	})
 	if err != nil {
@@ -137,6 +164,42 @@ func (o *OLM) ListCSVOwnedCRDsAsGVKs() ([]schema.GroupVersionKind, error) {
 	return gvks, nil
 }
 
+// ListCSVOwnedCRDsAsGVKs return the list of owned CRDs from all CSV objects as a list of GVKs.
+func (o *OLM) ListCSVOwnedCRDsAsGVKs() ([]schema.GroupVersionKind, error) {
+	ownedCRDs, err := o.ListCSVOwnedCRDs()
+	if err != nil {
+		o.logger.Error(err, "on listting CSVs")
+		return nil, err
+	}
+	return o.extractGVKs(ownedCRDs)
+}
+
+// ListGVKsFromCSVNamespacedName return the list of owned GVKs for a given CSV namespaced named.
+func (o *OLM) ListGVKsFromCSVNamespacedName(
+	namespacedName types.NamespacedName,
+) ([]schema.GroupVersionKind, error) {
+	logger := o.logger.WithValues("CSV.NamespacedName", namespacedName)
+	logger.Info("Reading CSV to extract GVKs...")
+	gvr := olmv1alpha1.SchemeGroupVersion.WithResource(csvResource)
+	resourceClient := o.client.Resource(gvr).Namespace(namespacedName.Namespace)
+	u, err := resourceClient.Get(namespacedName.Name, metav1.GetOptions{})
+	if err != nil {
+		logger.Error(err, "on reading CSV object")
+		return []schema.GroupVersionKind{}, err
+	}
+
+	unstructuredCSV := *u
+	csvs := []unstructured.Unstructured{unstructuredCSV}
+
+	ownedCRDs, err := o.extractOwnedCRDs(csvs)
+	if err != nil {
+		logger.Error(err, "on extracting owned CRDs")
+		return []schema.GroupVersionKind{}, err
+	}
+
+	return o.extractGVKs(ownedCRDs)
+}
+
 // NewOLM instantiate a new OLM.
 func NewOLM(client dynamic.Interface, ns string) *OLM {
 	return &OLM{
diff --git a/pkg/controller/servicebindingrequest/sbrcontroller.go b/pkg/controller/servicebindingrequest/sbrcontroller.go
new file mode 100644
index 00000000..4765c058
--- /dev/null
+++ b/pkg/controller/servicebindingrequest/sbrcontroller.go
@@ -0,0 +1,191 @@
+package servicebindingrequest
+
+import (
+	"os"
+
+	"github.com/go-logr/logr"
+	olmv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
+	"sigs.k8s.io/controller-runtime/pkg/source"
+
+	"github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1"
+)
+
+// SBRController hold the controller instance and methods for a ServiceBindingRequest.
+type SBRController struct {
+	Controller   controller.Controller            // controller-runtime instance
+	Client       dynamic.Interface                // kubernetes dynamic api client
+	watchingGVKs map[schema.GroupVersionKind]bool // cache to identify GVKs on watch
+	logger       logr.Logger                      // logger instance
+}
+
+var (
+	// controllerName commong name of this controller
+	controllerName = "servicebindingrequest-controller"
+	// defaultPredicate default predicate functions
+	defaultPredicate = predicate.Funcs{
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			// ignore updates to CR status in which case metadata.Generation does not change
+			return e.MetaOld.GetGeneration() != e.MetaNew.GetGeneration()
+		},
+		DeleteFunc: func(e event.DeleteEvent) bool {
+			// evaluates to false if the object has been confirmed deleted
+			return !e.DeleteStateUnknown
+		},
+	}
+)
+
+// newEnqueueRequestsForSBR returns a handler.EventHandler configured to map any incoming object to a
+// ServiceBindingRequest if it contains the required configuration.
+func (s *SBRController) newEnqueueRequestsForSBR() handler.EventHandler {
+	return &handler.EnqueueRequestsFromMapFunc{ToRequests: &SBRRequestMapper{}}
+}
+
+// createSourceForGVK creates a *source.Kind for the given gvk.
+func (s *SBRController) createSourceForGVK(gvk schema.GroupVersionKind) *source.Kind {
+	return &source.Kind{Type: s.createUnstructuredWithGVK(gvk)}
+}
+
+// createUnstructuredWithGVK creates a *unstructured.Unstructured with the given gvk.
+func (s *SBRController) createUnstructuredWithGVK(gvk schema.GroupVersionKind) *unstructured.Unstructured {
+	u := &unstructured.Unstructured{}
+	u.SetGroupVersionKind(gvk)
+	return u
+}
+
+// getWatchingGVKs return a list of GVKs that this controller is interested in watching.
+func (s *SBRController) getWatchingGVKs() ([]schema.GroupVersionKind, error) {
+	// standard resources types
+	gvks := []schema.GroupVersionKind{
+		{Group: "", Version: "v1", Kind: "Secret"},
+		{Group: "", Version: "v1", Kind: "ConfigMap"},
+	}
+
+	olm := NewOLM(s.Client, os.Getenv("WATCH_NAMESPACE"))
+	olmGVKs, err := olm.ListCSVOwnedCRDsAsGVKs()
+	if err != nil {
+		log.Error(err, "On listing owned CSV as GVKs")
+		return nil, err
+	}
+	log.WithValues("CSVOwnedGVK.Amount", len(olmGVKs)).
+		Info("Amount of GVK founds in CSV objects.")
+	return append(gvks, olmGVKs...), nil
+}
+
+// AddWatchForGVK creates a watch on a given GVK, as long as it's not duplicated.
+func (s *SBRController) AddWatchForGVK(gvk schema.GroupVersionKind) error {
+	logger := s.logger.WithValues("GVK", gvk)
+	logger.Info("Adding watch for GVK...")
+	if _, exists := s.watchingGVKs[gvk]; exists {
+		logger.Info("Skipping watch on GVK twice, it's already under watch!")
+		return nil
+	}
+
+	// saving GVK in cache
+	s.watchingGVKs[gvk] = true
+
+	logger.Info("Creating watch on GVK")
+	source := s.createSourceForGVK(gvk)
+	return s.Controller.Watch(source, s.newEnqueueRequestsForSBR(), defaultPredicate)
+}
+
+// addCSVWatch creates a watch on ClusterServiceVersion.
+func (s *SBRController) addCSVWatch() error {
+	csvGVK := olmv1alpha1.SchemeGroupVersion.WithKind(ClusterServiceVersionKind)
+	source := s.createSourceForGVK(csvGVK)
+	err := s.Controller.Watch(source, NewCreateWatchEventHandler(s), defaultPredicate)
+	if err != nil {
+		return err
+	}
+	log.WithValues("GVK", csvGVK).Info("Watch added for ClusterServiceVersion")
+
+	return nil
+}
+
+// addSBRWatch creates a watchon ServiceBindingRequest GVK.
+func (s *SBRController) addSBRWatch() error {
+	gvk := v1alpha1.SchemeGroupVersion.WithKind(ServiceBindingRequestKind)
+	logger := s.logger.WithValues("GKV", gvk)
+	source := s.createSourceForGVK(gvk)
+	err := s.Controller.Watch(source, s.newEnqueueRequestsForSBR(), defaultPredicate)
+	if err != nil {
+		logger.Error(err, "on creating watch for ServiceBindingRequest")
+		return err
+	}
+	logger.Info("Watch added for ServiceBindingRequest")
+
+	return nil
+}
+
+// addWhitelistedGVKWatches create watch on GVKs employed on CSVs.
+func (s *SBRController) addWhitelistedGVKWatches() error {
+	// list of interesting GVKs to watch
+	gvks, err := s.getWatchingGVKs()
+	if err != nil {
+		s.logger.Error(err, "on retrieving list of GVKs to watch")
+		return err
+	}
+
+	for _, gvk := range gvks {
+		logger := s.logger.WithValues("GVK", gvk)
+		logger.Info("Adding watch for whitelisted GVK...")
+		err = s.AddWatchForGVK(gvk)
+		if err != nil {
+			logger.Error(err, "on creating watch for GVK")
+			return err
+		}
+	}
+
+	return nil
+}
+
+// Watch setup "watch" for all GVKs relevant for SBRController.
+func (s *SBRController) Watch() error {
+	err := s.addSBRWatch()
+	if err != nil {
+		s.logger.Error(err, "on adding watch for ServiceBindingRequest")
+		return err
+	}
+
+	err = s.addWhitelistedGVKWatches()
+	if err != nil {
+		s.logger.Error(err, "on adding watch for whitelisted GVKs")
+		return err
+	}
+
+	err = s.addCSVWatch()
+	if err != nil {
+		s.logger.Error(err, "on adding watch for ClusterServiceVersion")
+		return err
+	}
+
+	return nil
+}
+
+// NewSBRController creates a new SBRController instance. It can return error on bootstrapping a new
+// dynamic client.
+func NewSBRController(
+	mgr manager.Manager,
+	options controller.Options,
+	client dynamic.Interface,
+) (*SBRController, error) {
+	c, err := controller.New(controllerName, mgr, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return &SBRController{
+		Controller:   c,
+		Client:       client,
+		watchingGVKs: make(map[schema.GroupVersionKind]bool),
+		logger:       logf.Log.WithName("sbrcontroller"),
+	}, nil
+}
diff --git a/pkg/controller/servicebindingrequest/watch.go b/pkg/controller/servicebindingrequest/watch.go
new file mode 100644
index 00000000..c73be381
--- /dev/null
+++ b/pkg/controller/servicebindingrequest/watch.go
@@ -0,0 +1,50 @@
+package servicebindingrequest
+
+import (
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+// CSVToWatcherMapper creates a EventHandler interface to map ClusterServiceVersion objects back to
+// controller and add given GVK to watch list.
+type CSVToWatcherMapper struct {
+	controller *SBRController
+}
+
+// Map requests directed to CSV objects and exctract related GVK to trigger another watch on
+// controller instance.
+func (c *CSVToWatcherMapper) Map(obj handler.MapObject) []reconcile.Request {
+	olm := NewOLM(c.controller.Client, obj.Meta.GetNamespace())
+	namespacedName := types.NamespacedName{
+		Namespace: obj.Meta.GetNamespace(),
+		Name:      obj.Meta.GetName(),
+	}
+
+	logger := log.WithName("CSVToWatcherMapper").WithValues("Obj.NamespacedName", namespacedName)
+
+	gvks, err := olm.ListGVKsFromCSVNamespacedName(namespacedName)
+	if err != nil {
+		logger.Error(err, "Failed on listing GVK with namespaced-name!")
+		return []reconcile.Request{}
+	}
+
+	for _, gvk := range gvks {
+		logger = logger.WithValues("GVK", gvk)
+		logger.Info("Adding watch for GVK")
+		err = c.controller.AddWatchForGVK(gvk)
+		if err != nil {
+			logger.Error(err, "Failed to create a watch")
+		}
+	}
+
+	return []reconcile.Request{}
+}
+
+// NewCreateWatchEventHandler creates a new instance of handler.EventHandler interface with
+// CSVToWatcherMapper as map-func.
+func NewCreateWatchEventHandler(controller *SBRController) handler.EventHandler {
+	return &handler.EnqueueRequestsFromMapFunc{
+		ToRequests: &CSVToWatcherMapper{controller: controller},
+	}
+}


Test patch:
diff --git a/pkg/controller/servicebindingrequest/annotations_test.go b/pkg/controller/servicebindingrequest/annotations_test.go
index a429f543..f5b29d25 100644
--- a/pkg/controller/servicebindingrequest/annotations_test.go
+++ b/pkg/controller/servicebindingrequest/annotations_test.go
@@ -12,10 +12,10 @@ import (
 )
 
 func TestAnnotationsExtractNamespacedName(t *testing.T) {
-	assert.Equal(t, types.NamespacedName{}, extractNamespacedName(map[string]string{}))
+	assert.Equal(t, types.NamespacedName{}, extractSBRNamespacedName(map[string]string{}))
 
 	data := map[string]string{sbrNamespaceAnnotation: "ns", sbrNameAnnotation: "name"}
-	assert.Equal(t, types.NamespacedName{Namespace: "ns", Name: "name"}, extractNamespacedName(data))
+	assert.Equal(t, types.NamespacedName{Namespace: "ns", Name: "name"}, extractSBRNamespacedName(data))
 }
 
 func TestAnnotationsGetSBRNamespacedNameFromObject(t *testing.T) {
@@ -48,10 +48,3 @@ func TestAnnotationsGetSBRNamespacedNameFromObject(t *testing.T) {
 		assert.Equal(t, types.NamespacedName{Namespace: "ns", Name: "name"}, namespacedName)
 	})
 }
-
-func TestAnnotationsIsSBRNamespacedNameEmpty(t *testing.T) {
-	assert.True(t, IsSBRNamespacedNameEmpty(types.NamespacedName{}))
-	assert.True(t, IsSBRNamespacedNameEmpty(types.NamespacedName{Namespace: "ns"}))
-	assert.True(t, IsSBRNamespacedNameEmpty(types.NamespacedName{Name: "name"}))
-	assert.False(t, IsSBRNamespacedNameEmpty(types.NamespacedName{Namespace: "ns", Name: "name"}))
-}
diff --git a/pkg/controller/servicebindingrequest/namespacedname_test.go b/pkg/controller/servicebindingrequest/namespacedname_test.go
new file mode 100644
index 00000000..683c6bab
--- /dev/null
+++ b/pkg/controller/servicebindingrequest/namespacedname_test.go
@@ -0,0 +1,15 @@
+package servicebindingrequest
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestNamespacednameIsSBRNamespacedNameEmpty(t *testing.T) {
+	assert.True(t, IsNamespacedNameEmpty(types.NamespacedName{}))
+	assert.True(t, IsNamespacedNameEmpty(types.NamespacedName{Namespace: "ns"}))
+	assert.True(t, IsNamespacedNameEmpty(types.NamespacedName{Name: "name"}))
+	assert.False(t, IsNamespacedNameEmpty(types.NamespacedName{Namespace: "ns", Name: "name"}))
+}
diff --git a/pkg/controller/servicebindingrequest/olm_test.go b/pkg/controller/servicebindingrequest/olm_test.go
index 5e18f61e..221813f3 100644
--- a/pkg/controller/servicebindingrequest/olm_test.go
+++ b/pkg/controller/servicebindingrequest/olm_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
 	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
 
 	"github.com/redhat-developer/service-binding-operator/test/mocks"
@@ -16,10 +17,21 @@ func init() {
 	logf.SetLogger(logf.ZapLogger(true))
 }
 
+func assertGVKs(t *testing.T, gvks []schema.GroupVersionKind) {
+	for _, gvk := range gvks {
+		t.Logf("Inspecting GVK: '%s'", gvk)
+		assert.NotEmpty(t, gvk.Group)
+		assert.NotEmpty(t, gvk.Version)
+		assert.NotEmpty(t, gvk.Kind)
+	}
+}
+
 func TestOLMNew(t *testing.T) {
 	ns := "controller"
+	csvName := "unit-csv"
+
 	f := mocks.NewFake(t, ns)
-	f.AddMockedUnstructuredCSV("unit-csv")
+	f.AddMockedUnstructuredCSV(csvName)
 	client := f.FakeDynClient()
 	olm := NewOLM(client, ns)
 
@@ -51,5 +63,14 @@ func TestOLMNew(t *testing.T) {
 		gvks, err := olm.ListCSVOwnedCRDsAsGVKs()
 		assert.NoError(t, err)
 		assert.Len(t, gvks, 1)
+		assertGVKs(t, gvks)
+	})
+
+	t.Run("ListGVKsFromCSVNamespacedName", func(t *testing.T) {
+		namespacedName := types.NamespacedName{Namespace: ns, Name: csvName}
+		gvks, err := olm.ListGVKsFromCSVNamespacedName(namespacedName)
+		assert.NoError(t, err)
+		assert.Len(t, gvks, 1)
+		assertGVKs(t, gvks)
 	})
 }