Repository: redhat-developer/service-binding-operator
Pull request #1296 (2022-10-31 21:35:33)

Rebind workloads when their mappings change
Signed-off-by: Andy Sadler <ansadler@redhat.com>

# Changes

This works in three stages:

1. During workload mapping's validation webhook, we serialize the old mapping into the `servicebinding.io/mapping` annotation on all relevant service bindings.
2. We run our reconcile pipeline once, to unbind the workload using the serialized mapping.  Once completed, we delete the mapping and immediate trigger reprocessing of the service binding.
3. We re-run our reconcile pipeline as normal using the new mapping to project data as expected.

/kind enhancement

# Submitter Checklist

As the author of this PR, please check off the items in this checklist:

- [ ] [Docs](https://github.com/redhat-developer/service-binding-operator/blob/master/CONTRIBUTING.md#docs) 
  included if any changes are user facing
- [x] [Tests](https://github.com/redhat-developer/service-binding-operator/blob/master/CONTRIBUTING.md#tests)
  included if any functionality added or changed. For bugfixes please include tests that can catch regressions
- [x] All acceptance test scenarios included in the PR which verifies a bugfix or a requested feature reported by a non-member are tagged with `@external-feedback` tag.
- [x] Follows the [commit message standard](https://github.com/redhat-developer/service-binding-operator/blob/master/CONTRIBUTING.md#commits)



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

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from sadlerap by writing `/assign @sadlerap` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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":["sadlerap"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer) Report
> Merging [#1296](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer) (0574dda) into [master](https://codecov.io/gh/redhat-developer/service-binding-operator/commit/a6d68cb7250229685d529758c05d50c560b655df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer) (a6d68cb) will **decrease** coverage by `1.38%`.
> The diff coverage is `45.50%`.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296/graphs/tree.svg?width=650&height=150&src=pr&token=CICBeVWlrC&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer)](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer)

```diff
@@            Coverage Diff             @@
##           master    #1296      +/-   ##
==========================================
- Coverage   58.06%   56.67%   -1.39%     
==========================================
  Files          33       35       +2     
  Lines        2797     2987     +190     
==========================================
+ Hits         1624     1693      +69     
- Misses       1018     1130     +112     
- Partials      155      164       +9     
```


| [Impacted Files](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer) | Coverage Δ | |
|---|---|---|
| [.../v1beta1/clusterworkloadresourcemapping\_webhook.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer#diff-YXBpcy9zcGVjL3YxYmV0YTEvY2x1c3Rlcndvcmtsb2FkcmVzb3VyY2VtYXBwaW5nX3dlYmhvb2suZ28=) | `76.34% <0.00%> (-10.25%)` | :arrow_down: |
| [pkg/reconcile/pipeline/api.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer#diff-cGtnL3JlY29uY2lsZS9waXBlbGluZS9hcGkuZ28=) | `77.77% <ø> (ø)` | |
| [...kg/reconcile/pipeline/context/spec\_binding\_impl.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer#diff-cGtnL3JlY29uY2lsZS9waXBlbGluZS9jb250ZXh0L3NwZWNfYmluZGluZ19pbXBsLmdv) | `65.35% <0.00%> (-3.61%)` | :arrow_down: |
| [pkg/reconcile/pipeline/handler/mapping/impl.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer#diff-cGtnL3JlY29uY2lsZS9waXBlbGluZS9oYW5kbGVyL21hcHBpbmcvaW1wbC5nbw==) | `66.66% <ø> (ø)` | |
| [apis/webhooks/validator.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer#diff-YXBpcy93ZWJob29rcy92YWxpZGF0b3IuZ28=) | `46.28% <46.28%> (ø)` | |
| [pkg/reconcile/pipeline/context/impl.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer#diff-cGtnL3JlY29uY2lsZS9waXBlbGluZS9jb250ZXh0L2ltcGwuZ28=) | `64.97% <62.50%> (-1.28%)` | :arrow_down: |
| [pkg/reconcile/pipeline/handler/project/impl.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer#diff-cGtnL3JlY29uY2lsZS9waXBlbGluZS9oYW5kbGVyL3Byb2plY3QvaW1wbC5nbw==) | `56.70% <100.00%> (+2.49%)` | :arrow_up: |
| [apis/webhooks/admission.go](https://codecov.io/gh/redhat-developer/service-binding-operator/pull/1296/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redhat-developer#diff-YXBpcy93ZWJob29rcy9hZG1pc3Npb24uZ28=) | `0.00% <0.00%> (ø)` | |

------

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

</details>
openshift-ci[bot]: @sadlerap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.11-acceptance | 42ce7b28d72152035ec55d723420dadcec3a7b90 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.11-acceptance/1587429264431517696) | true | `/test 4.11-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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[bot]: @sadlerap: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.11-acceptance | 42ce7b28d72152035ec55d723420dadcec3a7b90 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.11-acceptance/1587429264431517696) | true | `/test 4.11-acceptance`
ci/prow/4.9-acceptance | 42ce7b28d72152035ec55d723420dadcec3a7b90 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.9-acceptance/1587429264536375296) | true | `/test 4.9-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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 -->
sadlerap: /retest
openshift-ci[bot]: @sadlerap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.9-acceptance | 42ce7b28d72152035ec55d723420dadcec3a7b90 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.9-acceptance/1587519377949855744) | true | `/test 4.9-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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 -->
sadlerap: /retest
openshift-ci[bot]: @sadlerap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.9-acceptance | 42ce7b28d72152035ec55d723420dadcec3a7b90 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.9-acceptance/1587569813029392384) | true | `/test 4.9-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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 -->
sadlerap: /retest
openshift-ci[bot]: @sadlerap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.11-acceptance | 3019f3b35b0a3d90bb82dabc2d825decf22869b6 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.11-acceptance/1588161270748549120) | true | `/test 4.11-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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 -->
baijum: /lgtm
openshift-ci[bot]: @sadlerap: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.11-acceptance | 3019f3b35b0a3d90bb82dabc2d825decf22869b6 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.11-acceptance/1588161270748549120) | true | `/test 4.11-acceptance`
ci/prow/4.12-acceptance | 5bb6729a496e62cf7979971c50bbffd22c71987e | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.12-acceptance/1588196361218560000) | true | `/test 4.12-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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 -->
sadlerap: /retest
openshift-ci[bot]: New changes are detected. LGTM label has been removed.
openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from baijum by writing `/assign @baijum` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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":["baijum"]} -->
openshift-ci[bot]: @sadlerap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.11-acceptance | 67fd0e486a643e9c6353e663e696ecdf9d7d37e0 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.11-acceptance/1588567585257951232) | true | `/test 4.11-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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[bot]: @sadlerap: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.11-acceptance | 67fd0e486a643e9c6353e663e696ecdf9d7d37e0 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.11-acceptance/1588567585257951232) | true | `/test 4.11-acceptance`
ci/prow/4.10-acceptance | 67fd0e486a643e9c6353e663e696ecdf9d7d37e0 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.10-acceptance/1588567585153093632) | true | `/test 4.10-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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[bot]: @sadlerap: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.11-acceptance | 67fd0e486a643e9c6353e663e696ecdf9d7d37e0 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.11-acceptance/1588567585257951232) | true | `/test 4.11-acceptance`
ci/prow/4.10-acceptance | 67fd0e486a643e9c6353e663e696ecdf9d7d37e0 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.10-acceptance/1588626739091738624) | true | `/test 4.10-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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 -->
sadlerap: /retest
openshift-ci[bot]: @sadlerap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.12-acceptance | 9096e4f932c3ef84429bc3a3c014c4b8dcd026d4 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.12-acceptance/1589656997744087040) | true | `/test 4.12-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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[bot]: @sadlerap: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.12-acceptance | 9096e4f932c3ef84429bc3a3c014c4b8dcd026d4 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.12-acceptance/1589656997744087040) | true | `/test 4.12-acceptance`
ci/prow/4.9-acceptance | 9096e4f932c3ef84429bc3a3c014c4b8dcd026d4 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-developer_service-binding-operator/1296/pull-ci-redhat-developer-service-binding-operator-master-4.9-acceptance/1589656997773447168) | true | `/test 4.9-acceptance`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-developer&repo=service-binding-operator&pr=1296). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sadlerap).

<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 -->
sadlerap: /retest

Patch:
diff --git a/apis/common.go b/apis/common.go
index ab193ad1..a1bd4139 100644
--- a/apis/common.go
+++ b/apis/common.go
@@ -3,18 +3,20 @@ package apis
 import (
 	"encoding/json"
 	"errors"
+	"reflect"
+
 	"k8s.io/api/authentication/v1"
 	authv1 "k8s.io/api/authentication/v1"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"reflect"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 const (
 	finalizerName          = "finalizer.servicebinding.openshift.io"
 	requesterAnnotationKey = "servicebinding.io/requester"
+	MappingAnnotationKey   = "servicebinding.io/mapping"
 )
 
 func MaybeAddFinalizer(obj Object) bool {
diff --git a/apis/spec/v1beta1/clusterworkloadresourcemapping_webhook.go b/apis/spec/v1beta1/clusterworkloadresourcemapping_webhook.go
index 12c01871..df2542be 100644
--- a/apis/spec/v1beta1/clusterworkloadresourcemapping_webhook.go
+++ b/apis/spec/v1beta1/clusterworkloadresourcemapping_webhook.go
@@ -21,18 +21,12 @@ import (
 	"strings"
 
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	"k8s.io/client-go/util/jsonpath"
-	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
 )
 
-func (r *ClusterWorkloadResourceMapping) SetupWebhookWithManager(mgr ctrl.Manager) error {
-	return ctrl.NewWebhookManagedBy(mgr).
-		For(r).
-		Complete()
-}
-
 // EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
 
 // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
@@ -145,3 +139,21 @@ func verifyJsonPath(node jsonpath.Node, path *field.Path, value string) field.Er
 	}
 	return errs
 }
