Repository: redhat-developer/service-binding-operator
Pull request #707 (2020-10-02 03:57:37)

Docs: Application workload author's guide
1. Took out the annotation docs from https://github.com/redhat-developer/service-binding-operator/pull/628
2. Updated readme.
3. NEEDS REVIEW --> https://github.com/redhat-developer/service-binding-operator/pull/707/files#diff-ddcac56756c142138b380b68fe56e798

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 **sbose78** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @sbose78` 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":["sbose78"]} -->
pedjak: /hold

#628 needs to be merged first
sbose78: Thank you @Avni-Sharma  , could you do another round of review, please?
baijum: /hold cancel

#628 merged

Patch:
diff --git a/Makefile b/Makefile
index 4984e2f9..8de92a08 100644
--- a/Makefile
+++ b/Makefile
@@ -257,15 +257,14 @@ test-unit-with-coverage:
 
 .PHONY: test-acceptance-setup
 ## Setup the environment for the acceptance tests
-test-acceptance-setup: setup-venv test-setup set-test-namespace deploy-rbac deploy-crds
+test-acceptance-setup: setup-venv
 ifeq ($(TEST_ACCEPTANCE_START_SBO), local)
-test-acceptance-setup:
+test-acceptance-setup: test-cleanup create-test-namespace deploy-test-3rd-party-crds set-test-namespace deploy-rbac deploy-crds
 	$(Q)echo "Starting local SBO instance"
 	$(eval TEST_ACCEPTANCE_SBO_STARTED := $(shell OPERATOR_NAMESPACE="$(TEST_NAMESPACE)" ZAP_FLAGS="$(ZAP_FLAGS)" OUTPUT="$(TEST_ACCEPTANCE_OUTPUT_DIR)" ./hack/deploy-sbo-local.sh))
 else ifeq ($(TEST_ACCEPTANCE_START_SBO), remote)
-test-acceptance-setup:
-	$(Q)echo "Using remote SBO instance running in '$(TEST_NAMESPACE)' namespace"
-	$(eval TEST_ACCEPTANCE_SBO_STARTED := $(TEST_NAMESPACE))
+test-acceptance-setup: test-cleanup create-test-namespace set-test-namespace
+	$(Q)echo "Using remote SBO instance running in '$(SBO_NAMESPACE)' namespace"
 else ifeq ($(TEST_ACCEPTANCE_START_SBO), operator-hub)
 test-acceptance-setup:
 	$(eval TEST_ACCEPTANCE_SBO_STARTED := $(shell ./hack/deploy-sbo-operator-hub.sh))
@@ -283,6 +282,7 @@ test-acceptance: test-acceptance-setup
 	$(Q)TEST_ACCEPTANCE_START_SBO=$(TEST_ACCEPTANCE_START_SBO) \
 		TEST_ACCEPTANCE_SBO_STARTED=$(TEST_ACCEPTANCE_SBO_STARTED) \
 		TEST_NAMESPACE=$(TEST_NAMESPACE) \
+		SBO_NAMESPACE=$(SBO_NAMESPACE) \
 		$(PYTHON_VENV_DIR)/bin/behave --junit --junit-directory $(TEST_ACCEPTANCE_OUTPUT_DIR) $(V_FLAG) --no-capture --no-capture-stderr $(TEST_ACCEPTANCE_TAGS_ARG) test/acceptance/features
 ifeq ($(TEST_ACCEPTANCE_START_SBO), local)
 	$(Q)kill $(TEST_ACCEPTANCE_SBO_STARTED)
diff --git a/README.md b/README.md
index c6bd61d3..84cdb1a6 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,13 @@ spec:
     id: postgresDB
 ```
 
