Repository: tektoncd/operator
Pull request #1458 (2023-04-26 13:38:16)

add support to pipelines controller's performance flags
# Changes

With this PR, Operator introduces new properties to update/change pipelines controller's HA support and performance configurations

new fields structured as follows in `TektonConfig` CR
```
spec:
  pipeline:
    ...fields omitted ...
    performance:
      disable-ha: false
      buckets: 3
      threads-per-controller: 3
      kube-api-qps: 40
      kube-api-burst: 40
```
NOTE: replicas count not handled by `TecktonConfig` CR. replicas should be applied manually by user with the following command,
```
kubectl --namespace tekton-pipelines scale deployment tekton-pipelines-controller --replicas=3
```

# 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
- [x] 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
added support to change `disable-ha` and `buckets` in https://tekton.dev/docs/pipelines/enabling-ha/
added support to change performance configurations from https://tekton.dev/docs/pipelines/tekton-controller-performance-configuration/
Performance configuration fields: `kube-api-qps`, `kube-api-burst`, `threads-per-controller`
```


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 **sm43** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @sm43` 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":["sm43"]} -->
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/tektonpipeline_validation.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651219150988644352/artifacts/line-cov.html#file1) | 66.7% | 76.0% | 9.3
[pkg/reconciler/common/utils.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651219150988644352/artifacts/line-cov.html#file3) | 90.0% | 85.7% | -4.3
[pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651219150988644352/artifacts/line-cov.html#file4) | 72.2% | 70.2% | -2.0

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/tektonpipeline_validation.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651400731850182656/artifacts/line-cov.html#file1) | 66.7% | 76.0% | 9.3
[pkg/reconciler/common/utils.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651400731850182656/artifacts/line-cov.html#file3) | 90.0% | 85.7% | -4.3
[pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651400731850182656/artifacts/line-cov.html#file4) | 72.2% | 70.2% | -2.0

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/tektonpipeline_validation.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651402819414003712/artifacts/line-cov.html#file1) | 66.7% | 76.0% | 9.3
[pkg/reconciler/common/utils.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651402819414003712/artifacts/line-cov.html#file3) | 90.0% | 85.7% | -4.3
[pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651402819414003712/artifacts/line-cov.html#file4) | 72.2% | 70.2% | -2.0

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/tektonpipeline_validation.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651449853948989440/artifacts/line-cov.html#file1) | 66.7% | 76.0% | 9.3
[pkg/reconciler/common/utils.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651449853948989440/artifacts/line-cov.html#file3) | 90.0% | 85.7% | -4.3
[pkg/reconciler/kubernetes/tektonpipeline/transform.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651449853948989440/artifacts/line-cov.html#file4) | Do not exist | 71.9% | 
[pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651449853948989440/artifacts/line-cov.html#file5) | 72.2% | 70.2% | -2.0

jkandasa: /cc @vdemeester 
tekton-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/tektoncd/operator/pull/1458#pullrequestreview-1404130148" 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":[]} -->
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/tektonpipeline_validation.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651658734797066240/artifacts/line-cov.html#file1) | 66.7% | 76.0% | 9.3
[pkg/reconciler/common/utils.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651658734797066240/artifacts/line-cov.html#file3) | 90.0% | 85.7% | -4.3
[pkg/reconciler/kubernetes/tektonpipeline/transform.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651658734797066240/artifacts/line-cov.html#file4) | Do not exist | 71.9% | 
[pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651658734797066240/artifacts/line-cov.html#file5) | 72.2% | 70.2% | -2.0

jkandasa: /cc @mramendi
added documentation for pipelines controller performance parameters
tekton-robot: @jkandasa: GitHub didn't allow me to request PR reviews from the following users: mramendi.

Note that only [tektoncd members](https://github.com/orgs/tektoncd/people) and repo collaborators can review this PR, and authors cannot review their own PRs.

<details>

In response to [this](https://github.com/tektoncd/operator/pull/1458#issuecomment-1526166324):

>/cc @mramendi
>added documentation for pipelines controller performance parameters


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.
</details>
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/tektonpipeline_validation.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651751229438562304/artifacts/line-cov.html#file1) | 66.7% | 76.0% | 9.3
[pkg/reconciler/common/utils.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651751229438562304/artifacts/line-cov.html#file3) | 90.0% | 85.7% | -4.3
[pkg/reconciler/kubernetes/tektonpipeline/transform.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651751229438562304/artifacts/line-cov.html#file4) | Do not exist | 71.9% | 
[pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-go-coverage/1651751229438562304/artifacts/line-cov.html#file5) | 72.2% | 70.2% | -2.0

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-integration-tests | fb6aaca7a0d5c2922d286c43182f4f320fcb2616 | [link](https://prow.tekton.dev/view/gs/tekton-prow/pr-logs/pull/tektoncd_operator/1458/pull-tekton-operator-integration-tests/1651751229291761664) | true | `/test pull-tekton-operator-integration-tests`

[Full PR test history](https://prow.tekton.dev/pr-history?org=tektoncd&repo=operator&pr=1458). [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 -->
jkandasa: /retest

Patch:
diff --git a/docs/TektonConfig.md b/docs/TektonConfig.md
index f0fc2ab99..d92214ae5 100644
--- a/docs/TektonConfig.md
+++ b/docs/TektonConfig.md
@@ -50,6 +50,12 @@ 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:
       resources:
       - taskrun
@@ -134,6 +140,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
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/pkg/apis/operator/v1alpha1/tektonchain_types.go b/pkg/apis/operator/v1alpha1/tektonchain_types.go
index 02548422c..d3baa33da 100644
--- a/pkg/apis/operator/v1alpha1/tektonchain_types.go
+++ b/pkg/apis/operator/v1alpha1/tektonchain_types.go
@@ -61,19 +61,19 @@ type TektonChainSpec struct {
 // Chain defines the field to provide chain configuration
 type Chain struct {
 	// taskrun artifacts config
-	ArtifactsTaskRunFormat  string `json:"artifacts.taskrun.format,omitempty"`
-	ArtifactsTaskRunStorage string `json:"artifacts.taskrun.storage,omitempty"`
-	ArtifactsTaskRunSigner  string `json:"artifacts.taskrun.signer,omitempty"`
+	ArtifactsTaskRunFormat  string  `json:"artifacts.taskrun.format,omitempty"`
+	ArtifactsTaskRunStorage *string `json:"artifacts.taskrun.storage,omitempty"`
+	ArtifactsTaskRunSigner  string  `json:"artifacts.taskrun.signer,omitempty"`
 
 	// pipelinerun artifacts config
-	ArtifactsPipelineRunFormat  string `json:"artifacts.pipelinerun.format,omitempty"`
-	ArtifactsPipelineRunStorage string `json:"artifacts.pipelinerun.storage,omitempty"`
-	ArtifactsPipelineRunSigner  string `json:"artifacts.pipelinerun.signer,omitempty"`
+	ArtifactsPipelineRunFormat  string  `json:"artifacts.pipelinerun.format,omitempty"`
+	ArtifactsPipelineRunStorage *string `json:"artifacts.pipelinerun.storage,omitempty"`
+	ArtifactsPipelineRunSigner  string  `json:"artifacts.pipelinerun.signer,omitempty"`
 
 	// oci artifacts config
-	ArtifactsOCIFormat  string `json:"artifacts.oci.format,omitempty"`
-	ArtifactsOCIStorage string `json:"artifacts.oci.storage,omitempty"`
-	ArtifactsOCISigner  string `json:"artifacts.oci.signer,omitempty"`
+	ArtifactsOCIFormat  string  `json:"artifacts.oci.format,omitempty"`
+	ArtifactsOCIStorage *string `json:"artifacts.oci.storage,omitempty"`
+	ArtifactsOCISigner  string  `json:"artifacts.oci.signer,omitempty"`
 
 	// storage configs
 	StorageGCSBucket             string `json:"storage.gcs.bucket,omitempty"`
diff --git a/pkg/apis/operator/v1alpha1/tektonchain_validation.go b/pkg/apis/operator/v1alpha1/tektonchain_validation.go
index 1b8e36407..121e10501 100644
--- a/pkg/apis/operator/v1alpha1/tektonchain_validation.go
+++ b/pkg/apis/operator/v1alpha1/tektonchain_validation.go
@@ -30,7 +30,7 @@ var (
 	allowedArtifactsPipelineRunFormat = sets.NewString("", "in-toto", "slsa/v1")
 	allowedX509SignerFulcioProvider   = sets.NewString("", "google", "spiffe", "github", "filesystem")
 	allowedTransparencyConfigEnabled  = sets.NewString("", "true", "false", "manual")
-	allowedArtifactsStorage           = sets.NewString("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")
+	allowedArtifactsStorage           = sets.NewString("", "tekton", "oci", "gcs", "docdb", "grafeas", "kafka")
 )
 
 func (tc *TektonChain) Validate(ctx context.Context) (errs *apis.FieldError) {
@@ -57,8 +57,8 @@ func (tcs *TektonChainSpec) ValidateChainConfig(path string) (errs *apis.FieldEr
 		errs = errs.Also(apis.ErrInvalidValue(tcs.ArtifactsTaskRunFormat, path+".artifacts.taskrun.format"))
 	}
 
-	if tcs.ArtifactsTaskRunStorage != "" {
-		input := strings.Split(tcs.ArtifactsTaskRunStorage, ",")
+	if tcs.ArtifactsTaskRunStorage != nil {
+		input := strings.Split(*tcs.ArtifactsTaskRunStorage, ",")
 		for i, v := range input {
 			input[i] = strings.TrimSpace(v)
 			if !allowedArtifactsStorage.Has(input[i]) {
@@ -77,8 +77,8 @@ func (tcs *TektonChainSpec) ValidateChainConfig(path string) (errs *apis.FieldEr
 		errs = errs.Also(apis.ErrInvalidValue(tcs.ArtifactsPipelineRunFormat, path+".artifacts.pipelinerun.format"))
 	}
 
-	if tcs.ArtifactsPipelineRunStorage != "" {
-		input := strings.Split(tcs.ArtifactsPipelineRunStorage, ",")
+	if tcs.ArtifactsPipelineRunStorage != nil {
+		input := strings.Split(*tcs.ArtifactsPipelineRunStorage, ",")
 		for i, v := range input {
 			input[i] = strings.TrimSpace(v)
 			if !allowedArtifactsStorage.Has(input[i]) {
@@ -99,8 +99,8 @@ func (tcs *TektonChainSpec) ValidateChainConfig(path string) (errs *apis.FieldEr
 		}
 	}
 
-	if tcs.ArtifactsOCIStorage != "" {
-		input := strings.Split(tcs.ArtifactsOCIStorage, ",")
+	if tcs.ArtifactsOCIStorage != nil {
+		input := strings.Split(*tcs.ArtifactsOCIStorage, ",")
 		for i, v := range input {
 			input[i] = strings.TrimSpace(v)
 			if !allowedArtifactsStorage.Has(input[i]) {
diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_types.go b/pkg/apis/operator/v1alpha1/tektonconfig_types.go
index 66c77a9b6..391605090 100644
--- a/pkg/apis/operator/v1alpha1/tektonconfig_types.go
+++ b/pkg/apis/operator/v1alpha1/tektonconfig_types.go
@@ -92,6 +92,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/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..385656e3f 100644
--- a/pkg/apis/operator/v1alpha1/tektonpipeline_validation.go
+++ b/pkg/apis/operator/v1alpha1/tektonpipeline_validation.go
@@ -61,5 +61,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 01704be98..aae1e2575 100644
--- a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go
@@ -104,6 +104,21 @@ func (in *Category) DeepCopy() *Category {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Chain) DeepCopyInto(out *Chain) {
 	*out = *in
+	if in.ArtifactsTaskRunStorage != nil {
+		in, out := &in.ArtifactsTaskRunStorage, &out.ArtifactsTaskRunStorage
+		*out = new(string)
+		**out = **in
+	}
+	if in.ArtifactsPipelineRunStorage != nil {
+		in, out := &in.ArtifactsPipelineRunStorage, &out.ArtifactsPipelineRunStorage
+		*out = new(string)
+		**out = **in
+	}
+	if in.ArtifactsOCIStorage != nil {
+		in, out := &in.ArtifactsOCIStorage, &out.ArtifactsOCIStorage
+		*out = new(string)
+		**out = **in
+	}
 	if in.StorageOCIRepositoryInsecure != nil {
 		in, out := &in.StorageOCIRepositoryInsecure, &out.StorageOCIRepositoryInsecure
 		*out = new(bool)
@@ -517,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
@@ -533,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
@@ -589,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
 }
 
@@ -1046,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/transformers.go b/pkg/reconciler/common/transformers.go
index 6b6bbbf26..ab1ee9a4c 100644
--- a/pkg/reconciler/common/transformers.go
+++ b/pkg/reconciler/common/transformers.go
@@ -531,10 +531,13 @@ func AddConfigMapValues(configMapName string, prop interface{}) mf.Transformer {
 				if !innerElem.IsValid() {
 					continue
 				}
+
 				if innerElem.Kind() == reflect.Bool {
 					cm.Data[key] = strconv.FormatBool(innerElem.Bool())
 				} else if innerElem.Kind() == reflect.Uint {
 					cm.Data[key] = strconv.FormatUint(innerElem.Uint(), 10)
+				} else if innerElem.Kind() == reflect.String {
+					cm.Data[key] = innerElem.String()
 				}
 				continue
 			}
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/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..a001dcbc8 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,12 +147,25 @@ 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


Test patch:
diff --git a/pkg/apis/operator/v1alpha1/tektonchain_validation_test.go b/pkg/apis/operator/v1alpha1/tektonchain_validation_test.go
index a3a06908d..11b291a6b 100644
--- a/pkg/apis/operator/v1alpha1/tektonchain_validation_test.go
+++ b/pkg/apis/operator/v1alpha1/tektonchain_validation_test.go
@@ -80,6 +80,7 @@ func Test_ValidateTektonChain_ConfigTaskRunFormat(t *testing.T) {
 }
 
 func Test_ValidateTektonChain_ConfigTaskRunStorage(t *testing.T) {
+	value := "test"
 	td := &TektonChain{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "chain",
@@ -90,7 +91,7 @@ func Test_ValidateTektonChain_ConfigTaskRunStorage(t *testing.T) {
 				TargetNamespace: "namespace",
 			},
 			Chain: Chain{
-				ArtifactsTaskRunStorage: "test",
+				ArtifactsTaskRunStorage: &value,
 			},
 		},
 	}
@@ -100,6 +101,7 @@ func Test_ValidateTektonChain_ConfigTaskRunStorage(t *testing.T) {
 }
 
 func Test_ValidateTektonChain_ConfigTaskRunStorageInvalidValidMix(t *testing.T) {
+	value := "tekton, test"
 	td := &TektonChain{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "chain",
@@ -110,7 +112,7 @@ func Test_ValidateTektonChain_ConfigTaskRunStorageInvalidValidMix(t *testing.T)
 				TargetNamespace: "namespace",
 			},
 			Chain: Chain{
-				ArtifactsTaskRunStorage: "tekton, test",
+				ArtifactsTaskRunStorage: &value,
 			},
 		},
 	}
@@ -120,6 +122,7 @@ func Test_ValidateTektonChain_ConfigTaskRunStorageInvalidValidMix(t *testing.T)
 }
 
 func Test_ValidateTektonChain_ConfigTaskRunStorageValid(t *testing.T) {
+	value := "tekton, oci"
 	td := &TektonChain{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "chain",
@@ -130,7 +133,7 @@ func Test_ValidateTektonChain_ConfigTaskRunStorageValid(t *testing.T) {
 				TargetNamespace: "namespace",
 			},
 			Chain: Chain{
-				ArtifactsTaskRunStorage: "tekton, oci",
+				ArtifactsTaskRunStorage: &value,
 			},
 		},
 	}
@@ -162,6 +165,7 @@ func Test_ValidateTektonChain_ConfigPipelineRunFormat(t *testing.T) {
 }
 
 func Test_ValidateTektonChain_ConfigPipelineRunStorage(t *testing.T) {
+	value := "test"
 	td := &TektonChain{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "chain",
@@ -172,7 +176,7 @@ func Test_ValidateTektonChain_ConfigPipelineRunStorage(t *testing.T) {
 				TargetNamespace: "namespace",
 			},
 			Chain: Chain{
-				ArtifactsPipelineRunStorage: "test",
+				ArtifactsPipelineRunStorage: &value,
 			},
 		},
 	}
@@ -182,6 +186,7 @@ func Test_ValidateTektonChain_ConfigPipelineRunStorage(t *testing.T) {
 }
 
 func Test_ValidateTektonChain_ConfigPipelineRunStorageInvalidValidMix(t *testing.T) {
+	value := "tekton, test"
 	td := &TektonChain{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "chain",
@@ -192,7 +197,7 @@ func Test_ValidateTektonChain_ConfigPipelineRunStorageInvalidValidMix(t *testing
 				TargetNamespace: "namespace",
 			},
 			Chain: Chain{
-				ArtifactsPipelineRunStorage: "tekton, test",
+				ArtifactsPipelineRunStorage: &value,
 			},
 		},
 	}
@@ -202,6 +207,7 @@ func Test_ValidateTektonChain_ConfigPipelineRunStorageInvalidValidMix(t *testing
 }
 
 func Test_ValidateTektonChain_ConfigPipelineRunStorageValid(t *testing.T) {
+	value := "tekton, oci"
 	td := &TektonChain{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "chain",
@@ -212,7 +218,7 @@ func Test_ValidateTektonChain_ConfigPipelineRunStorageValid(t *testing.T) {
 				TargetNamespace: "namespace",
 			},
 			Chain: Chain{
-				ArtifactsPipelineRunStorage: "tekton, oci",
+				ArtifactsPipelineRunStorage: &value,
 			},
 		},
 	}
diff --git a/pkg/apis/operator/v1alpha1/tektonpipeline_validation_test.go b/pkg/apis/operator/v1alpha1/tektonpipeline_validation_test.go
index 72357e05d..b5541fec0 100644
--- a/pkg/apis/operator/v1alpha1/tektonpipeline_validation_test.go
+++ b/pkg/apis/operator/v1alpha1/tektonpipeline_validation_test.go
@@ -81,3 +81,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/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",
+			},
+		},
+	}
+}