+
+func (mapping *ClusterWorkloadResourceMapping) AcceptsGVR(gvk *schema.GroupVersionResource) bool {
+	expectedName := fmt.Sprintf("%v.%v", gvk.Resource, gvk.Group)
+	if mapping.Name != expectedName {
+		return false
+	}
+	for _, version := range mapping.Spec.Versions {
+		switch version.Version {
+		case gvk.Version:
+			return true
+		case "*":
+			return true
+		default:
+			continue
+		}
+	}
+	return false
+}
diff --git a/apis/webhooks/validator.go b/apis/webhooks/validator.go
new file mode 100644
index 00000000..d8388355
--- /dev/null
+++ b/apis/webhooks/validator.go
@@ -0,0 +1,175 @@
+package webhooks
+
+import (
+	"context"
+	"encoding/json"
+
+	"github.com/redhat-developer/service-binding-operator/apis"
+	"github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1"
+	"github.com/redhat-developer/service-binding-operator/apis/spec/v1beta1"
+	"github.com/redhat-developer/service-binding-operator/pkg/client/kubernetes"
+	"k8s.io/apimachinery/pkg/api/meta"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/rest"
+	ctrl "sigs.k8s.io/controller-runtime"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+)
+
+type MappingValidator struct {
+	client dynamic.Interface
+	lookup kubernetes.K8STypeLookup
+}
+
+// log is for logging in this package.
+var log = logf.Log.WithName("WebHook Spec ClusterWorkloadResourceMapping")
+
+func NewMappingValidator(config *rest.Config, mapper meta.RESTMapper) (*MappingValidator, error) {
+	client, err := dynamic.NewForConfig(config)
+	if err != nil {
+		return nil, err
+	}
+	validator := &MappingValidator{
+		client: client,
+		lookup: kubernetes.ResourceLookup(mapper),
+	}
+
+	return validator, nil
+}
+
+func (validator *MappingValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&v1beta1.ClusterWorkloadResourceMapping{}).
+		WithValidator(validator).
+		Complete()
+}
+
+var _ webhook.CustomValidator = &MappingValidator{}
+
+func (validator *MappingValidator) ValidateCreate(ctx context.Context, obj runtime.Object) error {
+	mapping := obj.(*v1beta1.ClusterWorkloadResourceMapping)
+	err := mapping.ValidateCreate()
+	if err != nil {
+		log.Error(err, "Error validating mapping (create)", "mapping", mapping.Name)
+	}
+	return err
+}
+
+func (validator *MappingValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
+	mapping := newObj.(*v1beta1.ClusterWorkloadResourceMapping)
+	if err := mapping.ValidateCreate(); err != nil {
+		log.Error(err, "Error validating mapping (update)", "mapping", mapping.Name)
+		return err
+	}
+	oldMapping := oldObj.(*v1beta1.ClusterWorkloadResourceMapping)
+	err := Serialize(ctx, oldMapping, validator.client, validator.lookup)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (validator *MappingValidator) ValidateDelete(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
+
+func Serialize(ctx context.Context, mapping *v1beta1.ClusterWorkloadResourceMapping, client dynamic.Interface, lookup kubernetes.K8STypeLookup) error {
+	serialized, err := json.Marshal(mapping)
+	if err != nil {
+		return err
+	}
+	numItems := 0
+
+	gvr := v1beta1.GroupVersionResource
+	data, err := client.Resource(gvr).List(ctx, v1.ListOptions{})
+	if err != nil {
+		return err
+	}
+
+	for _, binding := range data.Items {
+		// we should filter out service bindings that the mapping doesn't affect.
+		sb := v1beta1.ServiceBinding{}
+		err = runtime.DefaultUnstructuredConverter.FromUnstructured(binding.Object, &sb)
+		if err != nil {
+			// short circuit, something's gone terribly wrong
+			return err
+		}
+
+		gvk, _ := sb.Spec.Workload.GroupVersionKind()
+		workloadGVR, err := lookup.ResourceForKind(*gvk)
+		if err != nil {
+			return err
+		}
+		if !mapping.AcceptsGVR(workloadGVR) {
+			// not a relevant binding, skip it
+			continue
+		}
+
+		annotations := binding.GetAnnotations()
+		if annotations == nil {
+			annotations = map[string]string{
+				apis.MappingAnnotationKey: string(serialized),
+			}
+		} else {
+			annotations[apis.MappingAnnotationKey] = string(serialized)
+		}
+		binding.SetAnnotations(annotations)
+
+		x, err := client.Resource(gvr).Namespace(sb.Namespace).Update(ctx, &binding, v1.UpdateOptions{})
+		if err != nil {
+			return err
+		}
+		log.Info("deployed service binding", "annotations", x.GetAnnotations())
+		numItems += 1
+	}
+
+	gvr = v1alpha1.GroupVersionResource
+	data, err = client.Resource(gvr).List(ctx, v1.ListOptions{})
+	if err != nil {
+		return err
+	}
+
+	for _, binding := range data.Items {
+		sb := v1alpha1.ServiceBinding{}
+		err = runtime.DefaultUnstructuredConverter.FromUnstructured(binding.Object, &sb)
+		if err != nil {
+			return err
+		}
+
+		var workloadGVR *schema.GroupVersionResource
+		if sb.Spec.Application.Kind != "" {
+			gvk, _ := sb.Spec.Application.GroupVersionKind()
+			if workloadGVR, err = lookup.ResourceForKind(*gvk); err != nil {
+				return err
+			}
+		} else {
+			workloadGVR, _ = sb.Spec.Application.GroupVersionResource()
+		}
+		if !mapping.AcceptsGVR(workloadGVR) {
+			continue
+		}
+
+		annotations := binding.GetAnnotations()
+		if annotations == nil {
+			annotations = map[string]string{
+				apis.MappingAnnotationKey: string(serialized),
+			}
+		} else {
+			annotations[apis.MappingAnnotationKey] = string(serialized)
+		}
+		binding.SetAnnotations(annotations)
+
+		x, err := client.Resource(gvr).Namespace(sb.Namespace).Update(ctx, &binding, v1.UpdateOptions{})
+		if err != nil {
+			return err
+		}
+		log.Info("deployed service binding", "annotations", x.GetAnnotations())
+		numItems += 1
+	}
+	log.Info("Rebinding", "num_objects", numItems)
+	return nil
+}
diff --git a/controllers/common.go b/controllers/common.go
index d17dfa09..aa3da7e3 100644
--- a/controllers/common.go
+++ b/controllers/common.go
@@ -55,9 +55,10 @@ func (r *BindingReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		return err
 	}
 	r.pipeline = pipeline
+	p := predicate.Or(predicate.GenerationChangedPredicate{}, predicate.AnnotationChangedPredicate{})
 	return ctrl.NewControllerManagedBy(mgr).
 		For(r.ReconcilingObject()).
-		WithEventFilter(predicate.GenerationChangedPredicate{}).
+		WithEventFilter(p).
 		WithOptions(controller.Options{MaxConcurrentReconciles: MaxConcurrentReconciles}).
 		Complete(r)
 }
diff --git a/main.go b/main.go
index bb3eff1e..693506e2 100644
--- a/main.go
+++ b/main.go
@@ -164,7 +164,15 @@ func main() {
 		setupLog.Error(err, "unable to create webhook", "webhook", "SPEC ServiceBinding")
 		os.Exit(1)
 	}
-	if err = (&specv1beta1.ClusterWorkloadResourceMapping{}).SetupWebhookWithManager(mgr); err != nil {
+	mappingValidator, err := webhooks.NewMappingValidator(
+		mgr.GetConfig(),
+		mgr.GetRESTMapper(),
+	)
+	if err != nil {
+		setupLog.Error(err, "unable to initialize webhook", "webhook", "ClusterWorkloadResourceMapping")
+		os.Exit(1)
+	}
+	if err = mappingValidator.SetupWebhookWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create webhook", "webhook", "ClusterWorkloadResourceMapping")
 		os.Exit(1)
 	}
diff --git a/pkg/reconcile/pipeline/api.go b/pkg/reconcile/pipeline/api.go
index b884fa39..c7f45c78 100644
--- a/pkg/reconcile/pipeline/api.go
+++ b/pkg/reconcile/pipeline/api.go
@@ -133,9 +133,15 @@ type Context interface {
 	// if no application found, return an error
 	Applications() ([]Application, error)
 
-	// Returns true if binding is about to be removed
+	// Returns true if the binding needs to be removed
 	UnbindRequested() bool
 
+	// Returns true if the binding is being removed
+	IsRemoved() bool
+
+	// Cleans temporary annotations on the underlying resources
+	CleanAnnotations() bool
+
 	BindingSecretName() string
 
 	// Return true if bindings should be projected as files inside application containers
diff --git a/pkg/reconcile/pipeline/context/impl.go b/pkg/reconcile/pipeline/context/impl.go
index 040471dc..acd3ab63 100644
--- a/pkg/reconcile/pipeline/context/impl.go
+++ b/pkg/reconcile/pipeline/context/impl.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"crypto/sha1"
 	"encoding/hex"
+	"encoding/json"
 	"fmt"
 	"sort"
 	"time"
@@ -88,6 +89,11 @@ type bindingImpl struct {
 }
 
 func (i *impl) UnbindRequested() bool {
+	_, found := i.bindingMeta.GetAnnotations()[apis.MappingAnnotationKey]
+	return found || i.IsRemoved()
+}
+
+func (i *impl) IsRemoved() bool {
 	return !i.bindingMeta.DeletionTimestamp.IsZero()
 }
 
@@ -163,6 +169,16 @@ func (i *bindingImpl) EnvBindings() []*pipeline.EnvBinding {
 	return make([]*pipeline.EnvBinding, 0)
 }
 
+func (i *bindingImpl) CleanAnnotations() bool {
+	annotations := i.serviceBinding.GetAnnotations()
+	_, found := annotations[apis.MappingAnnotationKey]
+	if found {
+		delete(annotations, apis.MappingAnnotationKey)
+		i.serviceBinding.SetAnnotations(annotations)
+	}
+	return found
+}
+
 func (i *bindingImpl) Mappings() map[string]string {
 	result := make(map[string]string)
 	for _, m := range i.serviceBinding.Spec.Mappings {
@@ -405,6 +421,10 @@ func (i *impl) persistBinding() error {
 	}
 	client := i.client.Resource(i.groupVersionResource()).Namespace(i.bindingMeta.Namespace)
 	_, err = client.UpdateStatus(context.Background(), u, metav1.UpdateOptions{})
+	if err != nil {
+		return err
+	}
+	_, err = client.Update(context.Background(), u, metav1.UpdateOptions{})
 	return err
 }
 
@@ -532,33 +552,38 @@ func (i *impl) WorkloadResourceTemplate(gvr *schema.GroupVersionResource, contai
 	}
 
 	var mappingTemplate *v1beta1.ClusterWorkloadResourceMappingTemplate = nil
-	mappingObj, err := i.client.Resource(mappingGVR).
-		Get(context.Background(),
-			gvr.GroupResource().String(),
-			metav1.GetOptions{})
-	if errors.IsNotFound(err) {
-		mappingTemplate = &defaultTemplate
-	} else if mappingObj != nil {
-		var mapping v1beta1.ClusterWorkloadResourceMapping
-		err := runtime.DefaultUnstructuredConverter.FromUnstructured(mappingObj.Object, &mapping)
-		if err != nil {
+	var mapping v1beta1.ClusterWorkloadResourceMapping
+	if mappingData, found := i.bindingMeta.Annotations[apis.MappingAnnotationKey]; found {
+		if err := json.Unmarshal([]byte(mappingData), &mapping); err != nil {
 			return nil, err
 		}
-
-		wildcardTemplate := defaultTemplate.DeepCopy()
-		for _, template := range mapping.Spec.Versions {
-			if template.Version == gvr.Version {
-				mappingTemplate = &template
-				break
-			} else if template.Version == "*" {
-				wildcardTemplate = &template
+	} else {
+		mappingObj, err := i.client.Resource(mappingGVR).
+			Get(context.Background(),
+				gvr.GroupResource().String(),
+				metav1.GetOptions{})
+		if errors.IsNotFound(err) {
+			mappingTemplate = &defaultTemplate
+		} else if mappingObj != nil {
+			err := runtime.DefaultUnstructuredConverter.FromUnstructured(mappingObj.Object, &mapping)
+			if err != nil {
+				return nil, err
 			}
+		} else {
+			return nil, err
 		}
-		if mappingTemplate == nil {
-			mappingTemplate = wildcardTemplate
+	}
+	wildcardTemplate := defaultTemplate.DeepCopy()
+	for _, template := range mapping.Spec.Versions {
+		if template.Version == gvr.Version {
+			mappingTemplate = &template
+			break
+		} else if template.Version == "*" {
+			wildcardTemplate = &template
 		}
-	} else {
-		return nil, err
+	}
+	if mappingTemplate == nil {
+		mappingTemplate = wildcardTemplate
 	}
 
 	if len(mappingTemplate.Containers) == 0 {
@@ -586,7 +611,6 @@ func (i *impl) WorkloadResourceTemplate(gvr *schema.GroupVersionResource, contai
 		return nil, err
 	}
 
-	i.resourceMapping = workloadMapping
 	return workloadMapping, nil
 }
 
diff --git a/pkg/reconcile/pipeline/context/spec_binding_impl.go b/pkg/reconcile/pipeline/context/spec_binding_impl.go
index 9c7150df..c711ee1d 100644
--- a/pkg/reconcile/pipeline/context/spec_binding_impl.go
+++ b/pkg/reconcile/pipeline/context/spec_binding_impl.go
@@ -112,6 +112,16 @@ func (i *specImpl) EnvBindings() []*pipeline.EnvBinding {
 	return result
 }
 
+func (i *specImpl) CleanAnnotations() bool {
+	annotations := i.serviceBinding.GetAnnotations()
+	_, found := annotations[apis.MappingAnnotationKey]
+	if found {
+		delete(annotations, apis.MappingAnnotationKey)
+		i.serviceBinding.SetAnnotations(annotations)
+	}
+	return found
+}
+
 func (i *specImpl) Services() ([]pipeline.Service, error) {
 	if i.services == nil {
 		serviceRef := i.serviceBinding.Spec.Service
diff --git a/pkg/reconcile/pipeline/handler/mapping/impl.go b/pkg/reconcile/pipeline/handler/mapping/impl.go
index c65f4334..4bebb329 100644
--- a/pkg/reconcile/pipeline/handler/mapping/impl.go
+++ b/pkg/reconcile/pipeline/handler/mapping/impl.go
@@ -3,8 +3,9 @@ package mapping
 import (
 	"bytes"
 	"encoding/json"
-	"github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline"
 	"text/template"
+
+	"github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline"
 )
 
 func Handle(ctx pipeline.Context) {
diff --git a/pkg/reconcile/pipeline/handler/project/impl.go b/pkg/reconcile/pipeline/handler/project/impl.go
index 0cc2f08c..fb72f174 100644
--- a/pkg/reconcile/pipeline/handler/project/impl.go
+++ b/pkg/reconcile/pipeline/handler/project/impl.go
@@ -229,7 +229,12 @@ func Unbind(ctx pipeline.Context) {
 			}
 		}
 	}
-	ctx.StopProcessing()
+
+	if ctx.CleanAnnotations() {
+		ctx.RetryProcessing(nil)
+	} else {
+		ctx.StopProcessing()
+	}
 }
 
 func stop(ctx pipeline.Context, err error) {
diff --git a/pkg/reconcile/pipeline/mocks/mocks_pipeline.go b/pkg/reconcile/pipeline/mocks/mocks_pipeline.go
index ccb8b6bb..5b1b3739 100644
--- a/pkg/reconcile/pipeline/mocks/mocks_pipeline.go
+++ b/pkg/reconcile/pipeline/mocks/mocks_pipeline.go
@@ -133,6 +133,20 @@ func (mr *MockContextMockRecorder) BindingSecretName() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindingSecretName", reflect.TypeOf((*MockContext)(nil).BindingSecretName))
 }
 
+// CleanAnnotations mocks base method.
+func (m *MockContext) CleanAnnotations() bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CleanAnnotations")
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// CleanAnnotations indicates an expected call of CleanAnnotations.
+func (mr *MockContextMockRecorder) CleanAnnotations() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanAnnotations", reflect.TypeOf((*MockContext)(nil).CleanAnnotations))
+}
+
 // Close mocks base method.
 func (m *MockContext) Close() error {
 	m.ctrl.T.Helper()
@@ -213,6 +227,20 @@ func (mr *MockContextMockRecorder) HasLabelSelector() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasLabelSelector", reflect.TypeOf((*MockContext)(nil).HasLabelSelector))
 }
 
+// IsRemoved mocks base method.
+func (m *MockContext) IsRemoved() bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "IsRemoved")
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// IsRemoved indicates an expected call of IsRemoved.
+func (mr *MockContextMockRecorder) IsRemoved() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRemoved", reflect.TypeOf((*MockContext)(nil).IsRemoved))
+}
+
 // Mappings mocks base method.
 func (m *MockContext) Mappings() map[string]string {
 	m.ctrl.T.Helper()


Test patch:
diff --git a/apis/spec/v1beta1/clusterworkloadresourcemapping_webhook_test.go b/apis/spec/v1beta1/clusterworkloadresourcemapping_webhook_test.go
index 690340d9..fbb1ad39 100644
--- a/apis/spec/v1beta1/clusterworkloadresourcemapping_webhook_test.go
+++ b/apis/spec/v1beta1/clusterworkloadresourcemapping_webhook_test.go
@@ -4,6 +4,8 @@ import (
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 )
 
@@ -220,3 +222,49 @@ var _ = Describe("Validation Webhook", func() {
 		Expect(mapping.validate()).To(BeNil())
 	})
 })
+
+var _ = Describe("AcceptGVR", func() {
+	var (
+		mapping ClusterWorkloadResourceMapping
+		gvr     schema.GroupVersionResource
+	)
+
+	BeforeEach(func() {
+		mapping = ClusterWorkloadResourceMapping{
+			ObjectMeta: v1.ObjectMeta{
+				Name: "foos.bar",
+			},
+			Spec: ClusterWorkloadResourceMappingSpec{
+				Versions: []ClusterWorkloadResourceMappingTemplate{
+					{
+						Version: "v1",
+					},
+				},
+			},
+		}
+		gvr = schema.GroupVersionResource{
+			Group:    "bar",
+			Resource: "foos",
+			Version:  "v1",
+		}
+	})
+
+	It("should accept a compatible GVR", func() {
+		Expect(mapping.AcceptsGVR(&gvr)).To(BeTrue())
+	})
+
+	It("should accept when matching a wildcard template", func() {
+		mapping.Spec.Versions[0].Version = "*"
+		Expect(mapping.AcceptsGVR(&gvr)).To(BeTrue())
+	})
+
+	It("should reject an incompatible GVR", func() {
+		mapping.Spec.Versions[0].Version = "v2"
+		Expect(mapping.AcceptsGVR(&gvr)).To(BeFalse())
+	})
+
+	It("should ignore GVRs with an incompatible Group/Resource", func() {
+		mapping.Name = "spams.bar"
+		Expect(mapping.AcceptsGVR(&gvr)).To(BeFalse())
+	})
+})
diff --git a/apis/webhooks/suite_test.go b/apis/webhooks/suite_test.go
new file mode 100644
index 00000000..1d51b19c
--- /dev/null
+++ b/apis/webhooks/suite_test.go
@@ -0,0 +1,13 @@
+package webhooks_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestBindingHandlers(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Binding Handlers Suite")
+}
diff --git a/apis/webhooks/validator_test.go b/apis/webhooks/validator_test.go
new file mode 100644
index 00000000..d93fa84b
--- /dev/null
+++ b/apis/webhooks/validator_test.go
@@ -0,0 +1,306 @@
+package webhooks
+
+import (
+	"context"
+	"encoding/json"
+
+	"github.com/golang/mock/gomock"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	"github.com/redhat-developer/service-binding-operator/apis"
+	"github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1"
+	"github.com/redhat-developer/service-binding-operator/apis/spec/v1beta1"
+	"github.com/redhat-developer/service-binding-operator/pkg/client/kubernetes/mocks"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic/fake"
+)
+
+var _ = Describe("Validate", func() {
+	var (
+		mockCtrl  *gomock.Controller
+		validator MappingValidator
+		ctx       context.Context
+		mapping   v1beta1.ClusterWorkloadResourceMapping
+		scheme    *runtime.Scheme
+		lookup    *mocks.MockK8STypeLookup
+	)
+
+	BeforeEach(func() {
+		mockCtrl = gomock.NewController(GinkgoT())
+		scheme = runtime.NewScheme()
+		Expect(v1beta1.AddToScheme(scheme)).NotTo(HaveOccurred())
+		Expect(v1alpha1.AddToScheme(scheme)).NotTo(HaveOccurred())
+		ctx = context.Background()
+		mapping = v1beta1.ClusterWorkloadResourceMapping{
+			Spec: v1beta1.ClusterWorkloadResourceMappingSpec{
+				Versions: []v1beta1.ClusterWorkloadResourceMappingTemplate{
+					v1beta1.DefaultTemplate,
+				},
+			},
+		}
+		lookup = mocks.NewMockK8STypeLookup(mockCtrl)
+		validator = MappingValidator{
+			client: nil,
+			lookup: lookup,
+		}
+	})
+
+	var _ = Describe("Create", func() {
+		It("should accept a valid mapping", func() {
+			err := validator.ValidateCreate(ctx, &mapping)
+			Expect(err).NotTo(HaveOccurred())
+		})
+
+		It("should reject an invalid mapping", func() {
+			mapping.Spec.Versions[0].Volumes = "foo.bar"
+			err := validator.ValidateCreate(ctx, &mapping)
+			Expect(err).To(HaveOccurred())
+		})
+	})
+
+	var _ = Describe("Update", func() {
+		It("should reject an invalid mapping", func() {
+			oldMapping := mapping.DeepCopy()
+			mapping.Spec.Versions[0].Volumes = "foo.bar"
+			err := validator.ValidateUpdate(ctx, oldMapping, &mapping)
+			Expect(err).To(HaveOccurred())
+		})
+
+		It("should accept a valid mapping", func() {
+			oldMapping := mapping.DeepCopy()
+			mapping.Spec.Versions[0].Volumes = ".spec.volumes"
+			validator.client = fake.NewSimpleDynamicClient(scheme)
+			err := validator.ValidateUpdate(ctx, oldMapping, &mapping)
+			Expect(err).NotTo(HaveOccurred())
+		})
+
+		It("should serialize old mappings into relevant service bindings", func() {
+			mapping.Name = "foos.bar"
+			oldMapping := mapping.DeepCopy()
+			mapping.Spec.Versions[0].Volumes = ".spec.volumes"
+			relevantSB := v1beta1.ServiceBinding{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "relevant",
+				},
+				Spec: v1beta1.ServiceBindingSpec{
+					Workload: v1beta1.ServiceBindingWorkloadReference{
+						APIVersion: "bar/v1",
+						Kind:       "Foo",
+						Name:       "x",
+					},
+				},
+			}
+			irrelevantSB := v1beta1.ServiceBinding{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "irrelevant",
+				},
+				Spec: v1beta1.ServiceBindingSpec{
+					Workload: v1beta1.ServiceBindingWorkloadReference{
+						APIVersion: "bar/v1",
+						Kind:       "Spam",
+						Name:       "x",
+					},
+				},
+			}
+			validator.client = fake.NewSimpleDynamicClient(scheme, &relevantSB, &irrelevantSB)
+			spamGVK := schema.GroupVersionKind{Group: "bar", Version: "v1", Kind: "Spam"}
+			spamGVR := schema.GroupVersionResource{Group: "bar", Version: "v1", Resource: "spams"}
+			lookup.EXPECT().ResourceForKind(spamGVK).Return(&spamGVR, nil).Times(2)
+			fooGVK := schema.GroupVersionKind{Group: "bar", Version: "v1", Kind: "Foo"}
+			fooGVR := schema.GroupVersionResource{Group: "bar", Version: "v1", Resource: "foos"}
+			lookup.EXPECT().ResourceForKind(fooGVK).Return(&fooGVR, nil).Times(2)
+
+			err := validator.ValidateUpdate(ctx, oldMapping, &mapping)
+			Expect(err).NotTo(HaveOccurred())
+
+			oldData, err := json.Marshal(oldMapping)
+			Expect(err).NotTo(HaveOccurred())
+
+			data, err := validator.client.Resource(v1beta1.GroupVersionResource).
+				Get(ctx, "relevant", v1.GetOptions{})
+			Expect(err).NotTo(HaveOccurred())
+			Expect(data.GetAnnotations()).To(Equal(map[string]string{apis.MappingAnnotationKey: string(oldData)}))
+
+			data, err = validator.client.Resource(v1beta1.GroupVersionResource).
+				Get(ctx, "irrelevant", v1.GetOptions{})
+			Expect(err).NotTo(HaveOccurred())
+			Expect(data.GetAnnotations()).To(BeEmpty())
+		})
+	})
+
+	var _ = Describe("Delete", func() {
+		It("should not need to validate deletes", func() {
+			err := validator.ValidateDelete(ctx, &mapping)
+			Expect(err).NotTo(HaveOccurred())
+		})
+	})
+})
+
+var _ = Describe("Serialize", func() {
+
+	var (
+		mockCtrl *gomock.Controller
+		binding1 v1beta1.ServiceBinding
+		binding2 v1alpha1.ServiceBinding
+		mapping  v1beta1.ClusterWorkloadResourceMapping
+		lookup   *mocks.MockK8STypeLookup
+	)
+
+	BeforeEach(func() {
+		mockCtrl = gomock.NewController(GinkgoT())
+		binding1 = v1beta1.ServiceBinding{
+			ObjectMeta: v1.ObjectMeta{Name: "foo", Namespace: "bar"},
+			Spec: v1beta1.ServiceBindingSpec{
+				Workload: v1beta1.ServiceBindingWorkloadReference{
+					APIVersion: "foo.bar/v1",
+					Kind:       "Baz",
+					Name:       "spam",
+				},
+			},
+		}
+
+		binding2 = v1alpha1.ServiceBinding{
+			ObjectMeta: v1.ObjectMeta{Name: "foo", Namespace: "bar"},
+			Spec: v1alpha1.ServiceBindingSpec{
+				Application: v1alpha1.Application{
+					Ref: v1alpha1.Ref{
+						Group:   "foo.bar",
+						Version: "v1",
+						Kind:    "Baz",
+						Name:    "spam",
+					},
+				},
+			},
+		}
+
+		mapping = v1beta1.ClusterWorkloadResourceMapping{
+			ObjectMeta: v1.ObjectMeta{Name: "baz.foo.bar"},
+			Spec: v1beta1.ClusterWorkloadResourceMappingSpec{
+				Versions: []v1beta1.ClusterWorkloadResourceMappingTemplate{
+					v1beta1.DefaultTemplate,
+				},
+			},
+		}
+		lookup = mocks.NewMockK8STypeLookup(mockCtrl)
+		lookup.EXPECT().ResourceForKind(schema.GroupVersionKind{
+			Group:   "foo.bar",
+			Kind:    "Baz",
+			Version: "v1",
+		}).Return(&schema.GroupVersionResource{
+			Group:    "foo.bar",
+			Resource: "baz",
+			Version:  "v1",
+		}, nil).AnyTimes()
+	})
+
+	It("should serialize the mapping into annotations", func() {
+		scheme := runtime.NewScheme()
+		Expect(v1beta1.AddToScheme(scheme)).NotTo(HaveOccurred())
+		Expect(v1alpha1.AddToScheme(scheme)).NotTo(HaveOccurred())
+		client := fake.NewSimpleDynamicClient(scheme, &binding1, &binding2, &mapping)
+
+		data, err := json.Marshal(mapping)
+		Expect(err).NotTo(HaveOccurred())
+
+		err = Serialize(context.Background(), &mapping, client, lookup)
+		Expect(err).NotTo(HaveOccurred())
+
+		bindingUnstructured, err := client.
+			Resource(v1beta1.GroupVersionResource).
+			Namespace("bar").
+			Get(context.Background(), "foo", v1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(bindingUnstructured.GetAnnotations()[apis.MappingAnnotationKey]).To(Equal(string(data)))
+
+		bindingUnstructured, err = client.
+			Resource(v1alpha1.GroupVersionResource).
+			Namespace("bar").
+			Get(context.Background(), "foo", v1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(bindingUnstructured.GetAnnotations()[apis.MappingAnnotationKey]).To(Equal(string(data)))
+	})
+
+	It("should not modify existing annotations", func() {
+		annotations := map[string]string{
+			"foo": "bar",
+		}
+		binding1.SetAnnotations(annotations)
+		binding2.SetAnnotations(annotations)
+
+		scheme := runtime.NewScheme()
+		Expect(v1beta1.AddToScheme(scheme)).NotTo(HaveOccurred())
+		Expect(v1alpha1.AddToScheme(scheme)).NotTo(HaveOccurred())
+		client := fake.NewSimpleDynamicClient(scheme, &binding1, &binding2, &mapping)
+
+		data, err := json.Marshal(mapping)
+		Expect(err).NotTo(HaveOccurred())
+
+		err = Serialize(context.Background(), &mapping, client, lookup)
+		Expect(err).NotTo(HaveOccurred())
+
+		bindingUnstructured, err := client.
+			Resource(v1beta1.GroupVersionResource).
+			Namespace("bar").
+			Get(context.Background(), "foo", v1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(bindingUnstructured.GetAnnotations()[apis.MappingAnnotationKey]).To(Equal(string(data)))
+		Expect(bindingUnstructured.GetAnnotations()["foo"]).To(Equal("bar"))
+
+		bindingUnstructured, err = client.
+			Resource(v1alpha1.GroupVersionResource).
+			Namespace("bar").
+			Get(context.Background(), "foo", v1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(bindingUnstructured.GetAnnotations()[apis.MappingAnnotationKey]).To(Equal(string(data)))
+		Expect(bindingUnstructured.GetAnnotations()["foo"]).To(Equal("bar"))
+	})
+
+	It("should ignore irrelevant bindings", func() {
+		mapping.Name = "spam.foo.bar"
+		scheme := runtime.NewScheme()
+		Expect(v1beta1.AddToScheme(scheme)).NotTo(HaveOccurred())
+		Expect(v1alpha1.AddToScheme(scheme)).NotTo(HaveOccurred())
+		client := fake.NewSimpleDynamicClient(scheme, &binding1, &binding2, &mapping)
+
+		err := Serialize(context.Background(), &mapping, client, lookup)
+		Expect(err).NotTo(HaveOccurred())
+
+		bindingUnstructured, err := client.
+			Resource(v1beta1.GroupVersionResource).
+			Namespace("bar").
+			Get(context.Background(), "foo", v1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(bindingUnstructured.GetAnnotations()).NotTo(HaveKey(apis.MappingAnnotationKey))
+
+		bindingUnstructured, err = client.
+			Resource(v1alpha1.GroupVersionResource).
+			Namespace("bar").
+			Get(context.Background(), "foo", v1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(bindingUnstructured.GetAnnotations()).NotTo(HaveKey(apis.MappingAnnotationKey))
+	})
+
+	It("should use the resource field from the service binding when available", func() {
+		mapping.Name = "spam.foo.bar"
+		scheme := runtime.NewScheme()
+		Expect(v1beta1.AddToScheme(scheme)).NotTo(HaveOccurred())
+		Expect(v1alpha1.AddToScheme(scheme)).NotTo(HaveOccurred())
+
+		binding2.Spec.Application.Kind = ""
+		binding2.Spec.Application.Resource = "baz"
+
+		client := fake.NewSimpleDynamicClient(scheme, &binding2, &mapping)
+
+		err := Serialize(context.Background(), &mapping, client, lookup)
+		Expect(err).NotTo(HaveOccurred())
+
+		bindingUnstructured, err := client.
+			Resource(v1alpha1.GroupVersionResource).
+			Namespace("bar").
+			Get(context.Background(), "foo", v1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(bindingUnstructured.GetAnnotations()).NotTo(HaveKey(apis.MappingAnnotationKey))
+	})
+})
diff --git a/pkg/reconcile/pipeline/context/impl_test.go b/pkg/reconcile/pipeline/context/impl_test.go
index d529d452..b9e59bbd 100644
--- a/pkg/reconcile/pipeline/context/impl_test.go
+++ b/pkg/reconcile/pipeline/context/impl_test.go
@@ -1077,10 +1077,130 @@ var _ = Describe("Context", func() {
 			Expect(err).NotTo(HaveOccurred())
 			Expect(mapping.Volume).To(Equal([]string{"spec", "template", "spec", "volumes"}))
 			Expect(mapping.Containers).To(HaveLen(2))
+			for _, container := range mapping.Containers {
+				Expect(container.Env).To(Equal([]string{"env"}))
+				Expect(container.EnvFrom).To(Equal([]string{"envFrom"}))
+				Expect(container.Name).To(Equal([]string{"name"}))
+				Expect(container.VolumeMounts).To(Equal([]string{"volumeMounts"}))
+			}
+		})
+
+		It("should use mapping in annotation", func() {
+			ref := bindingapi.Application{
+				Ref: bindingapi.Ref{
+					Group:   "app",
+					Version: "v1",
+					Kind:    "Foo",
+					Name:    "app1",
+				},
+			}
+			gvr := schema.GroupVersionResource{Group: "app", Version: "v1", Resource: "foos"}
+
+			mappingSpec := v1beta1.ClusterWorkloadResourceMapping{
+				Spec: v1beta1.ClusterWorkloadResourceMappingSpec{
+					Versions: []v1beta1.ClusterWorkloadResourceMappingTemplate{
+						{
+							Version: "*",
+							Containers: []v1beta1.ClusterWorkloadResourceMappingContainer{
+								{
+									Path: ".spec.template.spec.containers",
+								},
+							},
+						},
+					},
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "foos.app",
+				},
+				TypeMeta: metav1.TypeMeta{
+					Kind:       v1beta1.WorkloadResourceMappingGroupVersionKind.Kind,
+					APIVersion: "servicebinding.io/v1beta1",
+				},
+			}
+
+			fakeData, err := json.Marshal(&mappingSpec)
+			Expect(err).NotTo(HaveOccurred())
+
+			sb := bindingapi.ServiceBinding{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "sb1",
+					Namespace: "ns1",
+					Annotations: map[string]string{
+						apis.MappingAnnotationKey: string(fakeData),
+					},
+				},
+				Spec: bindingapi.ServiceBindingSpec{
+					Application: ref,
+				},
+			}
+
+			u := v1beta1.ClusterWorkloadResourceMapping{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "foos.app",
+				},
+				Spec: v1beta1.ClusterWorkloadResourceMappingSpec{
+					Versions: []v1beta1.ClusterWorkloadResourceMappingTemplate{
+						v1beta1.DefaultTemplate,
+					},
+				},
+			}
+			Expect(err).NotTo(HaveOccurred())
+
+			scheme := runtime.NewScheme()
+			Expect(v1beta1.AddToScheme(scheme)).NotTo(HaveOccurred())
+			client := fake.NewSimpleDynamicClient(scheme, &u)
+			authClient := &fakeauth.FakeAuthorizationV1{}
+
+			ctx, err := Provider(client, authClient.SubjectAccessReviews(), typeLookup).Get(&sb)
+			Expect(err).NotTo(HaveOccurred())
+
+			mapping, err := ctx.WorkloadResourceTemplate(&gvr, "")
+			Expect(err).NotTo(HaveOccurred())
+			Expect(mapping.Volume).To(Equal([]string{"spec", "template", "spec", "volumes"}))
+			Expect(mapping.Containers).To(HaveLen(1))
 			Expect(mapping.Containers[0].Env).To(Equal([]string{"env"}))
 			Expect(mapping.Containers[0].EnvFrom).To(Equal([]string{"envFrom"}))
 			Expect(mapping.Containers[0].Name).To(Equal([]string{"name"}))
 			Expect(mapping.Containers[0].VolumeMounts).To(Equal([]string{"volumeMounts"}))
 		})
 	})
+
+	Describe("CleanAnnotations", func() {
+		var (
+			sb  *bindingapi.ServiceBinding
+			ctx pipeline.Context
+			err error
+		)
+
+		BeforeEach(func() {
+			sb = &bindingapi.ServiceBinding{}
+			ctx, err = Provider(nil, nil, nil).Get(sb)
+			Expect(err).NotTo(HaveOccurred())
+		})
+
+		It("should clean mapping annotations when present", func() {
+			sb.SetAnnotations(map[string]string{
+				apis.MappingAnnotationKey: "foo",
+			})
+			Expect(ctx.CleanAnnotations()).To(BeTrue())
+			Expect(sb.GetAnnotations()).To(BeEmpty())
+		})
+
+		It("should not touch other annotations present", func() {
+			sb.SetAnnotations(map[string]string{
+				apis.MappingAnnotationKey: "foo",
+				"spam":                    "eggs",
+			})
+			Expect(ctx.CleanAnnotations()).To(BeTrue())
+			Expect(sb.GetAnnotations()).To(Equal(map[string]string{"spam": "eggs"}))
+		})
+
+		It("should return false when mapping annotations are not present", func() {
+			sb.SetAnnotations(map[string]string{
+				"spam": "eggs",
+			})
+			Expect(ctx.CleanAnnotations()).To(BeFalse())
+			Expect(sb.GetAnnotations()).To(Equal(map[string]string{"spam": "eggs"}))
+		})
+	})
 })
diff --git a/pkg/reconcile/pipeline/context/spec_binding_impl_test.go b/pkg/reconcile/pipeline/context/spec_binding_impl_test.go
index 2c8e6895..000f104e 100644
--- a/pkg/reconcile/pipeline/context/spec_binding_impl_test.go
+++ b/pkg/reconcile/pipeline/context/spec_binding_impl_test.go
@@ -932,6 +932,45 @@ var _ = Describe("Spec API Context", func() {
 		})
 
 	})
+
+	Describe("CleanAnnotations", func() {
+		var (
+			sb  *specapi.ServiceBinding
+			ctx pipeline.Context
+			err error
+		)
+
+		BeforeEach(func() {
+			sb = &specapi.ServiceBinding{}
+			ctx, err = Provider(nil, nil, nil).Get(sb)
+			Expect(err).NotTo(HaveOccurred())
+		})
+
+		It("should clean mapping annotations when present", func() {
+			sb.SetAnnotations(map[string]string{
+				apis.MappingAnnotationKey: "foo",
+			})
+			Expect(ctx.CleanAnnotations()).To(BeTrue())
+			Expect(sb.GetAnnotations()).To(BeEmpty())
+		})
+
+		It("should not touch other annotations present", func() {
+			sb.SetAnnotations(map[string]string{
+				apis.MappingAnnotationKey: "foo",
+				"spam":                    "eggs",
+			})
+			Expect(ctx.CleanAnnotations()).To(BeTrue())
+			Expect(sb.GetAnnotations()).To(Equal(map[string]string{"spam": "eggs"}))
+		})
+
+		It("should return false when mapping annotations are not present", func() {
+			sb.SetAnnotations(map[string]string{
+				"spam": "eggs",
+			})
+			Expect(ctx.CleanAnnotations()).To(BeFalse())
+			Expect(sb.GetAnnotations()).To(Equal(map[string]string{"spam": "eggs"}))
+		})
+	})
 })
 
 func scheme(objs ...runtime.Object) *runtime.Scheme {
diff --git a/pkg/reconcile/pipeline/handler/project/impl_test.go b/pkg/reconcile/pipeline/handler/project/impl_test.go
index b1ef9958..c40a4122 100644
--- a/pkg/reconcile/pipeline/handler/project/impl_test.go
+++ b/pkg/reconcile/pipeline/handler/project/impl_test.go
@@ -777,249 +777,271 @@ var _ = Describe("Unbind handler", func() {
 	BeforeEach(func() {
 		mockCtrl = gomock.NewController(GinkgoT())
 		ctx = mocks.NewMockContext(mockCtrl)
-		ctx.EXPECT().UnbindRequested().Return(true)
-		ctx.EXPECT().StopProcessing()
 	})
 
 	AfterEach(func() {
 		mockCtrl.Finish()
 	})
 
-	It("should stop processing when there is error getting applications", func() {
-		ctx.EXPECT().Applications().Return(nil, errors.New("foo"))
-		project.Unbind(ctx)
-	})
-	It("should stop processing when there are no applications", func() {
-		ctx.EXPECT().Applications().Return([]pipeline.Application{}, nil)
+	It("should not unbind if unbinding is not requested", func() {
+		ctx.EXPECT().UnbindRequested().Return(false)
 		project.Unbind(ctx)
 	})
-	Context("successful processing", func() {
-		var (
-			deploymentsUnstructured    []*unstructured.Unstructured
-			deploymentsUnstructuredOld []*unstructured.Unstructured
-			secretName                 string
-			bindingName                string
-		)
 
+	Context("normal processing", func() {
 		BeforeEach(func() {
-			var apps []pipeline.Application
-			secretName = "secret1"
-			bindingName = "binding1"
-			ctx.EXPECT().BindingSecretName().Return(secretName)
-			ctx.EXPECT().BindingName().Return(bindingName)
-			ctx.EXPECT().EnvBindings().Return([]*pipeline.EnvBinding{})
-			d1 := deployment("d1", []corev1.Container{
-				{
-					Image: "foo",
-				},
-			})
-			d2 := deployment("d2", []corev1.Container{
-				{
-					Image: "foo2",
-					EnvFrom: []corev1.EnvFromSource{
-						{
-							SecretRef: &corev1.SecretEnvSource{
-								LocalObjectReference: corev1.LocalObjectReference{
-									Name: "bla",
+			ctx.EXPECT().UnbindRequested().Return(true)
+		})
+
+		It("should stop processing when there is error getting applications", func() {
+			ctx.EXPECT().Applications().Return(nil, errors.New("foo"))
+			ctx.EXPECT().StopProcessing()
+			project.Unbind(ctx)
+		})
+
+		It("should stop processing when there are no applications", func() {
+			ctx.EXPECT().Applications().Return([]pipeline.Application{}, nil)
+			ctx.EXPECT().StopProcessing()
+			project.Unbind(ctx)
+		})
+
+		Context("successful processing", func() {
+			var (
+				deploymentsUnstructured    []*unstructured.Unstructured
+				deploymentsUnstructuredOld []*unstructured.Unstructured
+				secretName                 string
+				bindingName                string
+			)
+
+			BeforeEach(func() {
+				var apps []pipeline.Application
+				secretName = "secret1"
+				bindingName = "binding1"
+				ctx.EXPECT().IsRemoved().AnyTimes().Return(true)
+				ctx.EXPECT().BindingSecretName().Return(secretName)
+				ctx.EXPECT().BindingName().Return(bindingName)
+				ctx.EXPECT().EnvBindings().Return([]*pipeline.EnvBinding{})
+				d1 := deployment("d1", []corev1.Container{
+					{
+						Image: "foo",
+					},
+				})
+				d2 := deployment("d2", []corev1.Container{
+					{
+						Image: "foo2",
+						EnvFrom: []corev1.EnvFromSource{
+							{
+								SecretRef: &corev1.SecretEnvSource{
+									LocalObjectReference: corev1.LocalObjectReference{
+										Name: "bla",
+									},
 								},
 							},
-						},
-						{
-							SecretRef: &corev1.SecretEnvSource{
-								LocalObjectReference: corev1.LocalObjectReference{
-									Name: secretName,
+							{
+								SecretRef: &corev1.SecretEnvSource{
+									LocalObjectReference: corev1.LocalObjectReference{
+										Name: secretName,
+									},
 								},
 							},
 						},
 					},
-				},
-			})
-			d3 := deployment("d2", []corev1.Container{
-				{
-					Image: "foo2",
-					EnvFrom: []corev1.EnvFromSource{
-						{
-							SecretRef: &corev1.SecretEnvSource{
-								LocalObjectReference: corev1.LocalObjectReference{
-									Name: secretName,
+				})
+				d3 := deployment("d2", []corev1.Container{
+					{
+						Image: "foo2",
+						EnvFrom: []corev1.EnvFromSource{
+							{
+								SecretRef: &corev1.SecretEnvSource{
+									LocalObjectReference: corev1.LocalObjectReference{
+										Name: secretName,
+									},
 								},
 							},
 						},
 					},
-				},
-			})
-			d4 := deployment("d1", []corev1.Container{
-				{
-					Image: "foo",
-					VolumeMounts: []corev1.VolumeMount{
-						{
-							Name:      bindingName,
-							MountPath: "/bla",
+				})
+				d4 := deployment("d1", []corev1.Container{
+					{
+						Image: "foo",
+						VolumeMounts: []corev1.VolumeMount{
+							{
+								Name:      bindingName,
+								MountPath: "/bla",
+							},
 						},
 					},
-				},
-			})
-			d5 := deployment("d1", []corev1.Container{
-				{
-					Image: "foo",
-					VolumeMounts: []corev1.VolumeMount{
-						{
-							Name:      bindingName,
-							MountPath: "/bla",
-						},
-						{
-							Name:      "bla",
-							MountPath: "/bla2",
+				})
+				d5 := deployment("d1", []corev1.Container{
+					{
+						Image: "foo",
+						VolumeMounts: []corev1.VolumeMount{
+							{
+								Name:      bindingName,
+								MountPath: "/bla",
+							},
+							{
+								Name:      "bla",
+								MountPath: "/bla2",
+							},
 						},
 					},
-				},
-			})
-			d6 := deployment("d1", []corev1.Container{
-				{
-					Image: "foo",
-				},
-			})
-			d6.Spec.Template.Spec.Volumes = []corev1.Volume{
-				{
-					Name: bindingName,
-					VolumeSource: corev1.VolumeSource{
-						Secret: &corev1.SecretVolumeSource{
-							SecretName: secretName,
+				})
+				d6 := deployment("d1", []corev1.Container{
+					{
+						Image: "foo",
+					},
+				})
+				d6.Spec.Template.Spec.Volumes = []corev1.Volume{
+					{
+						Name: bindingName,
+						VolumeSource: corev1.VolumeSource{
+							Secret: &corev1.SecretVolumeSource{
+								SecretName: secretName,
+							},
 						},
 					},
-				},
-			}
-			d7 := deployment("d1", []corev1.Container{
-				{
-					Image: "foo",
-				},
-			})
-			d7.Spec.Template.Spec.Volumes = []corev1.Volume{
-				{
-					Name: bindingName,
-					VolumeSource: corev1.VolumeSource{
-						Secret: &corev1.SecretVolumeSource{
-							SecretName: secretName,
+				}
+				d7 := deployment("d1", []corev1.Container{
+					{
+						Image: "foo",
+					},
+				})
+				d7.Spec.Template.Spec.Volumes = []corev1.Volume{
+					{
+						Name: bindingName,
+						VolumeSource: corev1.VolumeSource{
+							Secret: &corev1.SecretVolumeSource{
+								SecretName: secretName,
+							},
 						},
 					},
-				},
-				{
-					Name: "foo",
-					VolumeSource: corev1.VolumeSource{
-						Secret: &corev1.SecretVolumeSource{
-							SecretName: "foo",
+					{
+						Name: "foo",
+						VolumeSource: corev1.VolumeSource{
+							Secret: &corev1.SecretVolumeSource{
+								SecretName: "foo",
+							},
 						},
 					},
-				},
-			}
-			for _, d := range []*appsv1.Deployment{d1, d2, d3, d4, d5, d6, d7} {
-				u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d)
-				if err != nil {
-					Fail(err.Error())
-				}
-				res := &unstructured.Unstructured{Object: u}
-				deploymentsUnstructured = append(deploymentsUnstructured, res)
-				deploymentsUnstructuredOld = append(deploymentsUnstructuredOld, res.DeepCopy())
-				app := mocks.NewMockApplication(mockCtrl)
-				containers, _, _ := converter.NestedResources(&corev1.Container{}, u, strings.Split("spec.template.spec.containers", ".")...)
-				var metaContainers []pipeline.MetaContainer
-				for _, c := range containers {
-					name, _, _ := unstructured.NestedString(c, "name")
-					metaContainers = append(metaContainers, pipeline.MetaContainer{
-						Data:        c,
-						Name:        name,
-						Env:         []string{"env"},
-						EnvFrom:     []string{"envFrom"},
-						VolumeMount: []string{"volumeMounts"},
-					})
 				}
-				template := pipeline.MetaPodSpec{
-					Containers: metaContainers,
-					Volume:     strings.Split("spec.template.spec.volumes", "."),
-					Data:       u,
+				for _, d := range []*appsv1.Deployment{d1, d2, d3, d4, d5, d6, d7} {
+					u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d)
+					if err != nil {
+						Fail(err.Error())
+					}
+					res := &unstructured.Unstructured{Object: u}
+					deploymentsUnstructured = append(deploymentsUnstructured, res)
+					deploymentsUnstructuredOld = append(deploymentsUnstructuredOld, res.DeepCopy())
+					app := mocks.NewMockApplication(mockCtrl)
+					containers, _, _ := converter.NestedResources(&corev1.Container{}, u, strings.Split("spec.template.spec.containers", ".")...)
+					var metaContainers []pipeline.MetaContainer
+					for _, c := range containers {
+						name, _, _ := unstructured.NestedString(c, "name")
+						metaContainers = append(metaContainers, pipeline.MetaContainer{
+							Data:        c,
+							Name:        name,
+							Env:         []string{"env"},
+							EnvFrom:     []string{"envFrom"},
+							VolumeMount: []string{"volumeMounts"},
+						})
+					}
+					template := pipeline.MetaPodSpec{
+						Containers: metaContainers,
+						Volume:     strings.Split("spec.template.spec.volumes", "."),
+						Data:       u,
+					}
+					app.EXPECT().BindablePods().Return(&template, nil)
+					apps = append(apps, app)
 				}
-				app.EXPECT().BindablePods().Return(&template, nil)
-				apps = append(apps, app)
-			}
 
-			ctx.EXPECT().Applications().Return(apps, nil)
-		})
-		It("should remove secret refs", func() {
-			project.Unbind(ctx)
-			Expect(deploymentsUnstructured[0]).To(Equal(deploymentsUnstructuredOld[0]))
-			Expect(deploymentsUnstructured[1]).NotTo(Equal(deploymentsUnstructuredOld[1]))
-			Expect(deploymentsUnstructured[2]).NotTo(Equal(deploymentsUnstructuredOld[2]))
-			Expect(deploymentsUnstructured[3]).NotTo(Equal(deploymentsUnstructuredOld[3]))
-			Expect(deploymentsUnstructured[4]).NotTo(Equal(deploymentsUnstructuredOld[4]))
-			Expect(deploymentsUnstructured[5]).NotTo(Equal(deploymentsUnstructuredOld[5]))
-			Expect(deploymentsUnstructured[6]).NotTo(Equal(deploymentsUnstructuredOld[6]))
-			d2 := deployment("d2", []corev1.Container{
-				{
-					Image: "foo2",
-					EnvFrom: []corev1.EnvFromSource{
-						{
-							SecretRef: &corev1.SecretEnvSource{
-								LocalObjectReference: corev1.LocalObjectReference{
-									Name: "bla",
+				ctx.EXPECT().Applications().Return(apps, nil)
+			})
+			It("should remove secret refs", func() {
+				ctx.EXPECT().CleanAnnotations().Return(false)
+				ctx.EXPECT().StopProcessing()
+				project.Unbind(ctx)
+				Expect(deploymentsUnstructured[0]).To(Equal(deploymentsUnstructuredOld[0]))
+				Expect(deploymentsUnstructured[1]).NotTo(Equal(deploymentsUnstructuredOld[1]))
+				Expect(deploymentsUnstructured[2]).NotTo(Equal(deploymentsUnstructuredOld[2]))
+				Expect(deploymentsUnstructured[3]).NotTo(Equal(deploymentsUnstructuredOld[3]))
+				Expect(deploymentsUnstructured[4]).NotTo(Equal(deploymentsUnstructuredOld[4]))
+				Expect(deploymentsUnstructured[5]).NotTo(Equal(deploymentsUnstructuredOld[5]))
+				Expect(deploymentsUnstructured[6]).NotTo(Equal(deploymentsUnstructuredOld[6]))
+				d2 := deployment("d2", []corev1.Container{
+					{
+						Image: "foo2",
+						EnvFrom: []corev1.EnvFromSource{
+							{
+								SecretRef: &corev1.SecretEnvSource{
+									LocalObjectReference: corev1.LocalObjectReference{
+										Name: "bla",
+									},
 								},
 							},
 						},
 					},
-				},
-			})
-			d3 := deployment("d2", []corev1.Container{
-				{
-					Image:   "foo2",
-					EnvFrom: []v1.EnvFromSource{},
-				},
-			})
-			d4 := deployment("d1", []corev1.Container{
-				{
-					Image:        "foo",
-					VolumeMounts: []v1.VolumeMount{},
-				},
-			})
-			d5 := deployment("d1", []corev1.Container{
-				{
-					Image: "foo",
-					VolumeMounts: []corev1.VolumeMount{
-						{
-							Name:      "bla",
-							MountPath: "/bla2",
+				})
+				d3 := deployment("d2", []corev1.Container{
+					{
+						Image:   "foo2",
+						EnvFrom: []v1.EnvFromSource{},
+					},
+				})
+				d4 := deployment("d1", []corev1.Container{
+					{
+						Image:        "foo",
+						VolumeMounts: []v1.VolumeMount{},
+					},
+				})
+				d5 := deployment("d1", []corev1.Container{
+					{
+						Image: "foo",
+						VolumeMounts: []corev1.VolumeMount{
+							{
+								Name:      "bla",
+								MountPath: "/bla2",
+							},
 						},
 					},
-				},
-			})
-			d6 := deployment("d1", []corev1.Container{
-				{
-					Image: "foo",
-				},
-			})
-			d6.Spec.Template.Spec.Volumes = []corev1.Volume{}
-			d7 := deployment("d1", []corev1.Container{
-				{
-					Image: "foo",
-				},
-			})
-			d7.Spec.Template.Spec.Volumes = []corev1.Volume{
-				{
-					Name: "foo",
-					VolumeSource: corev1.VolumeSource{
-						Secret: &corev1.SecretVolumeSource{
-							SecretName: "foo",
+				})
+				d6 := deployment("d1", []corev1.Container{
+					{
+						Image: "foo",
+					},
+				})
+				d6.Spec.Template.Spec.Volumes = []corev1.Volume{}
+				d7 := deployment("d1", []corev1.Container{
+					{
+						Image: "foo",
+					},
+				})
+				d7.Spec.Template.Spec.Volumes = []corev1.Volume{
+					{
+						Name: "foo",
+						VolumeSource: corev1.VolumeSource{
+							Secret: &corev1.SecretVolumeSource{
+								SecretName: "foo",
+							},
 						},
 					},
-				},
-			}
-			for i, expectedDeployment := range []*appsv1.Deployment{nil, d2, d3, d4, d5, d6, d7} {
-				if expectedDeployment == nil {
-					continue
 				}
-				d := &appsv1.Deployment{}
-				err := runtime.DefaultUnstructuredConverter.FromUnstructured(deploymentsUnstructured[i].Object, d)
-				Expect(err).NotTo(HaveOccurred())
-				Expect(d.Spec.Template).To(Equal(expectedDeployment.Spec.Template))
-			}
+				for i, expectedDeployment := range []*appsv1.Deployment{nil, d2, d3, d4, d5, d6, d7} {
+					if expectedDeployment == nil {
+						continue
+					}
+					d := &appsv1.Deployment{}
+					err := runtime.DefaultUnstructuredConverter.FromUnstructured(deploymentsUnstructured[i].Object, d)
+					Expect(err).NotTo(HaveOccurred())
+					Expect(d.Spec.Template).To(Equal(expectedDeployment.Spec.Template))
+				}
+			})
+			It("should re-queue the service binding when annotations were cleaned", func() {
+				ctx.EXPECT().CleanAnnotations().Return(true)
+				ctx.EXPECT().StopProcessing().Times(0)
+				ctx.EXPECT().RetryProcessing(nil)
+				project.Unbind(ctx)
+			})
 		})
 	})
 })
diff --git a/test/acceptance/features/steps/app.py b/test/acceptance/features/steps/app.py
index 82b47be4..5583b46f 100644
--- a/test/acceptance/features/steps/app.py
+++ b/test/acceptance/features/steps/app.py
@@ -58,3 +58,16 @@ def resource_jsonpath_value(context, json_path, res_name, json_value=""):
     (crdName, name) = res_name.split("/")
     polling2.poll(lambda: openshift.get_resource_info_by_jsonpath(crdName, name, context.namespace.name, json_path) == json_value,
                   step=5, timeout=800, ignore_exceptions=(json.JSONDecodeError,))
+
+
+@step(u'jsonpath "{json_path}" on "{res_name}" should contain "{json_value}"')
+def resource_jsonpath_contains(context, json_path, res_name, json_value):
+    openshift = Openshift()
+    json_path = substitute_scenario_id(context, json_path)
+    res_name = substitute_scenario_id(context, res_name)
+    json_value = json.loads(substitute_scenario_id(context, json_value))
+    (crdName, name) = res_name.split("/")
+    actual_value = polling2.poll(lambda: openshift.get_resource_info_by_jsonpath(crdName, name, context.namespace.name, json_path),
+                                 step=5, timeout=800, ignore_exceptions=(json.JSONDecodeError))
+    data = json.loads(actual_value)
+    assert json_value in data, "Expected {str(json_value)} in {str(data)}"
diff --git a/test/acceptance/features/workloadResourceMapping.feature b/test/acceptance/features/workloadResourceMapping.feature
index ff882716..5c17be52 100644
--- a/test/acceptance/features/workloadResourceMapping.feature
+++ b/test/acceptance/features/workloadResourceMapping.feature
@@ -414,7 +414,10 @@ Feature: Bind services to workloads based on workload resource mapping
                     key:  password
             """
         Then Service Binding is ready
-        And jsonpath "{.spec.containerSpecs[0].envData}" on "notpodspecs/$scenario_id-npc" should return "[{"name":"BINDING_TYPE","valueFrom":{"secretKeyRef":{"key":"type","name":"$scenario_id-secret"}}},{"name":"BINDING_USERNAME","valueFrom":{"secretKeyRef":{"key":"username","name":"$scenario_id-secret"}}},{"name":"BINDING_PASSWORD","valueFrom":{"secretKeyRef":{"key":"password","name":"$scenario_id-secret"}}},{"name":"SERVICE_BINDING_ROOT","value":"/bindings"}]"
+        And jsonpath "{.spec.containerSpecs[0].envData}" on "notpodspecs/$scenario_id-npc" should contain "{"name":"BINDING_TYPE","valueFrom":{"secretKeyRef":{"key":"type","name":"$scenario_id-secret"}}}"
+        And jsonpath "{.spec.containerSpecs[0].envData}" on "notpodspecs/$scenario_id-npc" should contain "{"name":"BINDING_USERNAME","valueFrom":{"secretKeyRef":{"key":"username","name":"$scenario_id-secret"}}}"
+        And jsonpath "{.spec.containerSpecs[0].envData}" on "notpodspecs/$scenario_id-npc" should contain "{"name":"BINDING_PASSWORD","valueFrom":{"secretKeyRef":{"key":"password","name":"$scenario_id-secret"}}}"
+        And jsonpath "{.spec.containerSpecs[0].envData}" on "notpodspecs/$scenario_id-npc" should contain "{"name":"SERVICE_BINDING_ROOT","value":"/bindings"}"
 
     @spec
     Scenario: Projecting values into a non-PodSpec resource for a specific API version
@@ -779,3 +782,303 @@ Feature: Bind services to workloads based on workload resource mapping
                       - name: .metadata.name
             """
         Then Error message is thrown
+
+    @spec
+    Scenario: SPEC Altering a workload mapping should project values where there were previously none
+        Given The Secret is present
+            """
+            apiVersion: v1
+            kind: Secret
+            metadata:
+                name: $scenario_id-secret
+            stringData:
+                username: foo
+                password: bar
+                type: db
+            """
+        And The Custom Resource is present
+            """
+            apiVersion: "stable.example.com/v1"
+            kind: NotPodSpec
+            metadata:
+                name: $scenario_id-npc
+            spec:
+                containerSpecs:
+                  - id: $scenario_id-id
+                initContainerSpecs:
+                  - id: $scenario_id-init
+            """
+        And The Workload Resource Mapping is present
+            """
+            apiVersion: servicebinding.io/v1beta1
+            kind: ClusterWorkloadResourceMapping
+            metadata:
+                name: notpodspecs.stable.example.com
+            spec:
+                versions:
+                  - version: "*"
+                    volumes: .spec.volumeData
+                    containers:
+                      - path: .spec.containerSpecs[*]
+                        name: .id
+                        env: .envData
+                        volumeMounts: .volumeEntries
+            """
+        And Service Binding is applied
+            """
+            apiVersion: servicebinding.io/v1beta1
+            kind: ServiceBinding
+            metadata:
+                name: $scenario_id-binding
+            spec:
+                service:
+                    name: $scenario_id-secret
+                    kind: Secret
+                    apiVersion: v1
+                workload:
+                    apiVersion: stable.example.com/v1
+                    kind: NotPodSpec
+                    name: $scenario_id-npc
+            """
+        And Service Binding is ready
+        And jsonpath "{.spec.containerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        And jsonpath "{.spec.initContainerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return no value
+        When The Workload Resource Mapping is present
+            """
+            apiVersion: servicebinding.io/v1beta1
+            kind: ClusterWorkloadResourceMapping
+            metadata:
+                name: notpodspecs.stable.example.com
+            spec:
+                versions:
+                  - version: "*"
+                    volumes: .spec.volumeData
+                    containers:
+                      - path: .spec.containerSpecs[*]
+                        name: .id
+                        env: .envData
+                        volumeMounts: .volumeEntries
+                      - path: .spec.initContainerSpecs[*]
+                        name: .id
+                        env: .envData
+                        volumeMounts: .volumeEntries
+            """
+        Then Service Binding is ready
+        And jsonpath "{.spec.containerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        And jsonpath "{.spec.initContainerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+
+    @spec
+    Scenario: SPEC Altering a workload mapping should remove stale projections
+        Given The Secret is present
+            """
+            apiVersion: v1
+            kind: Secret
+            metadata:
+                name: $scenario_id-secret
+            stringData:
+                username: foo
+                password: bar
+                type: db
+            """
+        And The Custom Resource is present
+            """
+            apiVersion: "stable.example.com/v1"
+            kind: NotPodSpec
+            metadata:
+                name: $scenario_id-npc
+            spec:
+                containerSpecs:
+                  - id: $scenario_id-id
+                initContainerSpecs:
+                  - id: $scenario_id-init
+            """
+        And Service Binding is applied
+            """
+            apiVersion: servicebinding.io/v1beta1
+            kind: ServiceBinding
+            metadata:
+                name: $scenario_id-binding
+            spec:
+                service:
+                    name: $scenario_id-secret
+                    kind: Secret
+                    apiVersion: v1
+                workload:
+                    apiVersion: stable.example.com/v1
+                    kind: NotPodSpec
+                    name: $scenario_id-npc
+            """
+        And Service Binding is ready
+        And jsonpath "{.spec.containerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        And jsonpath "{.spec.initContainerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        When The Workload Resource Mapping is present
+            """
+            apiVersion: servicebinding.io/v1beta1
+            kind: ClusterWorkloadResourceMapping
+            metadata:
+                name: notpodspecs.stable.example.com
+            spec:
+                versions:
+                  - version: "*"
+                    volumes: .spec.volumeData
+                    containers:
+                      - path: .spec.containerSpecs[*]
+                        name: .id
+                        env: .envData
+                        volumeMounts: .volumeEntries
+            """
+        Then Service Binding is ready
+        And jsonpath "{.spec.containerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        And jsonpath "{.spec.initContainerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[]"
+
+    Scenario: Altering a workload mapping should project values where there were previously none
+        Given The Secret is present
+            """
+            apiVersion: v1
+            kind: Secret
+            metadata:
+                name: $scenario_id-secret
+            stringData:
+                username: foo
+                password: bar
+                type: db
+            """
+        And The Custom Resource is present
+            """
+            apiVersion: "stable.example.com/v1"
+            kind: NotPodSpec
+            metadata:
+                name: $scenario_id-npc
+            spec:
+                containerSpecs:
+                  - id: $scenario_id-id
+                initContainerSpecs:
+                  - id: $scenario_id-init
+            """
+        And The Workload Resource Mapping is present
+            """
+            apiVersion: servicebinding.io/v1beta1
+            kind: ClusterWorkloadResourceMapping
+            metadata:
+                name: notpodspecs.stable.example.com
+            spec:
+                versions:
+                  - version: "*"
+                    volumes: .spec.volumeData
+                    containers:
+                      - path: .spec.containerSpecs[*]
+                        name: .id
+                        env: .envData
+                        volumeMounts: .volumeEntries
+            """
+        And Service Binding is applied
+            """
+            apiVersion: binding.operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: $scenario_id-binding
+            spec:
+                bindAsFiles: true
+                application:
+                    name: $scenario_id-npc
+                    group: stable.example.com
+                    version: v1
+                    resource: notpodspecs
+                services:
+                  - version: v1
+                    group: ""
+                    kind: Secret
+                    name: $scenario_id-secret
+            """
+        And Service Binding is ready
+        And jsonpath "{.spec.containerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        And jsonpath "{.spec.initContainerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return no value
+        When The Workload Resource Mapping is present
+            """
+            apiVersion: servicebinding.io/v1beta1
+            kind: ClusterWorkloadResourceMapping
+            metadata:
+                name: notpodspecs.stable.example.com
+            spec:
+                versions:
+                  - version: "*"
+                    volumes: .spec.volumeData
+                    containers:
+                      - path: .spec.containerSpecs[*]
+                        name: .id
+                        env: .envData
+                        volumeMounts: .volumeEntries
+                      - path: .spec.initContainerSpecs[*]
+                        name: .id
+                        env: .envData
+                        volumeMounts: .volumeEntries
+            """
+        Then Service Binding is ready
+        And jsonpath "{.spec.containerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        And jsonpath "{.spec.initContainerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+
+    Scenario: Altering a workload mapping should remove stale projections
+        Given The Secret is present
+            """
+            apiVersion: v1
+            kind: Secret
+            metadata:
+                name: $scenario_id-secret
+            stringData:
+                username: foo
+                password: bar
+                type: db
+            """
+        And The Custom Resource is present
+            """
+            apiVersion: "stable.example.com/v1"
+            kind: NotPodSpec
+            metadata:
+                name: $scenario_id-npc
+            spec:
+                containerSpecs:
+                  - id: $scenario_id-id
+                initContainerSpecs:
+                  - id: $scenario_id-init
+            """
+        And Service Binding is applied
+            """
+            apiVersion: binding.operators.coreos.com/v1alpha1
+            kind: ServiceBinding
+            metadata:
+                name: $scenario_id-binding
+            spec:
+                bindAsFiles: true
+                application:
+                    name: $scenario_id-npc
+                    group: stable.example.com
+                    version: v1
+                    resource: notpodspecs
+                services:
+                  - version: v1
+                    group: ""
+                    kind: Secret
+                    name: $scenario_id-secret
+            """
+        And Service Binding is ready
+        And jsonpath "{.spec.containerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        And jsonpath "{.spec.initContainerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        When The Workload Resource Mapping is present
+            """
+            apiVersion: servicebinding.io/v1beta1
+            kind: ClusterWorkloadResourceMapping
+            metadata:
+                name: notpodspecs.stable.example.com
+            spec:
+                versions:
+                  - version: "*"
+                    volumes: .spec.volumeData
+                    containers:
+                      - path: .spec.containerSpecs[*]
+                        name: .id
+                        env: .envData
+                        volumeMounts: .volumeEntries
+            """
+        Then Service Binding is ready
+        And jsonpath "{.spec.containerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[{"mountPath":"/bindings/$scenario_id-binding","name":"$scenario_id-binding"}]"
+        And jsonpath "{.spec.initContainerSpecs[0].volumeEntries}" on "notpodspecs/$scenario_id-npc" should return "[]"