Repository: tektoncd/operator
Pull request #1465 (2023-05-02 06:38:42)

allow to disable global pruner job and completely disable pruner feature
# Changes

Without this PR, If we disable global pruner jobs (`config.pruner.schedule=""`), disables all the jobs, even though there is  a namespace with custom schedule with an annotation (example: `operator.tekton.dev/prune.schedule=0 2 * * *`).

With this PR, If we disable global pruner jobs (`config.pruner.schedule=""`), disables only the global job. If there is a namespace with custom schedule with an annotation (example: `operator.tekton.dev/prune.schedule=0 2 * * *`), there will be a running job for that. To disable the pruner feature completely, user has to add `config.pruner.disabled=true`

* Fixes: [SRVKP-1764](https://issues.redhat.com/browse/SRVKP-1764)

# Submitter Checklist

These are the criteria that every PR should meet, please check them off as you
review them:

- [x] Run `make test lint` before submitting a PR
- [ ] Includes [tests](https://github.com/tektoncd/community/blob/master/standards.md#principles) (if functionality changed/added)
- [x] Includes [docs](https://github.com/tektoncd/community/blob/master/standards.md#principles) (if user facing)
- [x] Commit messages follow [commit message best practices](https://github.com/tektoncd/community/blob/master/standards.md#commit-messages)

_See [the contribution guide](https://github.com/tektoncd/operator/blob/master/CONTRIBUTING.md) for more details._

# Release Notes

```release-note
To disable the pruner feature completely, update `config.pruner.disabled=true`
```



tekton-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 **savitaashture** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @savitaashture` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://prow.tekton.dev/command-help?repo=tektoncd%2Foperator).

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

- **[OWNERS](https://github.com/tektoncd/operator/blob/main/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":["savitaashture"]} -->
jkandasa: /cc @vdemeester 
tekton-robot: The following is the coverage report on the affected files.
Say `/test pull-tekton-operator-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[pkg/apis/operator/v1alpha1/tektonconfig_defaults.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1465/pull-tekton-operator-go-coverage/1653287890311974912/artifacts/line-cov.html#file0) | 86.4% | 100.0% | 13.6
[pkg/apis/operator/v1alpha1/tektonconfig_types.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1465/pull-tekton-operator-go-coverage/1653287890311974912/artifacts/line-cov.html#file1) | 12.5% | 0.0% | -12.5
[pkg/apis/operator/v1alpha1/tektonconfig_validation.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1465/pull-tekton-operator-go-coverage/1653287890311974912/artifacts/line-cov.html#file2) | 86.5% | 86.1% | -0.4
[pkg/reconciler/common/prune.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1465/pull-tekton-operator-go-coverage/1653287890311974912/artifacts/line-cov.html#file3) | 90.1% | 89.4% | -0.7

tekton-robot: @jkandasa: 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
--- | --- | --- | --- | ---
pull-tekton-operator-unit-tests | 354bba2b628307ec9403c6b91ec9c424476b0a5b | [link](https://prow.tekton.dev/view/gs/tekton-prow/pr-logs/pull/tektoncd_operator/1465/pull-tekton-operator-unit-tests/1653287890190340096) | true | `/test pull-tekton-operator-unit-tests`

[Full PR test history](https://prow.tekton.dev/pr-history?org=tektoncd&repo=operator&pr=1465). [Your PR dashboard](https://prow.tekton.dev/pr?query=is%3Apr%20state%3Aopen%20author%3Ajkandasa).

<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 -->
tekton-robot: The following is the coverage report on the affected files.
Say `/test pull-tekton-operator-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[pkg/apis/operator/v1alpha1/tektonconfig_defaults.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1465/pull-tekton-operator-go-coverage/1653296743774687232/artifacts/line-cov.html#file0) | 86.4% | 100.0% | 13.6
[pkg/apis/operator/v1alpha1/tektonconfig_types.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1465/pull-tekton-operator-go-coverage/1653296743774687232/artifacts/line-cov.html#file1) | 12.5% | 0.0% | -12.5
[pkg/apis/operator/v1alpha1/tektonconfig_validation.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1465/pull-tekton-operator-go-coverage/1653296743774687232/artifacts/line-cov.html#file2) | 86.5% | 86.1% | -0.4
[pkg/reconciler/common/prune.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1465/pull-tekton-operator-go-coverage/1653296743774687232/artifacts/line-cov.html#file4) | 90.1% | 89.4% | -0.7

tekton-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/tektoncd/operator/pull/1465#pullrequestreview-1408889707" title="Approved">vdemeester</a>*

The full list of commands accepted by this bot can be found [here](https://prow.tekton.dev/command-help?repo=tektoncd%2Foperator).

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

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

- ~~[OWNERS](https://github.com/tektoncd/operator/blob/main/OWNERS)~~ [vdemeester]

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

Patch:
diff --git a/components.yaml b/components.yaml
index 7cd50f25e..41c9bd5f6 100644
--- a/components.yaml
+++ b/components.yaml
@@ -12,7 +12,7 @@ pipeline:
   version: v0.47.0
 pipelines-as-code:
   github: openshift-pipelines/pipelines-as-code
-  version: v0.18.0
+  version: v0.19.0
 results:
   github: tektoncd/results
   version: v0.5.0
diff --git a/config/openshift/base/operator.yaml b/config/openshift/base/operator.yaml
index ed5aa191a..325682304 100644
--- a/config/openshift/base/operator.yaml
+++ b/config/openshift/base/operator.yaml
@@ -63,7 +63,7 @@ spec:
         - name: IMAGE_PIPELINES_PROXY
           value: ko://github.com/tektoncd/operator/cmd/openshift/proxy-webhook
         - name: IMAGE_JOB_PRUNER_TKN
-          value: gcr.io/tekton-releases/dogfooding/tkn@sha256:025de221fb059ca24a3b2d988889ea34bce48dc76c0cf0d6b4499edb8c21325f
+          value: gcr.io/tekton-releases/dogfooding/tkn@sha256:33acedd164a2c520e42d2ec401a9fc46c2c304a4714b6728aa2b8957bbf02dc6
         - name: METRICS_DOMAIN
           value: tekton.dev/operator
         - name: VERSION
@@ -81,15 +81,15 @@ spec:
         - name: CONFIG_OBSERVABILITY_NAME
           value: tekton-config-observability
         - name: IMAGE_HUB_TEKTON_HUB_DB
-          value: registry.redhat.io/rhel8/postgresql-13@sha256:d3872cca802419af86eab8b0a566ae94c85cec6a8786a3b97f43ca5fcb095248
+          value: registry.redhat.io/rhel8/postgresql-13@sha256:f5143ea832f3426515910665f4dc6ecca137499c9ac324871b6feb0282493bcc
         - name: IMAGE_ADDONS_PARAM_BUILDER_IMAGE
-          value: registry.redhat.io/rhel8/buildah@sha256:ac0b8714cc260c94435cab46fe41b3de0ccbc3d93e38c395fa9d52ac49e521fe
+          value: registry.redhat.io/rhel8/buildah@sha256:00795fafdab9bbaa22cd29d1faa1a01e604e4884a2c935c1bf8e3d1f0ad1c084
         - name: IMAGE_ADDONS_PARAM_KN_IMAGE
-          value: registry.redhat.io/openshift-serverless-1/client-kn-rhel8@sha256:89fb53858bb93ada958faff761915f45d01e3364b5e5df4ba05b01c4b8cde587
+          value: registry.redhat.io/openshift-serverless-1/client-kn-rhel8@sha256:3fcf747549ad9a8957291f1e943316230112f2d87b2fcc050342f5c9a58889fc
         - name: IMAGE_ADDONS_MVN_SETTINGS
-          value: registry.redhat.io/ubi8/ubi-minimal@sha256:6910799b75ad41f00891978575a0d955be2f800c51b955af73926e7ab59a41c3
+          value: registry.redhat.io/ubi8/ubi-minimal@sha256:987ae81ce046652ee4a2c3df54dad5e82faa1b078dab5d09f7cfaae11784ed30
         - name: IMAGE_ADDONS_SKOPEO_COPY
-          value: registry.redhat.io/rhel8/skopeo@sha256:52579594e3057f7a5f7a4665d0b5ceeb1204e24e020a45553d6aae7c6820bb31
+          value: registry.redhat.io/rhel8/skopeo@sha256:264dbe1ff1f9c3c4ad6e2e2f3ad8e29db57c48445efd8bc59eb081d6e48394b6
         - name: IMAGE_ADDONS_GENERATE
           value: registry.redhat.io/ocp-tools-4-tech-preview/source-to-image-rhel8@sha256:98d8cb3a255641ca6a1bce854e5e2460c20de9fb9b28e3cc67eb459f122873dd
         - name: IMAGE_ADDONS_GEN_ENV_FILE
diff --git a/docs/TektonChain.md b/docs/TektonChain.md
index a78f2f282..330edc191 100644
--- a/docs/TektonChain.md
+++ b/docs/TektonChain.md
@@ -8,13 +8,9 @@ weight: 9
 
 TektonChain custom resource allows user to install and manage [Tekton Chains][chains].
 
-TektonChain is an optional component and currently cannot be installed through TektonConfig. It has to be installed separately.
+It is recommended to install the component through [TektonConfig](./TektonConfig.md).
 
-To install TektonChain on your cluster follow steps as given below:
-
-- Make sure Tekton Pipelines is installed on your cluster, using the Operator.
-
-- Create the TektonChain CR.
+- TektonChain CR is as below
 
     - On Kubernetes, TektonChain CR is as below:
 
@@ -88,11 +84,14 @@ spec:
   storage.docdb.url: #value
   storage.grafeas.projectid: #value
   storage.grafeas.noteid: #value
+  storage.grafeas.notehint: #value
   builder.id: #value
   signers.x509.fulcio.enabled: #value (boolean - true/false)
   signers.x509.fulcio.address: #value
   signers.x509.fulcio.issuer: #value
   signers.x509.fulcio.provider: #value
+  signers.x509.identity.token.file: #value
+  signers.x509.tuf.mirror.url: #value
   signers.kms.kmsref: #value
   signers.kms.kmsref.auth.address: #value
   signers.kms.kmsref.auth.token: #value
diff --git a/docs/TektonConfig.md b/docs/TektonConfig.md
index f0fc2ab99..25cd24de5 100644
--- a/docs/TektonConfig.md
+++ b/docs/TektonConfig.md
@@ -14,6 +14,7 @@ Operator provides support for installing and managing following operator compone
 - [TektonPipeline](./TektonPipeline.md)
 - [TektonTrigger](./TektonTrigger.md)
 
+
 Other than the above components depending on the platform operator also provides support for
 - On both Kubernetes and OpenShift
     - [TektonChain](./TektonChain.md)
@@ -50,7 +51,14 @@ The TektonConfig CR provides the following features
       metrics.taskrun.level: taskrun
       require-git-ssh-secret-known-hosts: false
       running-in-environment-with-injected-sidecars: true
+      performance:
+        disable-ha: false
+        buckets: 1
+        threads-per-controller: 2
+        kube-api-qps: 5.0
+        kube-api-burst: 10
     pruner:
+      disabled: false
       resources:
       - taskrun
       - pipelinerun
@@ -75,8 +83,8 @@ By default, namespace would be `tekton-pipelines` for Kubernetes and `openshift-
 
 This allows user to choose which all components to install on the cluster.
 There are 3 profiles available:
-- `all`: This profile will install all components (TektonPipeline and TektonTrigger)
-- `basic`:  This profile will install only TektonPipeline and TektonTrigger component
+- `all`: This profile will install all components (TektonPipeline, TektonTrigger and TektonChain)
+- `basic`:  This profile will install only TektonPipeline, TektonTrigger and TektonChain component
 - `lite`: This profile will install only TektonPipeline component
 
 On Kubernetes, `all` profile will install `TektonDashboard` and on OpenShift `TektonAddon` will be installed.
@@ -134,6 +142,12 @@ pipeline:
   metrics.taskrun.level: taskrun
   require-git-ssh-secret-known-hosts: false
   running-in-environment-with-injected-sidecars: true
+  performance:
+    disable-ha: false
+    buckets: 1
+    threads-per-controller: 2
+    kube-api-qps: 5.0
+    kube-api-burst: 10
 ```
 
 ### Pruner
@@ -142,6 +156,7 @@ Pruner provides auto clean up feature for the Tekton resources.
 Example:
 ```yaml
 pruner:
+  disabled: false
   resources:
     - taskrun
     - pipelinerun
@@ -149,14 +164,31 @@ pruner:
   keep-since: 1440
   schedule: "* * * * *"
 ```
+- `disabled` : if the value set as `true`, pruner feature will be disabled (default: `false`)
 - `prune-per-resource`: if the value set as `true` (default value `false`), the `keep` and `keep-since` applied to each resource. example: `tkn pipelinerun delete --pipeline=my-pipeline --keep=10`
 - `resources`: supported resources for auto prune are `taskrun` and `pipelinerun`
 - `keep`: maximum number of resources to keep while deleting or removing resources
 - `keep-since`: retain the resources younger than the specified value in minutes
 - `schedule`: how often to clean up resources. User can understand the schedule syntax [here][schedule].
 
-This is an `Optional` section.
-
+> ### Note:
+> if `disabled: false` and `schedule: ` with empty value, global pruner job will be disabled.
+> however, if there is a prune schedule (`operator.tekton.dev/prune.schedule`) annotation present with a value in a namespace. a namespace wide pruner jobs will be created
+
+#### Pruner Namespace annotations
+By default pruner job will be created from the global pruner config (`spec.pruner`), though user can customize a pruner config to a specific namespace with the following annotations. If some of the annotations are not present or has invalid value, for that value, falls back to global value or skipped the namespace.
+- `operator.tekton.dev/prune.skip` - pruner job will be skipped to a namespace, if the value set as `true`
+- `operator.tekton.dev/prune.schedule` - pruner job will be created on a specific schedule
+- `operator.tekton.dev/prune.keep` - maximum number of resources will be kept
+- `operator.tekton.dev/prune.keep-since` - retain the resources younger than the specified value in minutes
+- `operator.tekton.dev/prune.prune-per-resource` - the `keep` and `keep-since` applied to each resource
+- `operator.tekton.dev/prune.resources` - can be `taskrun` and/or `pipelinerun`, both value can be specified with comma separated. example: `taskrun,pipelinerun`
+- `operator.tekton.dev/prune.strategy` - allowed values: either `keep` or `keep-since`
+
+> ### Note: 
+> if a global value is not present the following values will be consider as default value <br> 
+> `resources: pipelinerun` <br>
+> `keep: 100` <br>
 ### Addon
 
 TektonAddon install some resources along with Tekton Pipelines on the cluster. This provides few ClusterTasks, PipelineTemplates.
diff --git a/docs/TektonPipeline.md b/docs/TektonPipeline.md
index e8490d332..203b8e1a8 100644
--- a/docs/TektonPipeline.md
+++ b/docs/TektonPipeline.md
@@ -32,6 +32,12 @@ spec:
   require-git-ssh-secret-known-hosts: false
   running-in-environment-with-injected-sidecars: true
   scope-when-expressions-to-task: false
+  performance:
+    disable-ha: false
+    buckets: 1
+    threads-per-controller: 2
+    kube-api-qps: 5.0
+    kube-api-burst: 10
 ```
 You can install this component using [TektonConfig](./TektonConfig.md) by choosing appropriate `profile`.
 
@@ -165,3 +171,35 @@ default is the only option available. If no sink is specified, no CloudEvent is
 Task declares but that a TaskRun does not explicitly provide.
 
 [Pipeline]:https://github.com/tektoncd/pipeline
+
+### Performance Properties
+```yaml
+spec:
+  # omitted other fields ...
+  performance:
+    disable-ha: false
+    buckets: 1
+    threads-per-controller: 2
+    kube-api-qps: 5.0
+    kube-api-burst: 10
+```
+These fields are optional and there is no default values. If user passes them, operator will include most of fields into the deployment `tekton-pipelines-controller` under the container `tekton-pipelines-controller` as arguments(duplicate name? No, container and deployment has the same name), otherwise pipelines controller's default values will be considered. and `buckets` field is updated into `config-leader-election` config-map under the namespace `tekton-pipelines`.
+
+A high level descriptions are given here. To get the detailed information please visit pipelines documentation, [High Availability Support](https://tekton.dev/docs/pipelines/enabling-ha/), and [Performance Configuration](https://tekton.dev/docs/pipelines/tekton-controller-performance-configuration/)
+
+
+* `disable-ha` - enable or disable ha feature, defaults in pipelines controller is `disable-ha=false`
+* `buckets` - buckets is the number of buckets used to partition key space of each reconciler. If this number is M and the replica number of the controller is N, the N replicas will compete for the M buckets. The owner of a bucket will take care of the reconciling for the keys partitioned into that bucket. The maximum value of `buckets` at this time is `10`. default value in pipeline controller is `1`
+* `threads-per-controller` - is the number of threads(aka worker) to use when processing the pipelines controller's workqueue, default value in pipelines controller is `2`
+* `kube-api-qps` - QPS indicates the maximum QPS to the cluster master from the REST client, default value in pipeline controller is `5.0`
+* `kube-api-burst` - maximum burst for throttle, default value in pipeline controller is `10`
+
+> #### Note:
+>
+> * Pipelines controller deployments `replicas` will not be handled by operator. User has to change the replicas as follows,
+>   ```
+>   kubectl --namespace tekton-pipelines scale deployment tekton-pipelines-controller --replicas=3
+>   ```
+>
+> * `kube-api-qps` and `kube-api-burst` will be multiplied by 2 in pipelines controller. To get the detailed information visit [Performance Configuration](https://tekton.dev/docs/pipelines/tekton-controller-performance-configuration/) guide
+> * if you modify or remove any of the performance properties, `tekton-pipelines-controller` deployment and `config-leader-election` config-map (if `buckets` changed) will be updated, and `tekton-pipelines-controller` pods will be recreated
diff --git a/operatorhub/openshift/config.yaml b/operatorhub/openshift/config.yaml
index 68a7594e4..16cff93de 100644
--- a/operatorhub/openshift/config.yaml
+++ b/operatorhub/openshift/config.yaml
@@ -47,7 +47,8 @@ image-substitutions:
       containerName: openshift-pipelines-operator-lifecycle
       envKeys:
       - IMAGE_PIPELINES_ARG__ENTRYPOINT_IMAGE
-- image: registry.redhat.io/openshift-pipelines/pipelines-git-init-rhel8@
+# add git-init image from 1.10.1 release
+- image: registry.redhat.io/openshift-pipelines/pipelines-git-init-rhel8@sha256:429ba9c10dfcff4ee0154ab75f9bd06fd306f019c0ed0b062ba7c03e56e3e33b
   replaceLocations:
     envTargets:
     - deploymentName: openshift-pipelines-operator
@@ -69,7 +70,7 @@ image-substitutions:
       containerName: openshift-pipelines-operator-lifecycle
       envKeys:
       - IMAGE_PIPELINES_ARG__NOP_IMAGE
-- image: registry.redhat.io/ubi8/ubi-minimal@sha256:6910799b75ad41f00891978575a0d955be2f800c51b955af73926e7ab59a41c3
+- image: registry.redhat.io/ubi8/ubi-minimal@sha256:987ae81ce046652ee4a2c3df54dad5e82faa1b078dab5d09f7cfaae11784ed30
   replaceLocations:
     envTargets:
     - deploymentName: openshift-pipelines-operator
@@ -104,21 +105,21 @@ image-substitutions:
       containerName: openshift-pipelines-operator-lifecycle
       envKeys:
       - IMAGE_TRIGGERS_ARG__EL_IMAGE
-- image: registry.redhat.io/openshift-serverless-1/client-kn-rhel8@sha256:89fb53858bb93ada958faff761915f45d01e3364b5e5df4ba05b01c4b8cde587
+- image: registry.redhat.io/openshift-serverless-1/client-kn-rhel8@sha256:3fcf747549ad9a8957291f1e943316230112f2d87b2fcc050342f5c9a58889fc
   replaceLocations:
     envTargets:
     - deploymentName: openshift-pipelines-operator
       containerName: openshift-pipelines-operator-lifecycle
       envKeys:
       - IMAGE_ADDONS_PARAM_KN_IMAGE
-- image: registry.redhat.io/rhel8/skopeo@sha256:52579594e3057f7a5f7a4665d0b5ceeb1204e24e020a45553d6aae7c6820bb31
+- image: registry.redhat.io/rhel8/skopeo@sha256:264dbe1ff1f9c3c4ad6e2e2f3ad8e29db57c48445efd8bc59eb081d6e48394b6
   replaceLocations:
     envTargets:
     - deploymentName: openshift-pipelines-operator
       containerName: openshift-pipelines-operator-lifecycle
       envKeys:
       - IMAGE_ADDONS_SKOPEO_COPY
-- image: registry.redhat.io/rhel8/buildah@sha256:ac0b8714cc260c94435cab46fe41b3de0ccbc3d93e38c395fa9d52ac49e521fe
+- image: registry.redhat.io/rhel8/buildah@sha256:00795fafdab9bbaa22cd29d1faa1a01e604e4884a2c935c1bf8e3d1f0ad1c084
   replaceLocations:
     envTargets:
     - deploymentName: openshift-pipelines-operator
@@ -134,7 +135,7 @@ image-substitutions:
       - IMAGE_ADDONS_GENERATE
       - IMAGE_ADDONS_GEN_ENV_FILE
 
-- image: registry.redhat.io/ubi8/ubi-minimal@sha256:6910799b75ad41f00891978575a0d955be2f800c51b955af73926e7ab59a41c3
+- image: registry.redhat.io/ubi8/ubi-minimal@sha256:987ae81ce046652ee4a2c3df54dad5e82faa1b078dab5d09f7cfaae11784ed30
   replaceLocations:
     envTargets:
       - deploymentName: openshift-pipelines-operator
@@ -168,7 +169,7 @@ image-substitutions:
         containerName: openshift-pipelines-operator-lifecycle
         envKeys:
           - IMAGE_CHAINS_TEKTON_CHAINS_CONTROLLER
-- image: registry.redhat.io/rhel8/postgresql-13@sha256:d3872cca802419af86eab8b0a566ae94c85cec6a8786a3b97f43ca5fcb095248
+- image: registry.redhat.io/rhel8/postgresql-13@sha256:f5143ea832f3426515910665f4dc6ecca137499c9ac324871b6feb0282493bcc
   replaceLocations:
     envTargets:
       - deploymentName: openshift-pipelines-operator
diff --git a/pkg/apis/operator/v1alpha1/const.go b/pkg/apis/operator/v1alpha1/const.go
index cb012d421..8b2bb5193 100644
--- a/pkg/apis/operator/v1alpha1/const.go
+++ b/pkg/apis/operator/v1alpha1/const.go
@@ -54,6 +54,10 @@ const (
 	Reinstalling   = "reinstalling"
 
 	RequeueDelay = 10 * time.Second
+
+	// pruner default schedule, used in auto generate tektonConfig
+	PrunerDefaultSchedule = "0 8 * * *"
+	PrunerDefaultKeep     = uint(100)
 )
 
 var (
@@ -90,6 +94,11 @@ var (
 		"pipelinerun",
 	}
 
+	// pruner default resource, used in auto generate tektonConfig
+	PruningDefaultResources = []string{
+		"pipelinerun",
+	}
+
 	AddonParams = map[string]ParamValue{
 		ClusterTasksParam:      defaultParamValue,
 		PipelineTemplatesParam: defaultParamValue,
diff --git a/pkg/apis/operator/v1alpha1/tektonchain_types.go b/pkg/apis/operator/v1alpha1/tektonchain_types.go
index d3baa33da..912c7408e 100644
--- a/pkg/apis/operator/v1alpha1/tektonchain_types.go
+++ b/pkg/apis/operator/v1alpha1/tektonchain_types.go
@@ -17,6 +17,10 @@ limitations under the License.
 package v1alpha1
 
 import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	duckv1 "knative.dev/pkg/apis/duck/v1"
 )
@@ -104,9 +108,29 @@ type Chain struct {
 	KMSAuthSpireSock     string `json:"signers.kms.auth.spire.sock,omitempty"`
 	KMSAuthSpireAudience string `json:"signers.kms.auth.spire.audience,omitempty"`
 
-	// transparency config
-	TransparencyConfigEnabled string `json:"transparency.enabled,omitempty"`
-	TransparencyConfigURL     string `json:"transparency.url,omitempty"`
+	TransparencyConfigEnabled BoolValue `json:"transparency.enabled,omitempty"`
+	TransparencyConfigURL     string    `json:"transparency.url,omitempty"`
+}
+
+type BoolValue string
+
+func (bv *BoolValue) UnmarshalJSON(value []byte) error {
+	var a string
+	var b bool
+	if err := json.Unmarshal(value, &a); err == nil {
+		// no error, it's a string
+		*bv = BoolValue(a)
+		return nil
+	} else if err := json.Unmarshal(value, &b); err == nil {
+		// it is a boolean
+		*bv = BoolValue(strconv.FormatBool(b))
+		return nil
+	}
+	return fmt.Errorf("Invalid value")
+}
+
+func (bv BoolValue) MarshalJson() ([]byte, error) {
+	return []byte(bv), nil
 }
 
 // TektonChainStatus defines the observed state of TektonChain
diff --git a/pkg/apis/operator/v1alpha1/tektonchain_validation.go b/pkg/apis/operator/v1alpha1/tektonchain_validation.go
index 121e10501..8a97aa4e7 100644
--- a/pkg/apis/operator/v1alpha1/tektonchain_validation.go
+++ b/pkg/apis/operator/v1alpha1/tektonchain_validation.go
@@ -119,7 +119,7 @@ func (tcs *TektonChainSpec) ValidateChainConfig(path string) (errs *apis.FieldEr
 		errs = errs.Also(apis.ErrInvalidValue(tcs.X509SignerFulcioProvider, path+".signers.x509.fulcio.provider"))
 	}
 
-	if !allowedTransparencyConfigEnabled.Has(tcs.TransparencyConfigEnabled) {
+	if !allowedTransparencyConfigEnabled.Has(string(tcs.TransparencyConfigEnabled)) {
 		errs = errs.Also(apis.ErrInvalidValue(tcs.TransparencyConfigEnabled, path+".transparency.enabled"))
 	}
 
diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go b/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go
index 5e51739ea..173d20123 100644
--- a/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go
+++ b/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go
@@ -18,6 +18,7 @@ package v1alpha1
 
 import (
 	"context"
+	"strings"
 
 	"knative.dev/pkg/ptr"
 )
@@ -59,19 +60,28 @@ func (tc *TektonConfig) SetDefaults(ctx context.Context) {
 		tc.Spec.Platforms.OpenShift = OpenShift{}
 	}
 
-	// before adding webhook we had default value for pruner's keep as 1
-	// but we expect user to define all values now otherwise webhook reject
-	// request so if a user has installed prev version and has not enabled
-	// pruner then `keep` will have a value 1 and after upgrading
-	// to newer version webhook will fail if keep has a value and
-	// other fields are not defined
-	// this handles that case by removing the default for keep if
-	// other pruner fields are not defined
-	if len(tc.Spec.Pruner.Resources) == 0 {
-		tc.Spec.Pruner.Keep = nil
-		tc.Spec.Pruner.Schedule = ""
-	} else if tc.Spec.Pruner.Schedule == "" {
-		tc.Spec.Pruner.Keep = nil
-		tc.Spec.Pruner.Resources = []string{}
+	// earlier pruner was disabled with empty schedule or empty resources
+	// now empty schedule, disables only the global cron job,
+	// if a namespace has prune schedule annotation, a cron job will be created for that
+	// to disable the pruner feature, "disabled" should be set as "true"
+	if !tc.Spec.Pruner.Disabled {
+		// if keep and keep-since is nil, update default keep value
+		if tc.Spec.Pruner.Keep == nil && tc.Spec.Pruner.KeepSince == nil {
+			keep := PrunerDefaultKeep
+			tc.Spec.Pruner.Keep = &keep
+		}
+
+		// if empty resources, update default resources
+		if len(tc.Spec.Pruner.Resources) == 0 {
+			tc.Spec.Pruner.Resources = PruningDefaultResources
+		}
+
+		// trim space and to lower case resource names
+		for index := range tc.Spec.Pruner.Resources {
+			value := tc.Spec.Pruner.Resources[index]
+			value = strings.TrimSpace(value)
+			value = strings.ToLower(value)
+			tc.Spec.Pruner.Resources[index] = value
+		}
 	}
 }
diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_types.go b/pkg/apis/operator/v1alpha1/tektonconfig_types.go
index 66c77a9b6..70f14b048 100644
--- a/pkg/apis/operator/v1alpha1/tektonconfig_types.go
+++ b/pkg/apis/operator/v1alpha1/tektonconfig_types.go
@@ -49,6 +49,8 @@ func (tp *TektonConfig) GetStatus() TektonComponentStatus {
 
 // Prune defines the pruner
 type Prune struct {
+	// enable or disable pruner feature
+	Disabled bool `json:"disabled"`
 	// apply the prune job to the individual resources
 	// +optional
 	PrunePerResource bool `json:"prune-per-resource,omitempty"`
@@ -92,6 +94,9 @@ type TektonConfigSpec struct {
 	// Trigger holds the customizable option for triggers component
 	// +optional
 	Trigger Trigger `json:"trigger,omitempty"`
+	// Chain holds the customizable option for chains component
+	// +optional
+	Chain Chain `json:"chain,omitempty"`
 	// Dashboard holds the customizable options for dashboards component
 	// +optional
 	Dashboard Dashboard `json:"dashboard,omitempty"`
diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_validation.go b/pkg/apis/operator/v1alpha1/tektonconfig_validation.go
index 261475eee..739e5630c 100644
--- a/pkg/apis/operator/v1alpha1/tektonconfig_validation.go
+++ b/pkg/apis/operator/v1alpha1/tektonconfig_validation.go
@@ -44,9 +44,8 @@ func (tc *TektonConfig) Validate(ctx context.Context) (errs *apis.FieldError) {
 		}
 	}
 
-	if !tc.Spec.Pruner.IsEmpty() {
-		errs = errs.Also(tc.Spec.Pruner.validate())
-	}
+	// validate pruner specifications
+	errs = errs.Also(tc.Spec.Pruner.validate())
 
 	if !tc.Spec.Addon.IsEmpty() {
 		errs = errs.Also(validateAddonParams(tc.Spec.Addon.Params, "spec.addon.params"))
@@ -64,6 +63,11 @@ func (tc *TektonConfig) Validate(ctx context.Context) (errs *apis.FieldError) {
 func (p Prune) validate() *apis.FieldError {
 	var errs *apis.FieldError
 
+	// if pruner job disable no validation required
+	if p.Disabled {
+		return errs
+	}
+
 	if len(p.Resources) != 0 {
 		for i, r := range p.Resources {
 			if !isValueInArray(PruningResource, r) {
@@ -84,10 +88,6 @@ func (p Prune) validate() *apis.FieldError {
 		errs = errs.Also(apis.ErrInvalidValue(*p.KeepSince, "spec.pruner.keep-since"))
 	}
 
-	if p.Schedule == "" {
-		errs = errs.Also(apis.ErrMissingField("spec.pruner.schedule"))
-	}
-
 	return errs
 }
 
diff --git a/pkg/apis/operator/v1alpha1/tektonpipeline_types.go b/pkg/apis/operator/v1alpha1/tektonpipeline_types.go
index d480a0514..e04ccb31c 100644
--- a/pkg/apis/operator/v1alpha1/tektonpipeline_types.go
+++ b/pkg/apis/operator/v1alpha1/tektonpipeline_types.go
@@ -105,6 +105,8 @@ type PipelineProperties struct {
 	OptionalPipelineProperties `json:",inline"`
 	// +optional
 	Resolvers `json:",inline"`
+	// +optional
+	Performance PipelinePerformanceProperties `json:"performance,omitempty"`
 }
 
 // OptionalPipelineProperties defines the fields which are to be
@@ -146,3 +148,37 @@ type ResolversConfig struct {
 	GitResolverConfig     map[string]string `json:"git-resolver-config,omitempty"`
 	ClusterResolverConfig map[string]string `json:"cluster-resolver-config,omitempty"`
 }
+
+// PipelinePerformanceProperties defines the fields which are configurable
+// to tune the performance of pipelines controller
+type PipelinePerformanceProperties struct {
+	// +optional
+	PipelinePerformanceLeaderElectionConfig `json:",inline"`
+	// +optional
+	PipelineDeploymentPerformanceArgs `json:",inline"`
+}
+
+// performance configurations to tune the performance of the pipeline controller
+// these properties will be added/updated in the ConfigMap(config-leader-election)
+// https://tekton.dev/docs/pipelines/enabling-ha/
+type PipelinePerformanceLeaderElectionConfig struct {
+	Buckets *uint `json:"buckets,omitempty"`
+}
+
+// performance configurations to tune the performance of the pipeline controller
+// these properties will be added/updated as arguments in pipeline controller deployment
+// https://tekton.dev/docs/pipelines/tekton-controller-performance-configuration/
+type PipelineDeploymentPerformanceArgs struct {
+	// if it is true, disables the HA feature
+	DisableHA bool `json:"disable-ha"`
+
+	// The number of workers to use when processing the pipelines controller's work queue
+	ThreadsPerController *int `json:"threads-per-controller,omitempty"`
+
+	// queries per second (QPS) and burst to the master from rest API client
+	// actually the number multiplied by 2
+	// https://github.com/pierretasci/pipeline/blob/05d67e427c722a2a57e58328d7097e21429b7524/cmd/controller/main.go#L85-L87
+	// defaults: https://github.com/tektoncd/pipeline/blob/34618964300620dca44d10a595e4af84e9903a55/vendor/k8s.io/client-go/rest/config.go#L45-L46
+	KubeApiQPS   *float32 `json:"kube-api-qps,omitempty"`
+	KubeApiBurst *int     `json:"kube-api-burst,omitempty"`
+}
diff --git a/pkg/apis/operator/v1alpha1/tektonpipeline_validation.go b/pkg/apis/operator/v1alpha1/tektonpipeline_validation.go
index a3269f512..46b9983ec 100644
--- a/pkg/apis/operator/v1alpha1/tektonpipeline_validation.go
+++ b/pkg/apis/operator/v1alpha1/tektonpipeline_validation.go
@@ -21,9 +21,14 @@ import (
 	"fmt"
 
 	"github.com/tektoncd/pipeline/pkg/apis/config"
+	"k8s.io/apimachinery/pkg/util/sets"
 	"knative.dev/pkg/apis"
 )
 
+var (
+	validatePipelineAllowedApiFields = sets.NewString("", config.AlphaAPIFields, config.BetaAPIFields, config.StableAPIFields)
+)
+
 func (tp *TektonPipeline) Validate(ctx context.Context) (errs *apis.FieldError) {
 
 	if apis.IsInDelete(ctx) {
@@ -44,11 +49,10 @@ func (tp *TektonPipeline) Validate(ctx context.Context) (errs *apis.FieldError)
 
 func (p *PipelineProperties) validate(path string) (errs *apis.FieldError) {
 
-	if p.EnableApiFields != "" {
-		if p.EnableApiFields != config.StableAPIFields && p.EnableApiFields != config.AlphaAPIFields {
-			errs = errs.Also(apis.ErrInvalidValue(p.EnableApiFields, path+".enable-api-fields"))
-		}
+	if !validatePipelineAllowedApiFields.Has(p.EnableApiFields) {
+		errs = errs.Also(apis.ErrInvalidValue(p.EnableApiFields, fmt.Sprintf("%s.enable-api-fields", path)))
 	}
+
 	if p.DefaultTimeoutMinutes != nil {
 		if *p.DefaultTimeoutMinutes == 0 {
 			errs = errs.Also(apis.ErrInvalidValue(p.DefaultTimeoutMinutes, path+".default-timeout-minutes"))
@@ -61,5 +65,22 @@ func (p *PipelineProperties) validate(path string) (errs *apis.FieldError) {
 		}
 	}
 
+	// validate performance properties
+	errs = errs.Also(p.Performance.validate(fmt.Sprintf("%s.performance", path)))
+
+	return errs
+}
+
+func (prof *PipelinePerformanceProperties) validate(path string) *apis.FieldError {
+	var errs *apis.FieldError
+
+	bucketsPath := fmt.Sprintf("%s.buckets", path)
+	// minimum and maximum allowed buckets value
+	if prof.Buckets != nil {
+		if *prof.Buckets < 1 || *prof.Buckets > 10 {
+			errs = errs.Also(apis.ErrOutOfBoundsValue(*prof.Buckets, 1, 10, bucketsPath))
+		}
+	}
+
 	return errs
 }
diff --git a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go
index a18cb43c3..aae1e2575 100644
--- a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go
@@ -532,6 +532,37 @@ func (in *Pipeline) DeepCopy() *Pipeline {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PipelineDeploymentPerformanceArgs) DeepCopyInto(out *PipelineDeploymentPerformanceArgs) {
+	*out = *in
+	if in.ThreadsPerController != nil {
+		in, out := &in.ThreadsPerController, &out.ThreadsPerController
+		*out = new(int)
+		**out = **in
+	}
+	if in.KubeApiQPS != nil {
+		in, out := &in.KubeApiQPS, &out.KubeApiQPS
+		*out = new(float32)
+		**out = **in
+	}
+	if in.KubeApiBurst != nil {
+		in, out := &in.KubeApiBurst, &out.KubeApiBurst
+		*out = new(int)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PipelineDeploymentPerformanceArgs.
+func (in *PipelineDeploymentPerformanceArgs) DeepCopy() *PipelineDeploymentPerformanceArgs {
+	if in == nil {
+		return nil
+	}
+	out := new(PipelineDeploymentPerformanceArgs)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PipelineMetricsProperties) DeepCopyInto(out *PipelineMetricsProperties) {
 	*out = *in
@@ -548,6 +579,45 @@ func (in *PipelineMetricsProperties) DeepCopy() *PipelineMetricsProperties {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PipelinePerformanceLeaderElectionConfig) DeepCopyInto(out *PipelinePerformanceLeaderElectionConfig) {
+	*out = *in
+	if in.Buckets != nil {
+		in, out := &in.Buckets, &out.Buckets
+		*out = new(uint)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PipelinePerformanceLeaderElectionConfig.
+func (in *PipelinePerformanceLeaderElectionConfig) DeepCopy() *PipelinePerformanceLeaderElectionConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(PipelinePerformanceLeaderElectionConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PipelinePerformanceProperties) DeepCopyInto(out *PipelinePerformanceProperties) {
+	*out = *in
+	in.PipelinePerformanceLeaderElectionConfig.DeepCopyInto(&out.PipelinePerformanceLeaderElectionConfig)
+	in.PipelineDeploymentPerformanceArgs.DeepCopyInto(&out.PipelineDeploymentPerformanceArgs)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PipelinePerformanceProperties.
+func (in *PipelinePerformanceProperties) DeepCopy() *PipelinePerformanceProperties {
+	if in == nil {
+		return nil
+	}
+	out := new(PipelinePerformanceProperties)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PipelineProperties) DeepCopyInto(out *PipelineProperties) {
 	*out = *in
@@ -604,6 +674,7 @@ func (in *PipelineProperties) DeepCopyInto(out *PipelineProperties) {
 	out.PipelineMetricsProperties = in.PipelineMetricsProperties
 	in.OptionalPipelineProperties.DeepCopyInto(&out.OptionalPipelineProperties)
 	in.Resolvers.DeepCopyInto(&out.Resolvers)
+	in.Performance.DeepCopyInto(&out.Performance)
 	return
 }
 
@@ -1061,6 +1132,7 @@ func (in *TektonConfigSpec) DeepCopyInto(out *TektonConfigSpec) {
 	in.Hub.DeepCopyInto(&out.Hub)
 	in.Pipeline.DeepCopyInto(&out.Pipeline)
 	out.Trigger = in.Trigger
+	in.Chain.DeepCopyInto(&out.Chain)
 	out.Dashboard = in.Dashboard
 	if in.Params != nil {
 		in, out := &in.Params, &out.Params
diff --git a/pkg/reconciler/common/prune.go b/pkg/reconciler/common/prune.go
index 29b2b8ac3..73961ff71 100644
--- a/pkg/reconciler/common/prune.go
+++ b/pkg/reconciler/common/prune.go
@@ -192,7 +192,7 @@ func (pr *Pruner) reconcileCronJobs() error {
 	pruneConfigsMap := make(map[string][]pruneConfig)
 
 	// verify prune job enabled in TektonConfig CR
-	if pr.tektonConfig.Spec.Pruner.Schedule != "" && len(pr.tektonConfig.Spec.Pruner.Resources) != 0 {
+	if !pr.tektonConfig.Spec.Pruner.Disabled {
 		// collect namespace details
 		namespaceList, err := pr.kubeClientset.CoreV1().Namespaces().List(pr.ctx, metav1.ListOptions{})
 		if err != nil {
@@ -297,6 +297,21 @@ func (pr *Pruner) getPruneConfig(namespace *corev1.Namespace) *pruneConfig {
 		return nil
 	}
 
+	// if the global schedule is disabled and there is no prune schedule annotation present in a namespace
+	// skip that namespace
+	if defaultPruneConfig.Schedule == "" && pr.getMapString(annotations, pruneAnnotationSchedule, "") == "" {
+		return nil
+	}
+
+	// update missing values from defaults
+	if defaultPruneConfig.Keep == nil && defaultPruneConfig.KeepSince == nil {
+		keep := v1alpha1.PrunerDefaultKeep
+		defaultPruneConfig.Keep = &keep
+	}
+	if len(defaultPruneConfig.Resources) == 0 {
+		defaultPruneConfig.Resources = v1alpha1.PruningDefaultResources
+	}
+
 	// update keep and keep-since based on the strategy
 	pruneStrategy := pr.getMapString(annotations, pruneAnnotationStrategy, "")
 	// update keep value
diff --git a/pkg/reconciler/common/utils.go b/pkg/reconciler/common/utils.go
index eaa5eb158..5092e4509 100644
--- a/pkg/reconciler/common/utils.go
+++ b/pkg/reconciler/common/utils.go
@@ -17,6 +17,7 @@ limitations under the License.
 package common
 
 import (
+	"encoding/json"
 	"fmt"
 
 	mf "github.com/manifestival/manifestival"
@@ -53,3 +54,12 @@ func FetchVersionFromConfigMap(manifest mf.Manifest, configMapName string) (stri
 
 	return "", configMapError
 }
+
+// converts struct to map with json encoding
+func StructToMap(in, out interface{}) error {
+	data, err := json.Marshal(in)
+	if err != nil {
+		return err
+	}
+	return json.Unmarshal(data, out)
+}
diff --git a/pkg/reconciler/kubernetes/tektondashboard/reconcile.go b/pkg/reconciler/kubernetes/tektondashboard/reconcile.go
index 74795cabb..588dd55a7 100644
--- a/pkg/reconciler/kubernetes/tektondashboard/reconcile.go
+++ b/pkg/reconciler/kubernetes/tektondashboard/reconcile.go
@@ -108,6 +108,11 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, td *v1alpha1.TektonDashb
 	} else {
 		manifest = r.fullaccessManifest
 	}
+
+	// When Tekton Dashboard is insalled targetNamespace is getting updated with the OwnerRef as TektonDashboard
+	// and hence deleting the component in the integration tests, targetNamespace was getting deleted. Hence
+	// filtering out the namespace here
+	manifest = manifest.Filter(mf.Not(mf.ByKind("Namespace")))
 	if err := r.installerSetClient.MainSet(ctx, td, &manifest, filterAndTransform(r.extension)); err != nil {
 		msg := fmt.Sprintf("Main Reconcilation failed: %s", err.Error())
 		logger.Error(msg)
diff --git a/pkg/reconciler/kubernetes/tektonpipeline/reconcile.go b/pkg/reconciler/kubernetes/tektonpipeline/reconcile.go
index 96b7d9f08..ea1913d31 100644
--- a/pkg/reconciler/kubernetes/tektonpipeline/reconcile.go
+++ b/pkg/reconciler/kubernetes/tektonpipeline/reconcile.go
@@ -98,7 +98,13 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tp *v1alpha1.TektonPipel
 	// Mark PreReconcile Complete
 	tp.Status.MarkPreReconcilerComplete()
 
-	if err := r.installerSetClient.MainSet(ctx, tp, &r.manifest, filterAndTransform(r.extension)); err != nil {
+	// When TektonPipeline component is deleted targetNamespace was getting deleted,
+	// because in pipeline reconciler targetNamespace was updated by adding few labels which
+	// in turn also updated the ownerRef of targetNamespace from TektonConfig to TektonPipeline.
+	// Since namespace is created in TektonConfig reconciler hence deleting TektonPipeline
+	// component should not delete the targetNamespace hence filtering out the namespace here
+	manifest := r.manifest.Filter(mf.Not(mf.ByKind("Namespace")))
+	if err := r.installerSetClient.MainSet(ctx, tp, &manifest, filterAndTransform(r.extension)); err != nil {
 		msg := fmt.Sprintf("Main Reconcilation failed: %s", err.Error())
 		logger.Error(msg)
 		if err == v1alpha1.REQUEUE_EVENT_AFTER {
diff --git a/pkg/reconciler/kubernetes/tektonpipeline/transform.go b/pkg/reconciler/kubernetes/tektonpipeline/transform.go
index d1ffd3636..6e9ab5dbe 100644
--- a/pkg/reconciler/kubernetes/tektonpipeline/transform.go
+++ b/pkg/reconciler/kubernetes/tektonpipeline/transform.go
@@ -18,23 +18,31 @@ package tektonpipeline
 
 import (
 	"context"
+	"fmt"
+	"sort"
 
 	mf "github.com/manifestival/manifestival"
 	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
 	"github.com/tektoncd/operator/pkg/reconciler/common"
 	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektoninstallerset/client"
+	appsv1 "k8s.io/api/apps/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	apimachineryRuntime "k8s.io/apimachinery/pkg/runtime"
 )
 
 const (
 	// Pipelines ConfigMap
-	FeatureFlag           = "feature-flags"
-	ConfigDefaults        = "config-defaults"
-	ConfigMetrics         = "config-observability"
-	ResolverFeatureFlag   = "resolvers-feature-flags"
-	bundleResolverConfig  = "bundleresolver-config"
-	clusterResolverConfig = "cluster-resolver-config"
-	hubResolverConfig     = "hubresolver-config"
-	gitResolverConfig     = "git-resolver-config"
+	FeatureFlag                   = "feature-flags"
+	ConfigDefaults                = "config-defaults"
+	ConfigMetrics                 = "config-observability"
+	ResolverFeatureFlag           = "resolvers-feature-flags"
+	bundleResolverConfig          = "bundleresolver-config"
+	clusterResolverConfig         = "cluster-resolver-config"
+	hubResolverConfig             = "hubresolver-config"
+	gitResolverConfig             = "git-resolver-config"
+	leaderElectionConfig          = "config-leader-election"
+	pipelinesControllerDeployment = "tekton-pipelines-controller"
+	pipelinesControllerContainer  = "tekton-pipelines-controller"
 )
 
 func filterAndTransform(extension common.Extension) client.FilterAndTransform {
@@ -58,6 +66,8 @@ func filterAndTransform(extension common.Extension) client.FilterAndTransform {
 			common.CopyConfigMap(hubResolverConfig, pipeline.Spec.HubResolverConfig),
 			common.CopyConfigMap(clusterResolverConfig, pipeline.Spec.ClusterResolverConfig),
 			common.CopyConfigMap(gitResolverConfig, pipeline.Spec.GitResolverConfig),
+			common.AddConfigMapValues(leaderElectionConfig, pipeline.Spec.Performance.PipelinePerformanceLeaderElectionConfig),
+			updatePerformanceFlagsInDeployment(pipeline),
 		}
 		trns = append(trns, extra...)
 
@@ -67,3 +77,105 @@ func filterAndTransform(extension common.Extension) client.FilterAndTransform {
 		return manifest, nil
 	}
 }
+
+// updates performance flags/args into pipelines controller container
+func updatePerformanceFlagsInDeployment(pipelineCR *v1alpha1.TektonPipeline) mf.Transformer {
+	return func(u *unstructured.Unstructured) error {
+		if u.GetKind() != "Deployment" || u.GetName() != pipelinesControllerDeployment {
+			return nil
+		}
+
+		// holds the flags needs to be added in the container args section
+		flags := map[string]interface{}{}
+
+		// convert struct to map with json tag
+		// so that, we can map the arguments as is
+		performanceSpec := pipelineCR.Spec.Performance
+		if err := common.StructToMap(&performanceSpec.PipelineDeploymentPerformanceArgs, &flags); err != nil {
+			return err
+		}
+
+		// if there is no flags to update, return from here
+		if len(flags) == 0 {
+			return nil
+		}
+
+		// convert unstructured object to deployment
+		dep := &appsv1.Deployment{}
+		err := apimachineryRuntime.DefaultUnstructuredConverter.FromUnstructured(u.Object, dep)
+		if err != nil {
+			return err
+		}
+
+		// include config-leader-election data into deployment pod label
+		// so that pods will be recreated, if there is a change in "buckets"
+		// NOTE: when writing this code replicas can not be handled from installersets,
+		// user needs to scale the deployment manually
+		leaderElectionConfigMapData := map[string]interface{}{}
+		if err = common.StructToMap(&performanceSpec.PipelinePerformanceLeaderElectionConfig, &leaderElectionConfigMapData); err != nil {
+			return err
+		}
+		podLabels := dep.Spec.Template.Labels
+		if podLabels == nil {
+			podLabels = map[string]string{}
+		}
+		// sort data keys in an order, to get the consistent hash value in installerset
+		labelKeys := getSortedKeys(leaderElectionConfigMapData)
+		for _, key := range labelKeys {
+			value := leaderElectionConfigMapData[key]
+			labelKey := fmt.Sprintf("%s.data.%s", leaderElectionConfig, key)
+			podLabels[labelKey] = fmt.Sprintf("%v", value)
+		}
+		dep.Spec.Template.Labels = podLabels
+
+		// sort flag keys in an order, to get the consistent hash value in installerset
+		flagKeys := getSortedKeys(flags)
+		// update performance arguments into target container
+		for containerIndex, container := range dep.Spec.Template.Spec.Containers {
+			if container.Name != pipelinesControllerContainer {
+				continue
+			}
+			for _, flagKey := range flagKeys {
+				// update the arg name with "-" prefix
+				expectedArg := fmt.Sprintf("-%s", flagKey)
+				argStringValue := fmt.Sprintf("%v", flags[flagKey])
+				argUpdated := false
+				for argIndex, existingArg := range container.Args {
+					if existingArg == expectedArg {
+						argValueIndex := argIndex + 1
+						if len(container.Args) > argValueIndex {
+							container.Args[argValueIndex] = argStringValue
+						} else {
+							container.Args = append(container.Args, argStringValue)
+						}
+						argUpdated = true
+						break
+					}
+				}
+				if !argUpdated {
+					container.Args = append(container.Args, expectedArg, argStringValue)
+				}
+			}
+			dep.Spec.Template.Spec.Containers[containerIndex] = container
+		}
+
+		// convert deployment to unstructured object
+		obj, err := apimachineryRuntime.DefaultUnstructuredConverter.ToUnstructured(dep)
+		if err != nil {
+			return err
+		}
+		u.SetUnstructuredContent(obj)
+
+		return nil
+	}
+}
+
+// sort keys in an order, to get the consistent hash value in installerset
+func getSortedKeys(input map[string]interface{}) []string {
+	keys := []string{}
+	for key := range input {
+		keys = append(keys, key)
+	}
+	sort.Strings(keys)
+	return keys
+}
diff --git a/pkg/reconciler/openshift/tektonpipeline/extension.go b/pkg/reconciler/openshift/tektonpipeline/extension.go
index 5d9dd10c2..196006594 100644
--- a/pkg/reconciler/openshift/tektonpipeline/extension.go
+++ b/pkg/reconciler/openshift/tektonpipeline/extension.go
@@ -77,6 +77,9 @@ func (oe openshiftExtension) PreReconcile(ctx context.Context, comp v1alpha1.Tek
 	if err != nil {
 		return err
 	}
+
+	// Filtering out the namespace because it add TektonPipeline as OwnerRef in targetNamespace
+	*manifest = manifest.Filter(mf.Not(mf.ByKind("Namespace")))
 	if err := oe.installerSetClient.PreSet(ctx, comp, manifest, filterAndTransform()); err != nil {
 		return err
 	}
diff --git a/pkg/reconciler/shared/tektonconfig/chain/chain.go b/pkg/reconciler/shared/tektonconfig/chain/chain.go
new file mode 100644
index 000000000..c6b4864cc
--- /dev/null
+++ b/pkg/reconciler/shared/tektonconfig/chain/chain.go
@@ -0,0 +1,151 @@
+/*
+Copyright 2023 The Tekton Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package chain
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strings"
+
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	op "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	apierrs "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"knative.dev/pkg/apis"
+)
+
+func EnsureTektonChainExists(ctx context.Context, clients op.TektonChainInterface, tc *v1alpha1.TektonChain) (*v1alpha1.TektonChain, error) {
+	tcCR, err := GetChain(ctx, clients, v1alpha1.ChainResourceName)
+
+	if err != nil {
+		if !apierrs.IsNotFound(err) {
+			return nil, err
+		}
+		if err := CreateChain(ctx, clients, tc); err != nil {
+			return nil, err
+		}
+		return nil, v1alpha1.RECONCILE_AGAIN_ERR
+	}
+
+	tcCR, err = UpdateChain(ctx, tcCR, tc, clients)
+	if err != nil {
+		return nil, err
+	}
+
+	ready, err := isTektonChainReady(tcCR)
+	if err != nil {
+		return nil, err
+	}
+	if !ready {
+		return nil, v1alpha1.RECONCILE_AGAIN_ERR
+	}
+
+	return tcCR, err
+}
+
+func EnsureTektonChainCRNotExists(ctx context.Context, clients op.TektonChainInterface) error {
+	if _, err := GetChain(ctx, clients, v1alpha1.ChainResourceName); err != nil {
+		if apierrs.IsNotFound(err) {
+			// TektonChain CR is gone, hence return nil
+			return nil
+		}
+		return err
+	}
+	// if the Get was successful, try deleting the CR
+	if err := clients.Delete(ctx, v1alpha1.ChainResourceName, metav1.DeleteOptions{}); err != nil {
+		if apierrs.IsNotFound(err) {
+			// TektonChain CR is gone, hence return nil
+			return nil
+		}
+		return fmt.Errorf("TektonChain %q failed to delete: %v", v1alpha1.ChainResourceName, err)
+	}
+	// if the Delete API call was success,
+	// then return requeue_event
+	// so that in a subsequent reconcile call the absence of the CR is verified by one of the 2 checks above
+	return v1alpha1.RECONCILE_AGAIN_ERR
+}
+
+func GetChain(ctx context.Context, clients op.TektonChainInterface, name string) (*v1alpha1.TektonChain, error) {
+	return clients.Get(ctx, name, metav1.GetOptions{})
+}
+
+func CreateChain(ctx context.Context, clients op.TektonChainInterface, tt *v1alpha1.TektonChain) error {
+	_, err := clients.Create(ctx, tt, metav1.CreateOptions{})
+	return err
+}
+
+func UpdateChain(ctx context.Context, old *v1alpha1.TektonChain, new *v1alpha1.TektonChain, clients op.TektonChainInterface) (*v1alpha1.TektonChain, error) {
+	// if the chain spec is changed then update the instance
+	updated := false
+
+	if new.Spec.TargetNamespace != old.Spec.TargetNamespace {
+		old.Spec.TargetNamespace = new.Spec.TargetNamespace
+		updated = true
+	}
+
+	if !reflect.DeepEqual(old.Spec.Chain, new.Spec.Chain) {
+		old.Spec.Chain = new.Spec.Chain
+		updated = true
+	}
+
+	if !reflect.DeepEqual(old.Spec.Config, new.Spec.Config) {
+		old.Spec.Config = new.Spec.Config
+		updated = true
+	}
+
+	if old.ObjectMeta.OwnerReferences == nil {
+		old.ObjectMeta.OwnerReferences = new.ObjectMeta.OwnerReferences
+		updated = true
+	}
+
+	if updated {
+		_, err := clients.Update(ctx, old, metav1.UpdateOptions{})
+		if err != nil {
+			return nil, err
+		}
+		return nil, v1alpha1.RECONCILE_AGAIN_ERR
+	}
+	return old, nil
+}
+
+// isTektonChainReady will check the status conditions of the TektonChain and return true if the TektonChain is ready.
+func isTektonChainReady(s *v1alpha1.TektonChain) (bool, error) {
+	if s.GetStatus() != nil && s.GetStatus().GetCondition(apis.ConditionReady) != nil {
+		if strings.Contains(s.GetStatus().GetCondition(apis.ConditionReady).Message, v1alpha1.UpgradePending) {
+			return false, v1alpha1.DEPENDENCY_UPGRADE_PENDING_ERR
+		}
+	}
+	return s.Status.IsReady(), nil
+}
+
+func GetTektonChainCR(config *v1alpha1.TektonConfig) *v1alpha1.TektonChain {
+	ownerRef := *metav1.NewControllerRef(config, config.GroupVersionKind())
+	return &v1alpha1.TektonChain{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:            v1alpha1.ChainResourceName,
+			OwnerReferences: []metav1.OwnerReference{ownerRef},
+		},
+		Spec: v1alpha1.TektonChainSpec{
+			CommonSpec: v1alpha1.CommonSpec{
+				TargetNamespace: config.Spec.TargetNamespace,
+			},
+			Config: config.Spec.Config,
+			Chain:  config.Spec.Chain,
+		},
+	}
+}
diff --git a/pkg/reconciler/shared/tektonconfig/controller.go b/pkg/reconciler/shared/tektonconfig/controller.go
index 45dd4f159..40c3ddc33 100644
--- a/pkg/reconciler/shared/tektonconfig/controller.go
+++ b/pkg/reconciler/shared/tektonconfig/controller.go
@@ -26,6 +26,7 @@ import (
 	mf "github.com/manifestival/manifestival"
 	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
 	operatorclient "github.com/tektoncd/operator/pkg/client/injection/client"
+	tektonChaininformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonchain"
 	tektonConfiginformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonconfig"
 	tektonInstallerinformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektoninstallerset"
 	tektonPipelineinformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonpipeline"
@@ -88,6 +89,11 @@ func NewExtensibleController(generator common.ExtensionGenerator) injection.Cont
 			Handler:    controller.HandleAll(impl.EnqueueControllerOf),
 		})
 
+		tektonChaininformer.Get(ctx).Informer().AddEventHandler(cache.FilteringResourceEventHandler{
+			FilterFunc: controller.FilterController(&v1alpha1.TektonConfig{}),
+			Handler:    controller.HandleAll(impl.EnqueueControllerOf),
+		})
+
 		tektonInstallerinformer.Get(ctx).Informer().AddEventHandler(cache.FilteringResourceEventHandler{
 			FilterFunc: controller.FilterController(&v1alpha1.TektonConfig{}),
 			Handler:    controller.HandleAll(impl.EnqueueControllerOf),
diff --git a/pkg/reconciler/shared/tektonconfig/instance.go b/pkg/reconciler/shared/tektonconfig/instance.go
index 52e3d8ed7..1534fd5c9 100644
--- a/pkg/reconciler/shared/tektonconfig/instance.go
+++ b/pkg/reconciler/shared/tektonconfig/instance.go
@@ -99,7 +99,7 @@ func (tc tektonConfig) ensureInstance(ctx context.Context) {
 }
 
 func (tc tektonConfig) createInstance(ctx context.Context) error {
-	pruneKeep := uint(100)
+	pruneKeep := v1alpha1.PrunerDefaultKeep
 	tcCR := &v1alpha1.TektonConfig{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: v1alpha1.ConfigResourceName,
@@ -110,10 +110,11 @@ func (tc tektonConfig) createInstance(ctx context.Context) error {
 				TargetNamespace: tc.namespace,
 			},
 			Pruner: v1alpha1.Prune{
-				Resources: []string{"pipelinerun"},
+				Disabled:  false,
+				Resources: v1alpha1.PruningDefaultResources,
 				Keep:      &pruneKeep,
 				KeepSince: nil,
-				Schedule:  "0 8 * * *",
+				Schedule:  v1alpha1.PrunerDefaultSchedule,
 			},
 		},
 	}
diff --git a/pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go b/pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go
index 194a078ad..fb09b409f 100644
--- a/pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go
+++ b/pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go
@@ -104,6 +104,11 @@ func UpdatePipeline(ctx context.Context, old *v1alpha1.TektonPipeline, new *v1al
 		updated = true
 	}
 
+	if !reflect.DeepEqual(old.Spec.Performance, new.Spec.Performance) {
+		old.Spec.Performance = new.Spec.Performance
+		updated = true
+	}
+
 	if old.ObjectMeta.OwnerReferences == nil {
 		old.ObjectMeta.OwnerReferences = new.ObjectMeta.OwnerReferences
 		updated = true
diff --git a/pkg/reconciler/shared/tektonconfig/tektonconfig.go b/pkg/reconciler/shared/tektonconfig/tektonconfig.go
index fe4465bac..5a23ddfbb 100644
--- a/pkg/reconciler/shared/tektonconfig/tektonconfig.go
+++ b/pkg/reconciler/shared/tektonconfig/tektonconfig.go
@@ -25,6 +25,7 @@ import (
 	clientset "github.com/tektoncd/operator/pkg/client/clientset/versioned"
 	tektonConfigreconciler "github.com/tektoncd/operator/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig"
 	"github.com/tektoncd/operator/pkg/reconciler/common"
+	"github.com/tektoncd/operator/pkg/reconciler/shared/tektonconfig/chain"
 	"github.com/tektoncd/operator/pkg/reconciler/shared/tektonconfig/pipeline"
 	"github.com/tektoncd/operator/pkg/reconciler/shared/tektonconfig/trigger"
 	"k8s.io/apimachinery/pkg/api/errors"
@@ -66,6 +67,9 @@ func (r *Reconciler) FinalizeKind(ctx context.Context, original *v1alpha1.Tekton
 		if err := trigger.EnsureTektonTriggerCRNotExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonTriggers()); err != nil {
 			return err
 		}
+		if err := chain.EnsureTektonChainCRNotExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonChains()); err != nil {
+			return err
+		}
 		if err := pipeline.EnsureTektonPipelineCRNotExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonPipelines()); err != nil {
 			return err
 		}
@@ -143,16 +147,29 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tc *v1alpha1.TektonConfi
 		if _, err := trigger.EnsureTektonTriggerExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonTriggers(), tektontrigger); err != nil {
 			tc.Status.MarkComponentNotReady(fmt.Sprintf("TektonTrigger: %s", err.Error()))
 			return v1alpha1.REQUEUE_EVENT_AFTER
+
+		}
+
+		tektonchain := chain.GetTektonChainCR(tc)
+		if _, err := chain.EnsureTektonChainExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonChains(), tektonchain); err != nil {
+			tc.Status.MarkComponentNotReady(fmt.Sprintf("TektonChain: %s", err.Error()))
+			return v1alpha1.REQUEUE_EVENT_AFTER
 		}
+
 	} else {
 		if err := trigger.EnsureTektonTriggerCRNotExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonTriggers()); err != nil {
 			tc.Status.MarkComponentNotReady(fmt.Sprintf("TektonTrigger: %s", err.Error()))
 			return v1alpha1.REQUEUE_EVENT_AFTER
 		}
+
+		if err := chain.EnsureTektonChainCRNotExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonChains()); err != nil {
+			tc.Status.MarkComponentNotReady(fmt.Sprintf("TektonChain: %s", err.Error()))
+			return v1alpha1.REQUEUE_EVENT_AFTER
+		}
 	}
 
 	// reconcile pruner installerSet
-	if !tc.Spec.Pruner.IsEmpty() {
+	if !tc.Spec.Pruner.Disabled {
 		err := r.reconcilePrunerInstallerSet(ctx, tc)
 		if err != nil {
 			return err


Test patch:
diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_validation_test.go b/pkg/apis/operator/v1alpha1/tektonconfig_validation_test.go
index 30f9529ba..7fb1f6b26 100644
--- a/pkg/apis/operator/v1alpha1/tektonconfig_validation_test.go
+++ b/pkg/apis/operator/v1alpha1/tektonconfig_validation_test.go
@@ -53,7 +53,9 @@ func Test_ValidateTektonConfig_MissingTargetNamespace(t *testing.T) {
 			Name:      "config",
 			Namespace: "namespace",
 		},
-		Spec: TektonConfigSpec{},
+		Spec: TektonConfigSpec{
+			Pruner: Prune{Disabled: true},
+		},
 	}
 
 	err := tc.Validate(context.TODO())
@@ -72,6 +74,7 @@ func Test_ValidateTektonConfig_InvalidProfile(t *testing.T) {
 				TargetNamespace: "namespace",
 			},
 			Profile: "test",
+			Pruner:  Prune{Disabled: true},
 		},
 	}
 
@@ -80,7 +83,6 @@ func Test_ValidateTektonConfig_InvalidProfile(t *testing.T) {
 }
 
 func Test_ValidateTektonConfig_InvalidPruningResource(t *testing.T) {
-
 	tc := &TektonConfig{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "config",
@@ -99,7 +101,7 @@ func Test_ValidateTektonConfig_InvalidPruningResource(t *testing.T) {
 	}
 
 	err := tc.Validate(context.TODO())
-	assert.Equal(t, "expected exactly one, got neither: spec.pruner.keep, spec.pruner.keep-since\ninvalid value: task: spec.pruner.resources[0]\nmissing field(s): spec.pruner.schedule", err.Error())
+	assert.Equal(t, "expected exactly one, got neither: spec.pruner.keep, spec.pruner.keep-since\ninvalid value: task: spec.pruner.resources[0]", err.Error())
 }
 
 func Test_ValidateTektonConfig_MissingKeepKeepsinceSchedule(t *testing.T) {
@@ -121,30 +123,7 @@ func Test_ValidateTektonConfig_MissingKeepKeepsinceSchedule(t *testing.T) {
 	}
 
 	err := tc.Validate(context.TODO())
-	assert.Equal(t, "expected exactly one, got neither: spec.pruner.keep, spec.pruner.keep-since\nmissing field(s): spec.pruner.schedule", err.Error())
-}
-
-func Test_ValidateTektonConfig_MissingSchedule(t *testing.T) {
-	keep := uint(2)
-	tc := &TektonConfig{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      "config",
-			Namespace: "namespace",
-		},
-		Spec: TektonConfigSpec{
-			CommonSpec: CommonSpec{
-				TargetNamespace: "namespace",
-			},
-			Profile: "all",
-			Pruner: Prune{
-				Keep:      &keep,
-				Resources: []string{"taskrun"},
-			},
-		},
-	}
-
-	err := tc.Validate(context.TODO())
-	assert.Equal(t, "missing field(s): spec.pruner.schedule", err.Error())
+	assert.Equal(t, "expected exactly one, got neither: spec.pruner.keep, spec.pruner.keep-since", err.Error())
 }
 
 func Test_ValidateTektonConfig_InvalidAddonParam(t *testing.T) {
@@ -167,6 +146,7 @@ func Test_ValidateTektonConfig_InvalidAddonParam(t *testing.T) {
 					},
 				},
 			},
+			Pruner: Prune{Disabled: true},
 		},
 	}
 
@@ -194,6 +174,7 @@ func Test_ValidateTektonConfig_InvalidAddonParamValue(t *testing.T) {
 					},
 				},
 			},
+			Pruner: Prune{Disabled: true},
 		},
 	}
 
@@ -218,6 +199,7 @@ func Test_ValidateTektonConfig_InvalidPipelineProperties(t *testing.T) {
 					EnableApiFields: "test",
 				},
 			},
+			Pruner: Prune{Disabled: true},
 		},
 	}
 
@@ -242,6 +224,7 @@ func Test_ValidateTektonConfig_InvalidTriggerProperties(t *testing.T) {
 					EnableApiFields: "test",
 				},
 			},
+			Pruner: Prune{Disabled: true},
 		},
 	}
 
diff --git a/pkg/apis/operator/v1alpha1/tektonhub_validation_test.go b/pkg/apis/operator/v1alpha1/tektonhub_validation_test.go
index 9e457ec17..83bd96278 100644
--- a/pkg/apis/operator/v1alpha1/tektonhub_validation_test.go
+++ b/pkg/apis/operator/v1alpha1/tektonhub_validation_test.go
@@ -44,6 +44,7 @@ func Test_ValidateTektonConfig_InvalidHubParam(t *testing.T) {
 					},
 				},
 			},
+			Pruner: Prune{Disabled: true},
 		},
 	}
 
@@ -71,6 +72,7 @@ func Test_ValidateTektonConfig_InvalidHubParamValue(t *testing.T) {
 					},
 				},
 			},
+			Pruner: Prune{Disabled: true},
 		},
 	}
 
diff --git a/pkg/apis/operator/v1alpha1/tektonpipeline_validation_test.go b/pkg/apis/operator/v1alpha1/tektonpipeline_validation_test.go
index 72357e05d..6ed841360 100644
--- a/pkg/apis/operator/v1alpha1/tektonpipeline_validation_test.go
+++ b/pkg/apis/operator/v1alpha1/tektonpipeline_validation_test.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"testing"
 
+	"github.com/tektoncd/pipeline/pkg/apis/config"
 	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"knative.dev/pkg/apis"
@@ -50,16 +51,28 @@ func Test_ValidateTektonPipeline_APIField(t *testing.T) {
 			CommonSpec: CommonSpec{
 				TargetNamespace: "namespace",
 			},
-			Pipeline: Pipeline{
-				PipelineProperties: PipelineProperties{
-					EnableApiFields: "prod",
-				},
-			},
 		},
 	}
 
-	err := tp.Validate(context.TODO())
-	assert.Equal(t, "invalid value: prod: spec.enable-api-fields", err.Error())
+	tests := []struct {
+		name     string
+		apiField string
+		err      string
+	}{
+		{name: "api-alpha", apiField: config.AlphaAPIFields, err: ""},
+		{name: "api-beta", apiField: config.AlphaAPIFields, err: ""},
+		{name: "api-stable", apiField: config.AlphaAPIFields, err: ""},
+		{name: "api-invalid", apiField: "prod", err: "invalid value: prod: spec.enable-api-fields"},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			tp.Spec.Pipeline.EnableApiFields = test.apiField
+			errs := tp.Validate(context.TODO())
+			assert.Equal(t, test.err, errs.Error())
+		})
+	}
+
 }
 
 func Test_ValidateTektonPipeline_OnDelete(t *testing.T) {
@@ -81,3 +94,50 @@ func Test_ValidateTektonPipeline_OnDelete(t *testing.T) {
 		t.Errorf("ValidateTektonPipeline.Validate() on Delete expected no error, but got one, ValidateTektonPipeline: %v", err)
 	}
 }
+
+func TestTektonPipelinePerformancePropertiesValidate(t *testing.T) {
+	tp := &TektonPipeline{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "pipeline",
+			Namespace: "bar",
+		},
+		Spec: TektonPipelineSpec{
+			CommonSpec: CommonSpec{
+				TargetNamespace: "foo",
+			},
+		},
+	}
+
+	// return pointer value
+	getBuckets := func(value uint) *uint {
+		return &value
+	}
+
+	// validate buckets minimum range
+	tp.Spec.PipelineProperties.Performance = PipelinePerformanceProperties{}
+	tp.Spec.PipelineProperties.Performance.DisableHA = false
+	tp.Spec.PipelineProperties.Performance.Buckets = getBuckets(0)
+	errs := tp.Validate(context.TODO())
+	assert.Equal(t, "expected 1 <= 0 <= 10: spec.performance.buckets", errs.Error())
+
+	// validate buckets maximum range
+	tp.Spec.PipelineProperties.Performance = PipelinePerformanceProperties{}
+	tp.Spec.PipelineProperties.Performance.DisableHA = false
+	tp.Spec.PipelineProperties.Performance.Buckets = getBuckets(11)
+	errs = tp.Validate(context.TODO())
+	assert.Equal(t, "expected 1 <= 11 <= 10: spec.performance.buckets", errs.Error())
+
+	// validate buckets valid range
+	tp.Spec.PipelineProperties.Performance = PipelinePerformanceProperties{}
+	tp.Spec.PipelineProperties.Performance.DisableHA = false
+	tp.Spec.PipelineProperties.Performance.Buckets = getBuckets(1)
+	errs = tp.Validate(context.TODO())
+	assert.Equal(t, "", errs.Error())
+
+	// validate buckets valid range
+	tp.Spec.PipelineProperties.Performance = PipelinePerformanceProperties{}
+	tp.Spec.PipelineProperties.Performance.DisableHA = false
+	tp.Spec.PipelineProperties.Performance.Buckets = getBuckets(10)
+	errs = tp.Validate(context.TODO())
+	assert.Equal(t, "", errs.Error())
+}
diff --git a/pkg/reconciler/common/prune_test.go b/pkg/reconciler/common/prune_test.go
index 0a0300298..16f63d9f0 100644
--- a/pkg/reconciler/common/prune_test.go
+++ b/pkg/reconciler/common/prune_test.go
@@ -601,6 +601,7 @@ func TestPrunerReconcile(t *testing.T) {
 					name: "TestPrunerEnabled",
 					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
 						tektonConfig.Spec.Pruner = v1alpha1.Prune{
+							Disabled:         false,
 							Keep:             getUintWithReference(20),
 							KeepSince:        nil,
 							Resources:        []string{"pipelinerun", "taskrun"},
@@ -628,14 +629,18 @@ func TestPrunerReconcile(t *testing.T) {
 					},
 				},
 				{ // reconcile #2
-					name: "TestPrunerDisabledWithEmptySchedule",
+					// only global schedule will be disable, if a namespace has prune schedule annotation, there will be a job for that
+					name: "TestPrunerGlobalScheduleDisabledWithEmptySchedule",
 					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
 						tektonConfig.Spec.Pruner.Schedule = ""
 						return nil
 					},
-					scheduleAndArgs: map[string]string{},
+					scheduleAndArgs: map[string]string{
+						"*/12 * * * *": "ns-seven;--keep=20,--keep-since=42;pipelinerun,taskrun;true",
+					},
 				},
 				{ // reconcile #3
+					// re-enables global schedule
 					name: "TestPrunerReEnabledSchedule",
 					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
 						tektonConfig.Spec.Pruner.Schedule = "*/5 * * * *"
@@ -647,15 +652,21 @@ func TestPrunerReconcile(t *testing.T) {
 					},
 				},
 				{ // reconcile #4
-					name: "TestPrunerDisabledWithEmptyResources",
+					// removes global resources
+					name: "TestPrunerWithGlobalEmptyResources",
 					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
 						tektonConfig.Spec.Pruner.Resources = []string{}
 						return nil
 					},
-					scheduleAndArgs: map[string]string{},
+					// default prune resources will be taken
+					scheduleAndArgs: map[string]string{
+						"*/5 * * * *":  "ns-one;--keep=20;pipelinerun;false ns-two;--keep=20;pipelinerun;false",
+						"*/12 * * * *": "ns-seven;--keep=20,--keep-since=42;pipelinerun;true",
+					},
 				},
 				{ // reconcile #5
-					name: "TestPrunerReEnabledWithAResource",
+					// adds global resources
+					name: "TestPrunerWithGlobalResources",
 					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
 						tektonConfig.Spec.Pruner.Resources = []string{"pipelinerun"}
 						return nil
@@ -666,7 +677,8 @@ func TestPrunerReconcile(t *testing.T) {
 					},
 				},
 				{ // reconcile #6
-					name: "TestPrunerReEnabledWithResources",
+					// modifies global resources
+					name: "TestPrunerUpdateGlobalResources",
 					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
 						tektonConfig.Spec.Pruner.Resources = []string{"pipelinerun", "taskrun"}
 						return nil
@@ -677,32 +689,63 @@ func TestPrunerReconcile(t *testing.T) {
 					},
 				},
 				{ // reconcile #7
+					// removes global keep and keepSince
 					name: "TestPrunerWithNilArguments",
 					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
 						tektonConfig.Spec.Pruner.Keep = nil
 						tektonConfig.Spec.Pruner.KeepSince = nil
 						return nil
 					},
+					// uses default keep value
 					scheduleAndArgs: map[string]string{
-						"*/12 * * * *": "ns-seven;--keep-since=42;pipelinerun,taskrun;true",
+						"*/5 * * * *":  "ns-one;--keep=100;pipelinerun,taskrun;false ns-two;--keep=100;pipelinerun,taskrun;false",
+						"*/12 * * * *": "ns-seven;--keep=100,--keep-since=42;pipelinerun,taskrun;true",
 					},
 				},
 				{ // reconcile #9
+					// removes global resources
 					name: "TestPrunerWithWithEmptyResources",
 					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
 						tektonConfig.Spec.Pruner.Resources = nil
 						return nil
 					},
-					scheduleAndArgs: map[string]string{},
+					// takes default resources
+					scheduleAndArgs: map[string]string{
+						"*/5 * * * *":  "ns-one;--keep=100;pipelinerun;false ns-two;--keep=100;pipelinerun;false",
+						"*/12 * * * *": "ns-seven;--keep=100,--keep-since=42;pipelinerun;true",
+					},
 				},
 				{ // reconcile #10
+					// adds global resources
 					name: "TestPrunerWithWithResources",
 					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
 						tektonConfig.Spec.Pruner.Resources = []string{"pipelinerun", "taskrun"}
 						return nil
 					},
 					scheduleAndArgs: map[string]string{
-						"*/12 * * * *": "ns-seven;--keep-since=42;pipelinerun,taskrun;true",
+						"*/5 * * * *":  "ns-one;--keep=100;pipelinerun,taskrun;false ns-two;--keep=100;pipelinerun,taskrun;false",
+						"*/12 * * * *": "ns-seven;--keep=100,--keep-since=42;pipelinerun,taskrun;true",
+					},
+				},
+				{ // reconcile #11
+					name: "TestPrunerDisableAll",
+					// disables pruner feature
+					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
+						tektonConfig.Spec.Pruner.Disabled = true
+						return nil
+					},
+					scheduleAndArgs: map[string]string{},
+				},
+				{ // reconcile #12
+					// re enables pruner feature
+					name: "TestPrunerReEnable",
+					applyChanges: func(tektonConfig *v1alpha1.TektonConfig, client *fake.Clientset, t *testing.T) func() {
+						tektonConfig.Spec.Pruner.Disabled = false
+						return nil
+					},
+					scheduleAndArgs: map[string]string{
+						"*/5 * * * *":  "ns-one;--keep=100;pipelinerun,taskrun;false ns-two;--keep=100;pipelinerun,taskrun;false",
+						"*/12 * * * *": "ns-seven;--keep=100,--keep-since=42;pipelinerun,taskrun;true",
 					},
 				},
 			},
diff --git a/pkg/reconciler/common/utils_test.go b/pkg/reconciler/common/utils_test.go
index d76e6b3b2..c401d0944 100644
--- a/pkg/reconciler/common/utils_test.go
+++ b/pkg/reconciler/common/utils_test.go
@@ -18,10 +18,12 @@ package common
 
 import (
 	"path"
+	"reflect"
 	"testing"
 
 	mf "github.com/manifestival/manifestival"
 	"gotest.tools/v3/assert"
+	"knative.dev/pkg/ptr"
 )
 
 func TestFetchVersionFromConfigMap(t *testing.T) {
@@ -67,3 +69,47 @@ func TestFetchVersionFromConfigMap_VersionKeyNotFound(t *testing.T) {
 
 	assert.Error(t, err, configMapError.Error())
 }
+
+func TestStructMap(t *testing.T) {
+	in := struct {
+		StringValue  string  `json:"str"`
+		Int32Ptr     *int32  `json:"int32Ptr"`
+		IntValue     int     `json:"intValue"`
+		Float32Value float32 `json:"float32_value"`
+		BoolValue    bool    `json:"bool-value"`
+	}{
+		StringValue:  "hi",
+		Int32Ptr:     ptr.Int32(1),
+		IntValue:     2,
+		Float32Value: 2.200001,
+		BoolValue:    false,
+	}
+
+	// json Unmarshal converts all the number types into float64
+	expectedOut := map[string]interface{}{
+		"str":           "hi",
+		"int32Ptr":      float64(1),
+		"intValue":      float64(2),
+		"float32_value": float64(2.200001),
+		"bool-value":    false,
+	}
+
+	actualOut := map[string]interface{}{}
+
+	err := StructToMap(&in, &actualOut)
+	assert.NilError(t, err)
+	assert.Check(t, reflect.DeepEqual(actualOut, expectedOut), actualOut)
+}
+
+func TestStructMapError(t *testing.T) {
+	in := struct {
+		StringValue string `json:"str"`
+	}{
+		StringValue: "hi",
+	}
+
+	actualOut := map[string]interface{}{}
+
+	err := StructToMap(&in, actualOut)
+	assert.Error(t, err, "json: Unmarshal(non-pointer map[string]interface {})")
+}
diff --git a/pkg/reconciler/kubernetes/tektonpipeline/transform_test.go b/pkg/reconciler/kubernetes/tektonpipeline/transform_test.go
new file mode 100644
index 000000000..38e092ffe
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonpipeline/transform_test.go
@@ -0,0 +1,131 @@
+/*
+Copyright 2023 The Tekton Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tektonpipeline
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"testing"
+
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	"gotest.tools/v3/assert"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	apimachineryRuntime "k8s.io/apimachinery/pkg/runtime"
+	"knative.dev/pkg/ptr"
+)
+
+func TestUpdatePerformanceFlagsInDeployment(t *testing.T) {
+	pipelineCR := &v1alpha1.TektonPipeline{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "pipeline",
+			Namespace: "xyz",
+		},
+	}
+	buckets := uint(2)
+	workers := int(3)
+	burst := int(33)
+	pipelineCR.Spec.Performance.Buckets = &buckets
+	pipelineCR.Spec.Performance.DisableHA = true
+	pipelineCR.Spec.Performance.KubeApiQPS = ptr.Float32(40.03)
+	pipelineCR.Spec.Performance.KubeApiBurst = &burst
+	pipelineCR.Spec.Performance.ThreadsPerController = &workers
+
+	depInput := &appsv1.Deployment{
+		TypeMeta: metav1.TypeMeta{
+			Kind: "Deployment",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      pipelinesControllerDeployment,
+			Namespace: "xyz",
+		},
+		Spec: appsv1.DeploymentSpec{
+			Replicas: ptr.Int32(1),
+			Selector: &metav1.LabelSelector{
+				MatchLabels: map[string]string{"app": "hello"},
+			},
+			Template: corev1.PodTemplateSpec{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels: map[string]string{"app": "hello"},
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "hello",
+							Image: "xyz",
+						},
+						{
+							Name:  pipelinesControllerContainer,
+							Image: "xyz",
+							Args:  []string{"-flag1", "v1", "-flag2", "v2", "-disable-ha"},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	// update expected output
+	depExpected := depInput.DeepCopy()
+	depExpected.Spec.Template.Labels = map[string]string{"app": "hello", "config-leader-election.data.buckets": "2"}
+	// flags order is important
+	depExpected.Spec.Template.Spec.Containers[1].Args = []string{
+		"-flag1", "v1",
+		"-flag2", "v2",
+		"-disable-ha", "true",
+		"-kube-api-burst", "33",
+		"-kube-api-qps", "40.03",
+		"-threads-per-controller", "3",
+	}
+
+	// convert to unstructured object
+	jsonBytes, err := json.Marshal(&depInput)
+	assert.NilError(t, err)
+	ud := &unstructured.Unstructured{}
+	err = json.Unmarshal(jsonBytes, ud)
+	assert.NilError(t, err)
+
+	// apply transformer
+	transformer := updatePerformanceFlagsInDeployment(pipelineCR)
+	err = transformer(ud)
+	assert.NilError(t, err)
+
+	// get transformed deployment
+	outDep := &appsv1.Deployment{}
+	err = apimachineryRuntime.DefaultUnstructuredConverter.FromUnstructured(ud.Object, outDep)
+	assert.NilError(t, err)
+
+	assert.Equal(t, true, reflect.DeepEqual(outDep, depExpected), fmt.Sprintf("transformed output:[%+v], expected:[%+v]", outDep, depExpected))
+}
+
+func TestGetSortedKeys(t *testing.T) {
+	in := map[string]interface{}{
+		"a1":  1,
+		"z1":  false,
+		"a2":  2,
+		"a3":  3,
+		"a10": 10,
+		"a11": 11,
+	}
+	expectedOut := []string{"a1", "a10", "a11", "a2", "a3", "z1"}
+
+	out := getSortedKeys(in)
+	assert.Equal(t, true, reflect.DeepEqual(out, expectedOut))
+}
diff --git a/pkg/reconciler/shared/tektonconfig/chain/chain_test.go b/pkg/reconciler/shared/tektonconfig/chain/chain_test.go
new file mode 100644
index 000000000..5ab6a0f97
--- /dev/null
+++ b/pkg/reconciler/shared/tektonconfig/chain/chain_test.go
@@ -0,0 +1,142 @@
+/*
+Copyright 2023 The Tekton Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package chain
+
+import (
+	"context"
+	"testing"
+
+	op "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+
+	"github.com/tektoncd/operator/pkg/client/injection/client/fake"
+	util "github.com/tektoncd/operator/pkg/reconciler/common/testing"
+	ts "knative.dev/pkg/reconciler/testing"
+)
+
+func TestEnsureTektonChainExists(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+	tt := GetTektonChainCR(getTektonConfig())
+
+	// first invocation should create instance as it is non-existent and return RECONCILE_AGAIN_ERR
+	_, err := EnsureTektonChainExists(ctx, c.OperatorV1alpha1().TektonChains(), tt)
+	util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR)
+
+	// during second invocation instance exists but waiting on dependencies (pipeline, Chains)
+	// hence returns RECONCILE_AGAIN_ERR
+	_, err = EnsureTektonChainExists(ctx, c.OperatorV1alpha1().TektonChains(), tt)
+	util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR)
+
+	// make upgrade checks pass
+	makeUpgradeCheckPass(t, ctx, c.OperatorV1alpha1().TektonChains())
+
+	// next invocation should return RECONCILE_AGAIN_ERR as Dashboard is waiting for installation (prereconcile, postreconcile, installersets...)
+	_, err = EnsureTektonChainExists(ctx, c.OperatorV1alpha1().TektonChains(), tt)
+	util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR)
+
+	// mark the instance ready
+	markChainsReady(t, ctx, c.OperatorV1alpha1().TektonChains())
+
+	// next invocation should return nil error as the instance is ready
+	_, err = EnsureTektonChainExists(ctx, c.OperatorV1alpha1().TektonChains(), tt)
+	util.AssertEqual(t, err, nil)
+
+	// test update propagation from tektonConfig
+	tt.Spec.TargetNamespace = "foobar"
+	_, err = EnsureTektonChainExists(ctx, c.OperatorV1alpha1().TektonChains(), tt)
+	util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR)
+
+	_, err = EnsureTektonChainExists(ctx, c.OperatorV1alpha1().TektonChains(), tt)
+	util.AssertEqual(t, err, nil)
+}
+
+func TestEnsureTektonChainCRNotExists(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+
+	// when no instance exists, nil error is returned immediately
+	err := EnsureTektonChainCRNotExists(ctx, c.OperatorV1alpha1().TektonChains())
+	util.AssertEqual(t, err, nil)
+
+	// create an instance for testing other cases
+	tt := GetTektonChainCR(getTektonConfig())
+	_, err = EnsureTektonChainExists(ctx, c.OperatorV1alpha1().TektonChains(), tt)
+	util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR)
+
+	// when an instance exists the first invocation should make the delete API call and
+	// return RECONCILE_AGAIN_ERROR. So that the deletion can be confirmed in a subsequent invocation
+	err = EnsureTektonChainCRNotExists(ctx, c.OperatorV1alpha1().TektonChains())
+	util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR)
+
+	// when the instance is completely removed from a cluster, the function should return nil error
+	err = EnsureTektonChainCRNotExists(ctx, c.OperatorV1alpha1().TektonChains())
+	util.AssertEqual(t, err, nil)
+}
+
+func markChainsReady(t *testing.T, ctx context.Context, c op.TektonChainInterface) {
+	t.Helper()
+	tr, err := c.Get(ctx, v1alpha1.ChainResourceName, metav1.GetOptions{})
+	util.AssertEqual(t, err, nil)
+	tr.Status.MarkDependenciesInstalled()
+	tr.Status.MarkPreReconcilerComplete()
+	tr.Status.MarkInstallerSetAvailable()
+	tr.Status.MarkInstallerSetReady()
+	tr.Status.MarkPostReconcilerComplete()
+	_, err = c.UpdateStatus(ctx, tr, metav1.UpdateOptions{})
+	util.AssertEqual(t, err, nil)
+}
+
+func makeUpgradeCheckPass(t *testing.T, ctx context.Context, c op.TektonChainInterface) {
+	t.Helper()
+	// set necessary version labels to make upgrade check pass
+	chain, err := c.Get(ctx, v1alpha1.ChainResourceName, metav1.GetOptions{})
+	util.AssertEqual(t, err, nil)
+	setDummyVersionLabel(t, chain)
+	_, err = c.Update(ctx, chain, metav1.UpdateOptions{})
+	util.AssertEqual(t, err, nil)
+}
+
+func setDummyVersionLabel(t *testing.T, tr *v1alpha1.TektonChain) {
+	t.Helper()
+
+	oprVersion := "v1.2.3"
+	t.Setenv(v1alpha1.VersionEnvKey, oprVersion)
+
+	labels := tr.GetLabels()
+	if labels == nil {
+		labels = map[string]string{}
+	}
+	labels[v1alpha1.ReleaseVersionKey] = oprVersion
+	tr.SetLabels(labels)
+}
+
+func getTektonConfig() *v1alpha1.TektonConfig {
+	return &v1alpha1.TektonConfig{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: v1alpha1.ConfigResourceName,
+		},
+		Spec: v1alpha1.TektonConfigSpec{
+			Profile: v1alpha1.ProfileAll,
+			CommonSpec: v1alpha1.CommonSpec{
+				TargetNamespace: "tekton-pipelines",
+			},
+		},
+	}
+}