+## User guide
+
+
+* [Application Workload Author's Guide](docs/application-author.md)
+* [Backing Service Provider Best Practices Guide](docs/BackingServiceBestPractices.md)
+
+
 
 
 ## Dependencies
diff --git a/deploy/crds/operators.coreos.com_servicebindings_crd.yaml b/deploy/crds/operators.coreos.com_servicebindings_crd.yaml
index 1a3a3cf6..094419bd 100644
--- a/deploy/crds/operators.coreos.com_servicebindings_crd.yaml
+++ b/deploy/crds/operators.coreos.com_servicebindings_crd.yaml
@@ -257,7 +257,10 @@ spec:
                 - kind
                 - version
                 type: object
+              minItems: 1
               type: array
+          required:
+          - services
           type: object
         status:
           description: ServiceBindingStatus defines the observed state of ServiceBinding
@@ -320,6 +323,8 @@ spec:
           - conditions
           - secret
           type: object
+      required:
+      - spec
       type: object
   version: v1alpha1
   versions:
diff --git a/docs/annotations.md b/docs/annotations.md
new file mode 100644
index 00000000..f648c437
--- /dev/null
+++ b/docs/annotations.md
@@ -0,0 +1,286 @@
+## Binding Metadata in Annotations
+
+ During a binding operation, annotations from relevant Kubernetes resources are extracted to gather information about what is interesting for binding. This information is eventually used to bind the application with the backing service by populating the binding Secret.
+
+### Requirements for specifying binding information in a backing service CRD / Kubernetes resource
+
+1. Extract a string from the Kubernetes resource.
+2. Extract a string from the Kubernetes resource and map it to a custom name in the binding Secret.
+3. Extract an entire ConfigMap/Secret from the Kubernetes resource.
+4. Extract a specific field from the ConfigMap/Secret from the Kubernetes resource, and bind it as an environment variable.
+5. Extract a specific field from the ConfigMap/Secret from the Kubernetes resource and bind it as a volume mount.
+6. Extract a specific field from the ConfigMap/Secret from the Kubernetes resource and map it to a different name in the binding Secret.
+7. Extract a “slice of maps” from the Kubernetes resource and generate multiple fields in the binding Secret.
+8. Extract a "slice of strings" from a Kubernetes resource and indicate the content in a specific index in the slice relevant for binding.
+
+
+### Data model : Building blocks for expressing binding information
+
+* `path`: A template representation of the path to the element in the Kubernetes resource. The value of `path` could be specified in either [JSONPath](https://kubernetes.io/docs/reference/kubectl/jsonpath/) or [GO templates](https://golang.org/pkg/text/template/)
+
+* `elementType`: Specifies if the value of the element referenced in `path` is of type `string` / `sliceOfStrings` / `sliceOfMaps`. Defaults to `string` if omitted.
+
+* `objectType`: Specifies if the value of the element indicated in `path` refers to a `ConfigMap`, `Secret`, or a plain string in the current namespace!  Defaults to `Secret` if omitted and `elementType` is a non-`string`.
+
+* `bindAs`: Specifies if the element is to be bound as an environment variable or a volume mount using the keywords `envVar` and `volume`, respectively. Defaults to `envVar` if omitted.
+
+* `sourceKey`: Specifies the key in the ConfigMap/Secret that is be added to the binding Secret. When used in conjunction with `elementType`=`sliceOfMaps`, `sourceKey` specifies the key in the slice of maps whose value would be used as a key in the binding Secret. This optional field is the operator author intends to express that only when a specific field in the referenced `Secret`/`ConfigMap` is bindable.
+
+* `sourceValue`: Specifies the key in the slice of maps whose value would be used as the value, corresponding to the value of the `sourceKey` which is added as the key, in the binding Secret. Mandatory only if `elementType` is `sliceOfMaps`.
+
+
+### A Sample CR : The Kubernetes resource that the application would bind to
+
+```
+    apiVersion: apps.kube.io/v1beta1
+    kind: Database
+    metadata:
+      name: my-cluster
+    spec:
+    ...
+    status:
+      bootstrap:
+        - type: plain
+          url: myhost2.example.com
+          name: hostGroup1
+        - type: tls
+          url: myhost1.example.com:9092,myhost2.example.com:9092
+          name: hostGroup2
+      data:
+        dbConfiguration: database-config  # ConfigMap
+        dbCredentials: database-cred-Secret # Secret
+        url: db.stage.ibm.com
+```
+
+
+
+### Scenarios
+
+
+1. #### Use everything from the Secret  “status.data.dbCredentials”
+
+    Requirement : *Extract an entire ConfigMap/Secret from the Kubernetes resource*
+
+
+    Annotation:
+
+    ```
+    “servicebinding.dev/dbcredentials”:”path={.status.data.dbcredentials},objectType=Secret”
+    ```
+
+
+    Descriptor:
+
+    ```
+    - path: data.dbcredentials
+      x-descriptors:
+        - urn:alm:descriptor:io.kubernetes:Secret 
+        - servicebinding
+    ```
+
+
+2. #### Use everything from the ConfigMap “status.data.dbConfiguration”
+
+
+    Requirement : *Extract an entire ConfigMap/Secret from the Kubernetes resource*
+
+    Annotation
+
+    ```
+    “servicebinding.dev/dbConfiguration”: "path={.status.data.dbConfiguration},objectType=ConfigMap”
+    ```
+
+
+    Descriptor
+
+    ```
+    - path: data.dbConfiguration
+      x-descriptors:
+        - urn:alm:descriptor:io.kubernetes:ConfigMap 
+        - servicebinding
+    ```
+
+3. #### Use “certificate” from the ConfigMap “status.data.dbConfiguration” as an environment variable
+
+    Requirement : *Extract a specific field from the ConfigMap/Secret from the Kubernetes resource and use it as an environment variable.*
+
+
+    Annotation
+
+    ```
+    “servicebinding.dev/certificate”:
+    "path={.status.data.dbConfiguration},objectType=ConfigMap"
+    ```
+
+
+    Descriptor
+
+
+    ```
+    - path: data.dbConfiguration
+      x-descriptors:
+        - urn:alm:descriptor:io.kubernetes:ConfigMap
+        - servicebinding:certificate:bindAs=envVar
+    ```
+
+
+4. #### Use “certificate” from the ConfigMap “status.data.dbConfiguration” as a volume mount
+
+    Requirement : *Extract a specific field from the ConfigMap/Secret from the Kubernetes resource and use it as a volume mount.*
+
+
+    Annotation
+
+    ```
+    “servicebinding.dev/certificate”:
+    "path={.status.data.dbConfiguration},bindAs=volume,objectType=ConfigMap"
+    ```
+
+
+    Descriptor
+
+    ```
+    - path: data.dbConfiguration
+      x-descriptors:
+        - urn:alm:descriptor:io.kubernetes:ConfigMap
+        - servicebinding:certificate:bindAs=volume
+    ```
+
+
+5. #### Use “db_timeout” from the ConfigMap “status.data.dbConfiguration” as “timeout” in the binding Secret.
+
+    Requirement: *Extract a specific field from the ConfigMap/Secret from the Kubernetes resource and map it to different name in the binding Secret*
+
+    Annotation
+
+    ```
+    “servicebinding.dev/timeout”:
+    “path={.status.data.dbConfiguration},objectType=ConfigMap,sourceKey=db_timeout”
+    ```
+
+
+    Descriptor
+
+    ```
+    - path: data.dbConfiguration
+      x-descriptors:
+        - urn:alm:descriptor:io.kubernetes:ConfigMap
+        - servicebinding:timeout:sourceKey=db_timeout
+    ```
+
+6. #### Use the attribute “status.data.url”
+
+    Requirement: *Extract a string from the Kubernetes resource.*
+
+    Annotation
+
+    ```
+    “servicebinding.dev/url”:"path={.status.data.url}"
+    ```
+
+    Descriptor
+
+    ```
+    - path: data.url
+      x-descriptors:
+        - servicebinding
+    ```
+
+7. #### Use the attribute “status.data.connectionURL” as uri in the binding Secret
+
+    Requirement: *Extract a string from the Kubernetes resource, and map it to custom name in the binding Secret.*
+
+    Annotation
+
+    ```
+    “servicebinding.dev/uri: "path={.status.data.connectionURL}”
+    ```
+
+
+
+    Descriptor
+
+    ```
+    - path: data.connectionURL
+      x-descriptors:
+        - servicebinding:uri
+    ```
+
+8. #### Use specific elements from the CR’s “status.bootstrap” to produce key/value pairs in the  binding Secret
+
+    Requirement: *Extract a “slice of maps” from the Kubernetes resource and generate multiple fields in the binding Secret.*
+
+    Annotation
+
+    ```
+    “servicebinding.dev/endpoints”:
+    "path={.status.bootstrap},elementType=sliceOfMaps,sourceKey=type,sourceValue=url"
+    ```
+
+
+    Descriptor
+
+    ```
+    - path: bootstrap
+      x-descriptors:
+        - servicebinding:endpoints:elementType=sliceOfMaps:sourceKey=type:sourceValue=url
+    ```
+
+9. #### Use Go template to produce key/value pairs in the binding Secret <kbd>EXPERIMENTAL</kbd>
+
+    Requirement: *Extract binding information from the Kubernetes resource using Go templates and generate multiple fields in the binding Secret.*
+
+    A sample Kafka CR:
+
+    ```
+    apiVersion: kafka.strimzi.io/v1alpha1
+    kind: Kafka
+    metadata:
+      name: my-cluster
+    ...
+    status:
+      listeners:
+        - type: plain
+          addresses:
+            - host: my-cluster-kafka-bootstrap.service-binding-demo.svc
+              port: 9092
+            - host: my-cluster-kafka-bootstrap.service-binding-demo.svc
+              port: 9093
+        - type: tls
+          addresses:
+            - host: my-cluster-kafka-bootstrap.service-binding-demo.svc
+              port: 9094
+    ```
+
+    Go Template:
+    ```
+    {{- range $idx1, $lis := .status.listeners -}}
+      {{- range $idx2, $adr := $el1.addresses -}}
+        {{ $lis.type }}_{{ $idx2 }}={{ printf "%s:%s\n" "$adr.host" "$adr.port" | b64enc | quote }}
+      {{- end -}}
+    {{- end -}}
+    ```
+
+    The above Go template produces the following string when executed on the sample Kafka CR:
+    ```
+    plain_0="<base64 encoding of my-cluster-kafka-bootstrap.service-binding-demo.svc:9092>"
+    plain_1="<base64 encoding of my-cluster-kafka-bootstrap.service-binding-demo.svc:9093>"
+    tls_0="<base64 encoding of my-cluster-kafka-bootstrap.service-binding-demo.svc:9094>"
+    ```
+
+    The string can then be parsed into key-value pairs to be added into the final binding secret. The Go template above can be written as one-liner and added as `{{GO TEMPLATE}}` in the annotation and descriptor below.
+
+    Annotation
+
+    ```
+    “servicebinding.dev:
+    "path={.status.listeners},elementType=template,source={{GO TEMPLATE}}"
+    ```
+
+    Descriptor
+
+    ```
+    - path: listeners
+      x-descriptors:
+        - servicebinding:elementType=template:source={{GO TEMPLATE}}
+    ```
diff --git a/docs/application-author.md b/docs/application-author.md
new file mode 100644
index 00000000..f10a27d7
--- /dev/null
+++ b/docs/application-author.md
@@ -0,0 +1,344 @@
+# Application Workload Author's Guide
+
+
+
+- [Introduction to Service Binding](#Introduction-to-Service-binding)
+
+- [Backing Service providing binding metadata](#Backing-service-providing-binding-metadata)
+
+- [Backing Service not providing binding metadata](#Backing-service-not-providing-binding-metadata)
+  * [Annotate Service Resources](##Annotate-service-resources)
+  * [Detect Binding Resources](#Detect-binding-resources)
+  * [Compose custom binding variables](#Compose-custom-binding-variables)
+
+- [Accessing the binding data from the application](#Accessing-the-binding-data-from-the-application)
+
+- [Binding non-podSpec-based application workloads]([#Binding-non-podSpec-based-application-workloads])
+
+<!-- toc -->
+
+
+# Introduction to Service Binding
+
+A Service Binding involves connecting an application to one or more backing services using a binding secret generated for the purpose of storing information to be consumed by the application.
+
+``` yaml
+apiVersion: operators.coreos.com/v1alpha1
+kind: ServiceBinding
+metadata:
+  name: binding-request
+  namespace: service-binding-demo
+
+spec:
+  ## Workload where binding information is
+  ## injected.
+
+  application:
+    name: java-app
+    group: apps
+    version: v1
+    resource: deployments
+
+  ## One or more Service resources from which
+  ## binding information is collected
+  services:
+  - group: database.example.com
+    version: v1alpha1
+    kind: DBInstance
+    name: db
+
+  - group: database.example.com
+    version: v1alpha1
+    kind: DBCredentials
+    name: db
+
+  - group: "route.openshift.io"
+    version: v1
+    kind: Route
+    name: auth-service
+```
+
+The application workload expects binding metadata to be present on the Kubernetes Resources representing the backing service.
+
+As shown above, you may also directly use a `ConfigMap` or a `Secret` itself as a service resource that would be used as a source of binding information.
+
+# Backing Service providing binding metadata
+
+If the backing service author has provided binding metadata in the corresponding CRD,
+then Service Binding acknowledges it and automatically creates a binding secret with
+the information relevant for binding.
+
+The backing service may provide binding information as
+* Metadata in the CRD as annotations
+* Metadata in the OLM bunde manifest file as Descriptors
+* Secret or ConfigMap
+
+If the backing service provides binding metadata, you may use the resource as is
+to express an intent to bind your workload with one or more service resources, by creating a `ServiceBinding`.
+
+# Backing Service not providing binding metadata
+
+If the backing service hasn't provided any binding metadata, the application author may annotate the Kubernetes resource representing the backing service such that the managed binding secret generated has the necessary binding information.
+
+As an application author, you have a couple of options to extract binding information 
+from the backing service: 
+
+* Decorate the backing service resource using annotations.
+* Define custom binding variables.
+
+In the following section, details of the above methods to make 
+a backing service consumable for your application workload, is explained.
+
+## Annotate Service Resources
+---
+
+The application author may consider specific elements of the backing service resource interesting for binding
+
+* A specific attribute in the `spec` section of the Kubernetes resource.
+* A specific attribute in the `status` section of the Kubernetes resource.
+* A specific attribute in the `data` section of the Kubernetes resource.
+* A specific attribute in a `Secret` referenced in the Kubernetes resource.
+* A specific attribute in a `ConfigMap` referenced in the Kubernetes resource.
+
+As an example, if the Cockroachdb authors do not provide any binding metadata in the CRD, you, as an application author may annotate the CR/kubernetes resource that manages the backing service ( cockroach DB ).
+
+Please refer to the [documentation](docs/roadmap.md) to annotate objects for binding metadata.
+
+The backing service could be represented as any one of the following:
+* Custom Resources.
+* Kubernetes Resources, such as `Ingress`, `ConfigMap` and `Secret`.
+* OpenShift Resources, such as `Routes`.
+
+## Compose custom binding variables
+---
+
+If the backing service doesn't expose binding metadata or the values exposed are not easily consumable, then an application author may compose custom binding variables using attributes in the Kubernetes resource representing the backing service.
+
+
+## Custom binding variables
+---
+
+
+The *custom binding variables* feature enables application authors to request customized binding secrets using a combination of Go and jsonpath templating.
+
+Example, the backing service CR may expose the host, port and database user in separate variables, but the application may need to consume this information as a connection string.
+
+
+
+
+``` yaml
+apiVersion: operators.coreos.com/v1alpha1
+kind: ServiceBinding
+metadata:
+  name: multi-service-binding
+  namespace: service-binding-demo
+spec:
+
+  application:
+    name: java-app
+    group: apps
+    version: v1
+    resource: deployments
+
+ services:
+  - group: postgresql.baiju.dev 
+    version: v1alpha1
+    kind: Database
+    name: db-demo   <--- Database service
+    id: postgresDB <--- Optional "id" field
+  - group: ibmcloud.ibm.com
+    version: v1alpha1
+    kind: Binding
+    name: mytranslator-binding <--- Translation service
+    id: translationService
+
+  customEnvVar:
+    ## From the database service
+    - name: JDBC_URL
+      value: 'jdbc:postgresql://{{ .postgresDB.status.dbConnectionIP }}:{{ .postgresDB.status.dbConnectionPort }}/{{ .postgresDB.status.dbName }}'
+    - name: DB_USER
+      value: '{{ .postgresDB.status.dbCredentials.user }}'
+
+    ## From the translator service
+    - name: LANGUAGE_TRANSLATOR_URL
+      value: '{{ index translationService.status.secretName "url" }}'
+    - name: LANGUAGE_TRANSLATOR_IAM_APIKEY
+      value: '{{ index translationService.status.secretName "apikey" }}'
+
+    ## From both the services!
+    - name: EXAMPLE_VARIABLE
+      value: '{{ .postgresDB.status.dbName }}{{ translationService.status.secretName}}'
+   
+    ## Generate JSON. 
+    - name: DB_JSON
+      value: {{ json .postgresDB.status }}
+    
+```
+
+This has been adopted in [IBM CodeEngine](https://cloud.ibm.com/docs/codeengine?topic=codeengine-kn-service-binding).
+
+
+*In future releases, the above would be supported as volume mounts too.*
+
+## Detect Binding Resources
+---
+
+The Service Binding Operator binds all information 'dependent' to the backing service CR by populating the binding secret with information from Routes, Services, ConfigMaps, and Secrets owned by the backing service CR if you express an intent to extract the same in case the backing service isn't annotated with the binding metadata.
+
+[This](https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/#owners-and-dependents) is how owner and dependent relationships are set in Kubernetes.
+
+The binding is initiated by the setting this `detectBindingResources: true` in the `ServiceBinding` CR's `spec`.
+
+``` yaml
+apiVersion: operators.coreos.com/v1alpha1
+kind: ServiceBinding
+metadata:
+  name: etcdbinding
+  namespace: service-binding-demo
+spec:
+  detectBindingResources: true
+  application:
+    name: java-app
+    group: apps
+    version: v1
+    resource: deployments
+  services:
+  - group: etcd.database.coreos.com
+    version: v1beta2
+    kind: EtcdCluster
+    name: etcd-cluster-example
+
+```
+
+When this API option is set to true, the Service Binding Operator automatically detects Routes, Services, ConfigMaps, and Secrets owned by the backing service CR and generates a binding secret out of it.
+
+
+
+## Accessing the binding data from the application
+---
+
+The binding secret generated for the `ServiceBinding` may be associated with the workload as environment variables or volume mounts ("files").
+
+
+``` yaml
+apiVersion: operators.coreos.com/v1alpha1
+kind: ServiceBinding
+metadata:
+  name: binding-request
+  namespace: service-binding-demo
+spec:
+  application:
+    name: java-app
+    group: apps
+    version: v1
+    resource: deployments
+  services:
+  - group: charts.helm.k8s.io
+    version: v1alpha1
+    kind: Cockroachdb
+    name: db-demo
+    id: db_1
+```
+
+The generated binding secret would look like this:
+
+``` yaml
+kind: Secret
+apiVersion: v1
+metadata:
+  name: example-servicebindingrequest
+  namespace: pgo
+data:
+  COCKROACHDB_CLUSTERIP: MTcyLjMwLjEwMS4zNA==
+  COCKROACHDB_CONF_PORT: MjYyNTc=
+type: Opaque
+```
+
+This would generate a binding secret and inject it into the workload as an environment variable or a volume mount in the path `/var/data` depending on the intent expressed in the annotations on the Custom Resource.
+
+
+Here's how the environment variables look like:
+
+```
+$ env | grep COCKROACHDB
+
+COCKROACHDB_CLUSTERIP=172.10.2.3
+COCKROACHDB_CONF_PORT=8090
+
+```
+
+Here's how the mount paths look like:
+
+```
+var
+├── data
+│   ├── COCKROACHDB_CLUSTERIP
+│   ├── COCKROACHDB_CONF_PORT
+
+```
+
+Instead of `/var/data`, you can specify a custom binding root path by specifying the same in `spec.mountPathPrefix`, example,
+
+``` yaml
+apiVersion: operators.coreos.com/v1alpha1
+kind: ServiceBinding
+metadata:
+  name: binding-request
+  namespace: service-binding-demo
+spec:
+  application:
+    name: java-app
+    group: apps
+    version: v1
+    resource: deployments
+  services:
+  - group: charts.helm.k8s.io
+    version: v1alpha1
+    kind: Cockroachdb
+    name: db-demo
+    id: db_1
+  mounthPathPrefix: '/bindings/accounts-db' # User configurable binding root
+```
+
+Here's how the mount paths would look like, where applicable:
+
+```
+bindings
+├── accounts-db
+│   ├── COCKROACHDB_CLUSTERIP
+│   ├── COCKROACHDB_CONF_PORT
+```
+
+
+**Note**
+
+*Injection of binding information as volume mounts is in the development phase and is not stable enough for use.*
+
+
+
+# Binding non-podSpec-based application workloads
+
+If your application is to be deployed as a non-podSPec-based workload such that the containers path should bind at a custom location, the `ServiceBinding` API provides an API to achieve that. 
+
+
+``` yaml
+apiVersion: operators.coreos.com/v1alpha1
+kind: ServiceBinding
+metadata:
+  name: binding-request
+  namespace: service-binding-demo
+spec:
+    application:
+        resourceRef: example-appconfig
+        group: stable.example.com
+        version: v1
+        resource: appconfigs
+        bindingPath:
+            secretPath: spec.secret # custom path to secret reference
+            containersPath: spec.containers # custom path to containers reference
+    ...
+    ...
+```
+
+A detailed documentation could be found [here](docs/binding-path.md).
+
diff --git a/hack/remove-sbr-finalizers.sh b/hack/remove-sbr-finalizers.sh
index f0575702..5f4eef3a 100755
--- a/hack/remove-sbr-finalizers.sh
+++ b/hack/remove-sbr-finalizers.sh
@@ -7,11 +7,12 @@ else
 fi
 
 # Remove SBR finalizers if CRD exists
-[ -z "$(kubectl get -f deploy/crds/*crd.yaml -o jsonpath="{.metadata.name}" --ignore-not-found)" ] && exit 0
+CRD_NAME=$(kubectl get -f deploy/crds/*crd.yaml -o jsonpath="{.metadata.name}" --ignore-not-found)
+[ -z $CRD_NAME ] && exit 0
 
-SBRS=($(kubectl get sbrs $USE_NS -o jsonpath="{.items[*].metadata.name}"))
-SBR_NS=($(kubectl get sbrs $USE_NS -o jsonpath="{.items[*].metadata.namespace}"))
+SBRS=($(kubectl get $CRD_NAME $USE_NS -o jsonpath="{.items[*].metadata.name}"))
+SBR_NS=($(kubectl get $CRD_NAME $USE_NS -o jsonpath="{.items[*].metadata.namespace}"))
 
 for i in "${!SBRS[@]}"; do
-    kubectl patch sbr/${SBRS[$i]} -p '{"metadata":{"finalizers":[]}}' --type=merge --namespace=${SBR_NS[$i]};
+    kubectl patch $CRD_NAME/${SBRS[$i]} -p '{"metadata":{"finalizers":[]}}' --type=merge --namespace=${SBR_NS[$i]};
 done
diff --git a/pkg/apis/operators/v1alpha1/servicebinding_types.go b/pkg/apis/operators/v1alpha1/servicebinding_types.go
index fbb020ab..2ff0091a 100644
--- a/pkg/apis/operators/v1alpha1/servicebinding_types.go
+++ b/pkg/apis/operators/v1alpha1/servicebinding_types.go
@@ -23,7 +23,8 @@ type ServiceBindingSpec struct {
 	CustomEnvVar []corev1.EnvVar `json:"customEnvVar,omitempty"`
 
 	// Services is used to identify multiple backing services.
-	Services *[]Service `json:"services,omitempty"`
+	// +kubebuilder:validation:MinItems:=1
+	Services []Service `json:"services"`
 
 	// Application is used to identify the application connecting to the
 	// backing service operator.
@@ -112,7 +113,8 @@ type ServiceBinding struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 
-	Spec   ServiceBindingSpec   `json:"spec,omitempty"`
+	// +required
+	Spec   ServiceBindingSpec   `json:"spec"`
 	Status ServiceBindingStatus `json:"status,omitempty"`
 }
 
diff --git a/pkg/apis/operators/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/operators/v1alpha1/zz_generated.deepcopy.go
index b1482d3a..62871307 100644
--- a/pkg/apis/operators/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/operators/v1alpha1/zz_generated.deepcopy.go
@@ -195,13 +195,9 @@ func (in *ServiceBindingSpec) DeepCopyInto(out *ServiceBindingSpec) {
 	}
 	if in.Services != nil {
 		in, out := &in.Services, &out.Services
-		*out = new([]Service)
-		if **in != nil {
-			in, out := *in, *out
-			*out = make([]Service, len(*in))
-			for i := range *in {
-				(*in)[i].DeepCopyInto(&(*out)[i])
-			}
+		*out = make([]Service, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
 	if in.Application != nil {
diff --git a/pkg/controller/servicebinding/mapper.go b/pkg/controller/servicebinding/mapper.go
index 068b3b8c..b48a4172 100644
--- a/pkg/controller/servicebinding/mapper.go
+++ b/pkg/controller/servicebinding/mapper.go
@@ -42,8 +42,7 @@ func isSecret(obj runtime.Object) bool {
 
 // isSBRService checks whether the given obj is a service in given sbr.
 func isSBRService(sbr *v1alpha1.ServiceBinding, obj runtime.Object) bool {
-	services := servicesOrEmptyList(sbr.Spec.Services)
-	for _, svc := range services {
+	for _, svc := range sbr.Spec.Services {
 		svcGVK := schema.GroupVersionKind{Group: svc.Group, Version: svc.Version, Kind: svc.Kind}
 		if obj.GetObjectKind().GroupVersionKind() == svcGVK {
 			return true
diff --git a/pkg/controller/servicebinding/reconciler.go b/pkg/controller/servicebinding/reconciler.go
index 27919e3a..40f5f834 100644
--- a/pkg/controller/servicebinding/reconciler.go
+++ b/pkg/controller/servicebinding/reconciler.go
@@ -78,14 +78,6 @@ func (r *reconciler) getServiceBinding(
 	return sbr, nil
 }
 
-func servicesOrEmptyList(maybeServices *[]v1alpha1.Service) []v1alpha1.Service {
-	services := []v1alpha1.Service{}
-	if maybeServices != nil {
-		services = *maybeServices
-	}
-	return services
-}
-
 // Reconcile a ServiceBinding by the following steps:
 // 1. Inspecting SBR in order to identify backend service. The service is composed by a CRD name and
 //    kind, and by inspecting "connects-to" label identify the name of service instance;
@@ -128,9 +120,7 @@ func (r *reconciler) Reconcile(request reconcile.Request) (reconcile.Result, err
 
 	ctx := context.Background()
 
-	selectors := servicesOrEmptyList(sbr.Spec.Services)
-
-	if len(selectors) == 0 {
+	if len(sbr.Spec.Services) == 0 {
 		_, updateErr := updateServiceBindingStatus(
 			r.dynClient,
 			sbr,
@@ -162,7 +152,7 @@ func (r *reconciler) Reconcile(request reconcile.Request) (reconcile.Result, err
 		logger.WithName("buildServiceContexts"),
 		r.dynClient,
 		sbr.GetNamespace(),
-		selectors,
+		sbr.Spec.Services,
 		sbr.Spec.DetectBindingResources,
 		r.restMapper,
 	)


Test patch:
diff --git a/pkg/controller/servicebinding/mapper_test.go b/pkg/controller/servicebinding/mapper_test.go
index b3f1b65e..4791d424 100644
--- a/pkg/controller/servicebinding/mapper_test.go
+++ b/pkg/controller/servicebinding/mapper_test.go
@@ -1,4 +1,4 @@
-	package servicebinding
+package servicebinding
 
 import (
 	"testing"
@@ -35,7 +35,7 @@ func TestSBRRequestMapperMap(t *testing.T) {
 				},
 				LocalObjectReference: corev1.LocalObjectReference{Name: "mapper-unit-deployment"},
 			},
-			Services: &[]v1alpha1.Service{
+			Services: []v1alpha1.Service{
 				{
 					GroupVersionKind: metav1.GroupVersionKind{
 						Group:   "",
diff --git a/pkg/controller/servicebinding/sbrcontroller_test.go b/pkg/controller/servicebinding/sbrcontroller_test.go
index 6692091e..89a8c1db 100644
--- a/pkg/controller/servicebinding/sbrcontroller_test.go
+++ b/pkg/controller/servicebinding/sbrcontroller_test.go
@@ -35,7 +35,7 @@ func TestSBRControllerBuildSBRPredicate(t *testing.T) {
 	t.Run("update", func(t *testing.T) {
 		sbrA := &v1alpha1.ServiceBinding{
 			Spec: v1alpha1.ServiceBindingSpec{
-				Services: &[]v1alpha1.Service{
+				Services: []v1alpha1.Service{
 					{
 						GroupVersionKind:     metav1.GroupVersionKind{Group: "test", Version: "v1alpha1", Kind: "TestHost"},
 						LocalObjectReference: corev1.LocalObjectReference{Name: ""},
@@ -45,7 +45,7 @@ func TestSBRControllerBuildSBRPredicate(t *testing.T) {
 		}
 		sbrB := &v1alpha1.ServiceBinding{
 			Spec: v1alpha1.ServiceBindingSpec{
-				Services: &[]v1alpha1.Service{
+				Services: []v1alpha1.Service{
 					{
 						GroupVersionKind:     metav1.GroupVersionKind{Group: "test", Version: "v1", Kind: "TestHost"},
 						LocalObjectReference: corev1.LocalObjectReference{Name: ""},
diff --git a/pkg/controller/servicebinding/servicebinder_test.go b/pkg/controller/servicebinding/servicebinder_test.go
index 16f19cd8..94b734a7 100644
--- a/pkg/controller/servicebinding/servicebinder_test.go
+++ b/pkg/controller/servicebinding/servicebinder_test.go
@@ -220,7 +220,7 @@ func TestServiceBinder_Bind(t *testing.T) {
 				},
 				LocalObjectReference: corev1.LocalObjectReference{Name: d.GetName()},
 			},
-			Services: &[]v1alpha1.Service{
+			Services: []v1alpha1.Service{
 				{
 					GroupVersionKind: metav1.GroupVersionKind{
 						Group:   db1.GetObjectKind().GroupVersionKind().Group,
@@ -255,7 +255,7 @@ func TestServiceBinder_Bind(t *testing.T) {
 				},
 				LocalObjectReference: corev1.LocalObjectReference{Name: d.GetName()},
 			},
-			Services: &[]v1alpha1.Service{
+			Services: []v1alpha1.Service{
 				{
 					GroupVersionKind: metav1.GroupVersionKind{
 						Group:   db1.GetObjectKind().GroupVersionKind().Group,
@@ -301,7 +301,7 @@ func TestServiceBinder_Bind(t *testing.T) {
 				},
 				LocalObjectReference: corev1.LocalObjectReference{Name: d.GetName()},
 			},
-			Services: &[]v1alpha1.Service{
+			Services: []v1alpha1.Service{
 				{
 					GroupVersionKind: metav1.GroupVersionKind{
 						Group:   db1.GetObjectKind().GroupVersionKind().Group,
@@ -336,7 +336,7 @@ func TestServiceBinder_Bind(t *testing.T) {
 			Application: &v1alpha1.Application{
 				LabelSelector: &metav1.LabelSelector{},
 			},
-			Services: &[]v1alpha1.Service{
+			Services: []v1alpha1.Service{
 				{
 					GroupVersionKind: metav1.GroupVersionKind{
 						Group:   db1.GetObjectKind().GroupVersionKind().Group,
@@ -371,7 +371,7 @@ func TestServiceBinder_Bind(t *testing.T) {
 				},
 				LocalObjectReference: corev1.LocalObjectReference{Name: d.GetName()},
 			},
-			Services: &[]v1alpha1.Service{},
+			Services: []v1alpha1.Service{},
 		},
 		Status: v1alpha1.ServiceBindingStatus{},
 	}
diff --git a/pkg/controller/servicebinding/servicecontext_test.go b/pkg/controller/servicebinding/servicecontext_test.go
index f62c0ecd..ba3eabcb 100644
--- a/pkg/controller/servicebinding/servicecontext_test.go
+++ b/pkg/controller/servicebinding/servicecontext_test.go
@@ -1,9 +1,10 @@
 package servicebinding
 
 import (
-	corev1 "k8s.io/api/core/v1"
 	"testing"
 
+	corev1 "k8s.io/api/core/v1"
+
 	routev1 "github.com/openshift/api/route/v1"
 	pgv1alpha1 "github.com/operator-backing-service-samples/postgresql-operator/pkg/apis/postgresql/v1alpha1"
 	"github.com/redhat-developer/service-binding-operator/pkg/apis/operators/v1alpha1"
@@ -49,7 +50,7 @@ func TestBuildServiceContexts(t *testing.T) {
 		sbr := f.AddMockedServiceBinding(sbrName, nil, firstResourceRef, "", deploymentsGVR, matchLabels)
 
 		serviceCtxs, err := buildServiceContexts(
-			logger, f.FakeDynClient(), firstNamespace, *sbr.Spec.Services, &falseBool, restMapper)
+			logger, f.FakeDynClient(), firstNamespace, sbr.Spec.Services, &falseBool, restMapper)
 
 		require.NoError(t, err, "buildServiceContexts must execute without errors")
 		require.Len(t, serviceCtxs, 1, "buildServiceContexts must return only one item")
@@ -97,7 +98,7 @@ func TestBuildServiceContexts(t *testing.T) {
 		sbrName := "services-in-different-ns"
 
 		sbr := f.AddMockedServiceBinding(sbrName, &sameNs, sameNsResourceRef, "", deploymentsGVR, matchLabels)
-		sbr.Spec.Services = &[]v1alpha1.Service{
+		sbr.Spec.Services = []v1alpha1.Service{
 			{
 				GroupVersionKind: metav1.GroupVersionKind{
 					Group:   mocks.CRDName,
@@ -106,7 +107,7 @@ func TestBuildServiceContexts(t *testing.T) {
 				},
 
 				LocalObjectReference: corev1.LocalObjectReference{Name: otherNsResourceRef},
-				Namespace: &otherNs,
+				Namespace:            &otherNs,
 			},
 			{
 				GroupVersionKind: metav1.GroupVersionKind{
@@ -115,12 +116,12 @@ func TestBuildServiceContexts(t *testing.T) {
 					Kind:    mocks.CRDKind,
 				},
 				LocalObjectReference: corev1.LocalObjectReference{Name: sameNsResourceRef},
-				Namespace: &sameNs,
+				Namespace:            &sameNs,
 			},
 		}
 
 		serviceCtxs, err := buildServiceContexts(
-			logger, f.FakeDynClient(), sameNs, *sbr.Spec.Services, &falseBool, restMapper)
+			logger, f.FakeDynClient(), sameNs, sbr.Spec.Services, &falseBool, restMapper)
 
 		require.NoError(t, err, "buildServiceContexts must execute without errors")
 		require.Len(t, serviceCtxs, 2, "buildServiceContexts must return both service contexts")
diff --git a/test/acceptance/features/bindAppToService.feature b/test/acceptance/features/bindAppToService.feature
index a78d4eec..b58bc1d4 100644
--- a/test/acceptance/features/bindAppToService.feature
+++ b/test/acceptance/features/bindAppToService.feature
@@ -670,3 +670,137 @@ Feature: Bind an application to a service
         And The application env var "BACKEND_SPEC_DBNAME" has value "db-demo"
         And jq ".status.conditions[] | select(.type=="CollectionReady").status" of Service Binding "rsa-2" should be changed to "True"
         And jq ".status.conditions[] | select(.type=="InjectionReady").status" of Service Binding "rsa-2" should be changed to "True"
+
+        @negative
+        Scenario: Service Binding with empty services is not allowed in the cluster
+        When Invalid Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-empty-services
+            spec:
+                services:
+            """
+        Then Error message "invalid: spec.services: Invalid value: \"null\"" is thrown
+        And Service Binding "binding-request-empty-services" is not persistent in the cluster
+
+        @negative
+        Scenario: Service Binding without gvk of services is not allowed in the cluster
+        When Invalid Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-without-gvk
+            spec:
+                services:
+                -   name: backend-demo
+            """
+        Then Error message "spec.services.group: Required value" is thrown
+        And Error message "spec.services.kind: Required value" is thrown
+        And Error message "spec.services.version: Required value" is thrown
+        And Service Binding "binding-request-without-gvk" is not persistent in the cluster
+
+        @negative
+        Scenario: Removing service from services field from existing serivce binding is not allowed
+        Given Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-remove-service
+            spec:
+                services:
+                -   group: service.example.com
+                    version: v1
+                    kind: Backserv
+                    name: demo-backserv-cr-2
+            """
+        When Invalid Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-remove-service
+            spec:
+                services:
+            """
+        Then Error message "invalid: spec.services: Required value" is thrown
+        And Service Binding "binding-request-remove-service" is not updated
+
+        @negative
+        Scenario: Service Binding without spec is not allowed in the cluster
+        When Invalid Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-without-spec
+            """
+        Then Error message "spec: Required value" is thrown
+        And Service Binding "binding-request-without-spec" is not persistent in the cluster
+
+        @negative
+        Scenario: Service Binding with empty spec is not allowed in the cluster
+        When Invalid Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-empty-spec
+            spec:
+            """
+        Then Error message "invalid: spec: Invalid value: \"null\"" is thrown
+        And Service Binding "binding-request-empty-spec" is not persistent in the cluster
+
+        @negative
+        Scenario: Emptying spec of existing serivce binding is not allowed
+        Given Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-emptying-spec
+            spec:
+                services:
+                -   group: service.example.com
+                    version: v1
+                    kind: Backserv
+                    name: demo-backserv-cr-2
+            """
+        When Invalid Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-emptying-spec
+            spec:
+            """
+        Then Error message "spec: Required value" is thrown
+        And Service Binding "binding-request-emptying-spec" is not updated
+
+        @negative
+        Scenario: Removing spec of existing serivce binding is not allowed
+        Given Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-remove-spec
+            spec:
+                services:
+                -   group: service.example.com
+                    version: v1
+                    kind: Backserv
+                    name: demo-backserv-cr-2
+            """
+        When Invalid Service Binding is applied
+            """
+            apiVersion: operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: binding-request-remove-spec
+            """
+        Then Error message "spec: Required value" is thrown
+        And Service Binding "binding-request-remove-spec" is not updated
diff --git a/test/acceptance/features/environment.py b/test/acceptance/features/environment.py
index 19d40560..40db44f3 100644
--- a/test/acceptance/features/environment.py
+++ b/test/acceptance/features/environment.py
@@ -13,13 +13,24 @@ before_all(context), after_all(context)
 """
 
 import subprocess
-from pyshould import should
+import os
+
+
+def before_all(_context):
+    start_sbo = os.getenv("TEST_ACCEPTANCE_START_SBO")
+    assert start_sbo is not None, "TEST_ACCEPTANCE_START_SBO is not set. It should be one of local, remote or operator-hub"
+    assert start_sbo in {"local", "remote", "operator-hub"}, "TEST_ACCEPTANCE_START_SBO should be one of local, remote or operator-hub"
+
+    if start_sbo == "local":
+        assert not os.getenv("TEST_ACCEPTANCE_SBO_STARTED").startswith("FAILED"), "TEST_ACCEPTANCE_SBO_STARTED shoud not be FAILED."
+    elif start_sbo == "remote":
+        sbo_namespace = os.getenv("SBO_NAMESPACE")
+        assert sbo_namespace is not None, "SBO_NAMESPACE is required but not set."
+        _context.sbo_namespace = sbo_namespace
+    else:
+        assert False, f"TEST_ACCEPTANCE_START_SBO={start_sbo} is currently unsupported."
 
 
 def before_scenario(_context, _scenario):
-    print("Getting OC status before {} scenario".format(_scenario))
-    code, output = subprocess.getstatusoutput('oc get project default')
-    print("[CODE] {}".format(code))
-    print("[CMD] {}".format(output))
-    code | should.be_equal_to(0)
-    print("***Connected to cluster***")
+    code, output = subprocess.getstatusoutput('kubectl get project default -o jsonpath="{.metadata.name}"')
+    assert code == 0, f"Checking connection to OS cluster by getting the 'default' project failed: {output}"
diff --git a/test/acceptance/features/requirements.txt b/test/acceptance/features/requirements.txt
index 5f14b0f2..eb634ced 100644
--- a/test/acceptance/features/requirements.txt
+++ b/test/acceptance/features/requirements.txt
@@ -1,4 +1,3 @@
 behave==1.2.6
-pyshould==0.7.1
 requests==2.24.0
 polling2==0.4.6
diff --git a/test/acceptance/features/steps/dboperator.py b/test/acceptance/features/steps/dboperator.py
index 204c0ca5..e0b65680 100644
--- a/test/acceptance/features/steps/dboperator.py
+++ b/test/acceptance/features/steps/dboperator.py
@@ -1,5 +1,4 @@
 import re
-from pyshould import should, should_not
 
 from command import Command
 from openshift import Openshift
@@ -50,9 +49,9 @@ class DbOperator():
         return True
 
     def get_package_manifest(self):
-        cmd = 'oc get packagemanifest %s -o "jsonpath={.metadata.name}"' % self.pkgManifest
+        cmd = f'oc get packagemanifest {self.pkgManifest} -o "jsonpath={{.metadata.name}}"'
         manifest = self.cmd.run_check_for_status(
             cmd, status=self.pkgManifest)
-        manifest | should_not.be_equal_to(None)
-        manifest | should.equal(self.pkgManifest)
+        assert manifest is not None, f"Unable to find packagemanifest '{self.pkgManifest}': {manifest}"
+        assert manifest == self.pkgManifest, f"Unexpected packagemanifest found: '{manifest}'. Expected: '{self.pkgManifest}'"
         return manifest
diff --git a/test/acceptance/features/steps/service_binding.py b/test/acceptance/features/steps/service_binding.py
index aece6eaf..6c1ec949 100644
--- a/test/acceptance/features/steps/service_binding.py
+++ b/test/acceptance/features/steps/service_binding.py
@@ -8,5 +8,11 @@ class ServiceBinding(object):
     openshift = Openshift()
 
     def create(self, yaml):
-        sbr_create_output = self.openshift.oc_apply(yaml)
-        return re.search(r'.*servicebinding.operators.coreos.com/.*(created|unchanged)', sbr_create_output)
+        return re.search(r'.*servicebinding.operators.coreos.com/.*(created|unchanged)', self.attempt_to_create(yaml))
+
+    def attempt_to_create(self, yaml):
+        return self.openshift.oc_apply(yaml)
+
+    def get_servicebinding_info_by_jsonpath(self, servicebinding_name, namespace, json_path):
+        openshift = Openshift()
+        return openshift.get_resource_info_by_jsonpath("servicebinding", servicebinding_name, namespace, json_path)
diff --git a/test/acceptance/features/steps/servicebindingoperator.py b/test/acceptance/features/steps/servicebindingoperator.py
index db4fc7dc..5ddfa16b 100644
--- a/test/acceptance/features/steps/servicebindingoperator.py
+++ b/test/acceptance/features/steps/servicebindingoperator.py
@@ -1,7 +1,6 @@
 import os
 
 from openshift import Openshift
-from pyshould import should, should_not
 
 
 class Servicebindingoperator():
@@ -9,25 +8,40 @@ class Servicebindingoperator():
     name = ""
     namespace = ""
 
+    name_pattern = f"{name}.*"
+
     def __init__(self,  name="service-binding-operator", namespace="openshift-operators"):
         self.namespace = namespace
         self.name = name
 
     def check_resources(self):
-        self.openshift.is_resource_in("servicebinding") | should.be_truthy.desc("CRD is in")
-        self.openshift.search_resource_in_namespace("rolebindings", self.name, self.namespace) | should_not.be_none.desc("Role binding is in")
-        self.openshift.search_resource_in_namespace("roles", self.name, self.namespace) | should_not.be_none.desc("Role is in")
-        self.openshift.search_resource_in_namespace("serviceaccounts", self.name, self.namespace) | should_not.be_none.desc("Service Account")
+        crd_name = "servicebinding"
+        assert self.openshift.is_resource_in(crd_name), f"CRD '{crd_name}' does not exist"
+
+        name_pattern = self.get_name_pattern()
+
+        output = self.openshift.search_resource_in_namespace("rolebindings", name_pattern, self.namespace)
+        assert output is not None, f"Unable to find role binding with name that maches '{name_pattern}' in namespace '{self.namespace}': {output}"
+
+        output = self.openshift.search_resource_in_namespace("roles", name_pattern, self.namespace)
+        assert output is not None, f"Unable to find role with name that maches '{name_pattern}' in namespace '{self.namespace}': {output}"
+
+        output = self.openshift.search_resource_in_namespace("serviceaccounts", name_pattern, self.namespace)
+        assert output is not None,  f"Unable to find service account with name that maches '{name_pattern}' in namespace '{self.namespace}': {output}"
+
         return True
 
     def is_running(self):
         start_sbo = os.getenv("TEST_ACCEPTANCE_START_SBO")
-        start_sbo | should_not.be_none.desc("TEST_ACCEPTANCE_START_SBO is set")
-        start_sbo | should.be_in({"local", "remote", "operator-hub"})
+        assert start_sbo is not None, "TEST_ACCEPTANCE_START_SBO is not set. It should be one of local, remote or operator-hub"
+        assert start_sbo in {"local", "remote", "operator-hub"}, "TEST_ACCEPTANCE_START_SBO should be one of local, remote or operator-hub"
 
         if start_sbo == "local":
-            os.getenv("TEST_ACCEPTANCE_SBO_STARTED") | should_not.start_with("FAILED").desc("TEST_ACCEPTANCE_SBO_STARTED is not FAILED")
+            assert not os.getenv("TEST_ACCEPTANCE_SBO_STARTED").startswith("FAILED"), "TEST_ACCEPTANCE_SBO_STARTED shoud not be FAILED."
         elif start_sbo == "operator-hub":
             return False
 
         return self.check_resources()
+
+    def get_name_pattern(self):
+        return self.name_pattern.format(name=self.name)
diff --git a/test/acceptance/features/steps/steps.py b/test/acceptance/features/steps/steps.py
index 74c0bc81..4619c98b 100644
--- a/test/acceptance/features/steps/steps.py
+++ b/test/acceptance/features/steps/steps.py
@@ -20,7 +20,6 @@ from namespace import Namespace
 from nodejs_application import NodeJSApp
 from openshift import Openshift
 from postgres_db import PostgresDB
-from pyshould import should, should_not
 from quarkus_application import QuarkusApplication
 from quarkus_s2i_builder_image import QuarkusS2IBuilderImage
 from serverless_operator import ServerlessOperator
@@ -34,7 +33,7 @@ def given_namespace_is_used(context, namespace_name):
     namespace = Namespace(namespace_name)
     if not namespace.is_present():
         print("Namespace is not present, creating namespace: {}...".format(namespace_name))
-        namespace.create() | should.be_truthy.desc("Namespace {} is created".format(namespace_name))
+        assert namespace.create(), f"Unable to create namespace '{namespace_name}'"
     print("Namespace {} is created!!!".format(namespace_name))
     context.namespace = namespace
 
@@ -59,7 +58,7 @@ def sbo_is_running_in_namespace(context, operator_namespace):
     Checks if the SBO is up and running in the given namespace
     """
     sb_operator = Servicebindingoperator(namespace=operator_namespace)
-    sb_operator.is_running() | should.be_truthy.desc("Service Binding Operator is running")
+    assert sb_operator.is_running(), "Service Binding Operator is not running"
     print("Service binding operator is running!!!")
 
 
@@ -83,8 +82,11 @@ sbo_is_running_step = u'Service Binding Operator is running'
 @given(sbo_is_running_step)
 @when(sbo_is_running_step)
 def sbo_is_running(context):
-    context.namespace | should_not.be_none.desc("Namespace set in context")
-    sbo_is_running_in_namespace(context, context.namespace.name)
+    if "sbo_namespace" in context:
+        sbo_is_running_in_namespace(context, context.sbo_namespace)
+    else:
+        assert context.namespace is not None, "Namespace is not set in context"
+        sbo_is_running_in_namespace(context, context.namespace.name)
 
 
 # STEP
@@ -93,9 +95,9 @@ def given_db_operator_is_installed(context):
     db_operator = DbOperator()
     if not db_operator.is_running():
         print("DB operator is not installed, installing...")
-        db_operator.install_catalog_source() | should.be_truthy.desc("DB catalog source installed")
-        db_operator.install_operator_subscription() | should.be_truthy.desc("DB operator subscription installed")
-        db_operator.is_running(wait=True) | should.be_truthy.desc("DB operator installed")
+        assert db_operator.install_catalog_source(), "Unable to install DB catalog source"
+        assert db_operator.install_operator_subscription(), "Unable to install DB operator subscription"
+        assert db_operator.is_running(wait=True), "Unable to launch DB operator"
     print("PostgresSQL DB operator is running!!!")
 
 
@@ -110,8 +112,8 @@ def nodejs_app_imported_from_image_is_running(context, application_name, applica
     application = NodeJSApp(application_name, namespace.name, application_image)
     if not application.is_running():
         print("application is not running, trying to import it")
-        application.install() | should.be_truthy.desc("Application is installed")
-        application.is_running(wait=True) | should.be_truthy.desc("Application is running")
+        assert application.install(), f"Unable to install application '{application_name}' from image '{application_image}'"
+        assert application.is_running(wait=True), f"Unable to start application '{application_name}' from image '{application_image}'"
     print("Nodejs application is running!!!")
     context.application = application
     context.application_type = "nodejs"
@@ -163,8 +165,8 @@ def db_instance_is_running(context, db_name):
 
     db = PostgresDB(db_name, namespace.name)
     if not db.is_running():
-        db.create() | should.be_truthy.desc("Postgres DB created")
-        db.is_running(wait=True) | should.be_truthy.desc("Postgres DB is running")
+        assert db.create(), f"Unable to create DB '{db_name}'"
+        assert db.is_running(wait=True), f"Unable to launch DB '{db_name}'"
     print(f"DB {db_name} is running!!!")
 
 
@@ -174,10 +176,13 @@ sbr_is_applied_step = u'Service Binding is applied'
 
 @given(sbr_is_applied_step)
 @when(sbr_is_applied_step)
+@then(sbr_is_applied_step)
 def sbr_is_applied(context):
     sbr_yaml = context.text
+    metadata_name = re.sub(r'.*: ', '', re.search(r'name: .*', sbr_yaml).group(0))
+    context.sbr_name = metadata_name
     sbr = ServiceBinding()
-    if context.__contains__("application") and context.__contains__("application_type"):
+    if "application" in context and "application_type" in context:
         application = context.application
         if context.application_type == "nodejs":
             context.application_original_generation = application.get_observed_generation()
@@ -208,18 +213,20 @@ def then_application_redeployed(context):
 @then(u'application should be connected to the DB "{db_name}"')
 def then_app_is_connected_to_db(context, db_name):
     application = context.application
-    app_db_name = application.get_response_from_api(wait=True, endpoint="/api/status/dbNameCM")
-    app_db_name | should.be_equal_to(db_name)
+    db_endpoint = "/api/status/dbNameCM"
+    app_db_name = application.get_response_from_api(wait=True, endpoint=db_endpoint)
+    assert app_db_name == db_name, f"Unexpected response from API ('{db_endpoint}'): '{app_db_name}'. Expected is '{db_name}'"
 
 
 # STEP
 @then(u'jsonpath "{json_path}" of Service Binding "{sbr_name}" should be changed to "{json_value_regex}"')
 def then_sbo_jsonpath_is(context, json_path, sbr_name, json_value_regex):
     openshift = Openshift()
-    openshift.search_resource_in_namespace("servicebindings", sbr_name, context.namespace.name) | should_not.be_none.desc("SBR {sbr_name} exists")
+    assert openshift.search_resource_in_namespace(
+        "servicebindings", sbr_name, context.namespace.name) is not None, f"Service Binding '{sbr_name}' does not exist in namespace '{context.namespace.name}'"
     result = openshift.get_resource_info_by_jsonpath("sbr", sbr_name, context.namespace.name, json_path, wait=True, timeout=600)
-    result | should_not.be_none.desc("jsonpath result")
-    re.fullmatch(json_value_regex, result) | should_not.be_none.desc("SBO jsonpath result \"{result}\" should match \"{json_value_regex}\"")
+    assert result is not None, f"Invalid result for SBO jsonpath: {result}."
+    assert re.fullmatch(json_value_regex, result) is not None, f"SBO jsonpath result \"{result}\" does not match \"{json_value_regex}\""
 
 
 # STEP
@@ -344,7 +351,7 @@ def check_secret_key_with_ip_value(context, secret_name, secret_key):
     json_path = f'{{.data.{secret_key}}}'
     polling2.poll(lambda: ipaddress.ip_address(
         openshift.get_resource_info_by_jsonpath("secrets", secret_name, context.namespace.name, json_path)),
-                  step=5, timeout=120, ignore_exceptions=(ValueError,))
+        step=5, timeout=120, ignore_exceptions=(ValueError,))
 
 
 # STEP
@@ -360,7 +367,7 @@ def apply_yaml(context):
     metadata_name = re.sub(r'.*: ', '', re.search(r'name: .*', yaml).group(0))
     output = openshift.oc_apply(yaml)
     result = re.search(rf'.*{metadata_name}.*(created|unchanged|configured)', output)
-    result | should_not.be_none.desc("CR {metadata_name} Created/Updated")
+    assert result is not None, f"Unable to apply YAML for CR '{metadata_name}': {output}"
 
 
 @then(u'Secret "{secret_ref}" has been injected in to CR "{cr_name}" of kind "{crd_name}" at path "{json_path}"')
@@ -394,3 +401,38 @@ def etc_cluster_is_running(context, etcd_name):
         print("etcd cluster not present, creating etcd cluster")
         assert etcd_cluster.create() is True, "etcd cluster is not created"
         assert etcd_cluster.is_present() is True, "etcd cluster is not present"
+
+
+@when(u'Invalid Service Binding is applied')
+def invalid_sbr_is_applied(context):
+    sbr = ServiceBinding()
+    # Get resource version of sbr if sbr is available
+    if "sbr_name" in context:
+        json_path = "{.metadata.resourceVersion}"
+        rv = sbr.get_servicebinding_info_by_jsonpath(context.sbr_name, context.namespace.name, json_path)
+        context.resource_version = rv
+    context.expected_error = sbr.attempt_to_create(context.text)
+
+
+@then(u'Error message "{err_msg}" is thrown')
+def validate_error(context, err_msg):
+    search = re.search(rf'.*{err_msg}.*', context.expected_error)
+    assert search is not None, f"Actual error: '{context.expected_error}', Expected error: '{err_msg}'"
+
+
+@then(u'Service Binding "{sb_name}" is not persistent in the cluster')
+def validate_absent_sb(context, sb_name):
+    openshift = Openshift()
+    output = openshift.search_resource_in_namespace("servicebindings", sb_name, context.namespace.name)
+    assert output is None, f"Service Binding {sb_name} is present in namespace '{context.namespace.name}'"
+
+
+@then(u'Service Binding "{sb_name}" is not updated')
+def validate_persistent_sb(context, sb_name):
+    openshift = Openshift()
+    json_path = "{.metadata.resourceVersion}"
+    output = openshift.get_resource_info_by_jsonpath("servicebindings", sb_name, context.namespace.name, json_path)
+    if output == context.resource_version:
+        assert True
+    else:
+        assert False, "Service Binding got updated"
diff --git a/test/mocks/mocks.go b/test/mocks/mocks.go
index 36655efa..36923683 100644
--- a/test/mocks/mocks.go
+++ b/test/mocks/mocks.go
@@ -437,15 +437,15 @@ func MultiNamespaceServiceBindingMock(
 			CustomEnvVar:    []corev1.EnvVar{},
 			Application: &v1alpha1.Application{
 				GroupVersionResource: metav1.GroupVersionResource{Group: applicationGVR.Group, Version: applicationGVR.Version, Resource: applicationGVR.Resource},
-				LocalObjectReference:                 corev1.LocalObjectReference{Name: applicationResourceRef},
+				LocalObjectReference: corev1.LocalObjectReference{Name: applicationResourceRef},
 				LabelSelector:        &metav1.LabelSelector{MatchLabels: matchLabels},
 			},
 			DetectBindingResources: &falseBoolPtr,
-			Services: &[]v1alpha1.Service{
+			Services: []v1alpha1.Service{
 				{
-					GroupVersionKind: metav1.GroupVersionKind{Group: CRDName, Version: CRDVersion, Kind: CRDKind},
-					LocalObjectReference:             corev1.LocalObjectReference{Name: backingServiceResourceRef},
-					Namespace:        &backingServiceNamespace,
+					GroupVersionKind:     metav1.GroupVersionKind{Group: CRDName, Version: CRDVersion, Kind: CRDKind},
+					LocalObjectReference: corev1.LocalObjectReference{Name: backingServiceResourceRef},
+					Namespace:            &backingServiceNamespace,
 				},
 			},
 		},
@@ -477,15 +477,15 @@ func ServiceBindingMock(
 			CustomEnvVar:    []corev1.EnvVar{},
 			Application: &v1alpha1.Application{
 				GroupVersionResource: metav1.GroupVersionResource{Group: applicationGVR.Group, Version: applicationGVR.Version, Resource: applicationGVR.Resource},
-				LocalObjectReference:                 corev1.LocalObjectReference{Name: applicationResourceRef},
+				LocalObjectReference: corev1.LocalObjectReference{Name: applicationResourceRef},
 				LabelSelector:        &metav1.LabelSelector{MatchLabels: matchLabels},
 			},
 			DetectBindingResources: &falseBoolPtr,
-			Services: &[]v1alpha1.Service{
+			Services: []v1alpha1.Service{
 				{
-					GroupVersionKind: metav1.GroupVersionKind{Group: CRDName, Version: CRDVersion, Kind: CRDKind},
-					LocalObjectReference:             corev1.LocalObjectReference{Name: backingServiceResourceRef},
-					Namespace:        backingServiceNamespace,
+					GroupVersionKind:     metav1.GroupVersionKind{Group: CRDName, Version: CRDVersion, Kind: CRDKind},
+					LocalObjectReference: corev1.LocalObjectReference{Name: backingServiceResourceRef},
+					Namespace:            backingServiceNamespace,
 				},
 			},
 		},