Repository: tektoncd/operator
Pull request #179 (2020-12-02 11:49:19)

Add TektonConfig CRD to install Tekton Component
# Changes

* Added new CRD called `TektonConfig`
   `TektonConfig` CRD provide options to user to choose which tekton component they want to install based on `profile`
* Updated doc for more info on `profile`
* Added API and Reconciler
* Added e2e and unit test

<!-- 🎉🎉🎉 Thank you for the PR!!! 🎉🎉🎉 -->

<!-- Describe your changes here- ideally you can get that description straight from
your descriptive commit message(s)! -->

# Submitter Checklist

These are the criteria that every PR should meet, please check them off as you
review them:

- [ ] Includes [tests](https://github.com/tektoncd/community/blob/master/standards.md#principles) (if functionality changed/added)
- [ ] Includes [docs](https://github.com/tektoncd/community/blob/master/standards.md#principles) (if user facing)
- [ ] 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

<!--
Describe any user facing changes here, or delete this block.

Examples of user facing changes:
- API changes
- Bug fixes
- Any changes in behavior
- Changes requiring upgrade notices or deprecation warnings

For pull requests with a release note:

    ```release-note
    Your release note here
    ```

For pull requests that require additional action from users switching to the new release, include the string "action required" (case insensitive) in the release note:

    ```release-note
    action required: your release note here
    ```

For pull requests that don't need to be mentioned at release time, use the `/release-note-none` Prow command to add the `release-note-none` label to the PR. You can also write the string "NONE" as a release note in your PR description:

    ```release-note
    NONE
    ```
-->
```release-note
  Added new CRD called `TektonConfig` to install TektonCompnents
```

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 **houshengbo**
You can assign the PR to them by writing `/assign @houshengbo` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=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":["houshengbo"]} -->
savitaashture: /cc @vdemeester @nikhil-thomas 
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_lifecycle.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334102395474415619/artifacts/line-cov.html#file1) | Do not exist | 76.5% | 
[pkg/apis/operator/v1alpha1/tektonconfig_types.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334102395474415619/artifacts/line-cov.html#file2) | Do not exist | 0.0% | 
[pkg/reconciler/common/config/addon.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334102395474415619/artifacts/line-cov.html#file4) | Do not exist | 75.6% | 
[pkg/reconciler/common/config/dashboard.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334102395474415619/artifacts/line-cov.html#file5) | Do not exist | 75.6% | 
[pkg/reconciler/common/config/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334102395474415619/artifacts/line-cov.html#file6) | Do not exist | 75.6% | 
[pkg/reconciler/common/config/trigger.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334102395474415619/artifacts/line-cov.html#file7) | Do not exist | 75.6% | 

tekton-robot: @savitaashture: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-tekton-operator-unit-tests | 755216557d9822bb23771d75fd97443d6b965c7c | [link](https://tekton-releases.appspot.com/build/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-unit-tests/1334102395474415616/) | `/test pull-tekton-operator-unit-tests`

[Full PR test history](https://tekton-releases.appspot.com/pr/tektoncd_operator/179). [Your PR dashboard](https://prow.tekton.dev/pr/savitaashture).

<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: @savitaashture: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-tekton-operator-build-tests | 755216557d9822bb23771d75fd97443d6b965c7c | [link](https://tekton-releases.appspot.com/build/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-build-tests/1334102395474415617/) | `/test pull-tekton-operator-build-tests`

[Full PR test history](https://tekton-releases.appspot.com/pr/tektoncd_operator/179). [Your PR dashboard](https://prow.tekton.dev/pr/savitaashture).

<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: @savitaashture: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-tekton-operator-unit-tests | 755216557d9822bb23771d75fd97443d6b965c7c | [link](https://tekton-releases.appspot.com/build/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-unit-tests/1334102395474415616/) | `/test pull-tekton-operator-unit-tests`
pull-tekton-operator-build-tests | 755216557d9822bb23771d75fd97443d6b965c7c | [link](https://tekton-releases.appspot.com/build/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-build-tests/1334102395474415617/) | `/test pull-tekton-operator-build-tests`

[Full PR test history](https://tekton-releases.appspot.com/pr/tektoncd_operator/179). [Your PR dashboard](https://prow.tekton.dev/pr/savitaashture).

<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_lifecycle.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334117620709855234/artifacts/line-cov.html#file1) | Do not exist | 76.5% | 
[pkg/apis/operator/v1alpha1/tektonconfig_types.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334117620709855234/artifacts/line-cov.html#file2) | Do not exist | 0.0% | 
[pkg/reconciler/common/config/addon.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334117620709855234/artifacts/line-cov.html#file5) | Do not exist | 75.6% | 
[pkg/reconciler/common/config/dashboard.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334117620709855234/artifacts/line-cov.html#file6) | Do not exist | 75.6% | 
[pkg/reconciler/common/config/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334117620709855234/artifacts/line-cov.html#file7) | Do not exist | 75.6% | 
[pkg/reconciler/common/config/trigger.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334117620709855234/artifacts/line-cov.html#file8) | Do not exist | 75.6% | 
[pkg/reconciler/common/extensions.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334117620709855234/artifacts/line-cov.html#file9) | 100.0% | 80.0% | -20.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/tektonconfig_lifecycle.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334120514314047490/artifacts/line-cov.html#file1) | Do not exist | 76.5% | 
[pkg/apis/operator/v1alpha1/tektonconfig_types.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334120514314047490/artifacts/line-cov.html#file2) | Do not exist | 0.0% | 
[pkg/reconciler/common/config/addon.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334120514314047490/artifacts/line-cov.html#file5) | Do not exist | 75.6% | 
[pkg/reconciler/common/config/dashboard.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334120514314047490/artifacts/line-cov.html#file6) | Do not exist | 75.6% | 
[pkg/reconciler/common/config/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334120514314047490/artifacts/line-cov.html#file7) | Do not exist | 75.6% | 
[pkg/reconciler/common/config/trigger.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334120514314047490/artifacts/line-cov.html#file8) | Do not exist | 75.6% | 

savitaashture: > 2\. is is absolutely necessary to pass the client to the PostReconcile method in `Extension` interface `PostReconcile(context.Context, v1alpha1.TektonComponent, operatorv1alpha1.OperatorV1alpha1Interface) error` . can we initialize the client(s) inside the implementaitons as we are passing context. It is fine if there is no way around it.

@nikhil-thomas tried with this function [Get(ctx)](https://github.com/tektoncd/operator/blob/main/pkg/client/injection/client/client.go#L42) function  but its always giving [error](https://github.com/tektoncd/operator/blob/main/pkg/client/injection/client/client.go#L46) somehow context don't have the info when i call from other .go files but same thing work inside [controller.go](https://github.com/tektoncd/operator/blob/main/pkg/reconciler/kubernetes/tektonpipeline/controller.go)
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_lifecycle.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334825535410278400/artifacts/line-cov.html#file1) | Do not exist | 76.5% | 
[pkg/apis/operator/v1alpha1/tektonconfig_types.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334825535410278400/artifacts/line-cov.html#file2) | Do not exist | 0.0% | 
[pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334825535410278400/artifacts/line-cov.html#file6) | Do not exist | 75.6% | 
[pkg/reconciler/kubernetes/tektonconfig/pipeline/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334825535410278400/artifacts/line-cov.html#file7) | Do not exist | 76.1% | 
[pkg/reconciler/kubernetes/tektonconfig/trigger/trigger.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334825535410278400/artifacts/line-cov.html#file8) | Do not exist | 75.6% | 
[pkg/reconciler/openshift/tektonconfig/extension/addon.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334825535410278400/artifacts/line-cov.html#file9) | Do not exist | 75.6% | 

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_lifecycle.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334826416000208898/artifacts/line-cov.html#file1) | Do not exist | 76.5% | 
[pkg/apis/operator/v1alpha1/tektonconfig_types.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334826416000208898/artifacts/line-cov.html#file2) | Do not exist | 0.0% | 
[pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334826416000208898/artifacts/line-cov.html#file6) | Do not exist | 75.6% | 
[pkg/reconciler/kubernetes/tektonconfig/pipeline/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334826416000208898/artifacts/line-cov.html#file7) | Do not exist | 76.1% | 
[pkg/reconciler/kubernetes/tektonconfig/trigger/trigger.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334826416000208898/artifacts/line-cov.html#file8) | Do not exist | 75.6% | 
[pkg/reconciler/openshift/tektonconfig/extension/addon.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1334826416000208898/artifacts/line-cov.html#file9) | Do not exist | 75.6% | 

tekton-robot: @savitaashture: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-tekton-operator-build-tests | 91b24eacdfbe9328daecc55524b8a1899feb0d35 | [link](https://tekton-releases.appspot.com/build/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-build-tests/1334826416000208896/) | `/test pull-tekton-operator-build-tests`

[Full PR test history](https://tekton-releases.appspot.com/pr/tektoncd_operator/179). [Your PR dashboard](https://prow.tekton.dev/pr/savitaashture).

<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: @savitaashture: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-tekton-operator-build-tests | 91b24eacdfbe9328daecc55524b8a1899feb0d35 | [link](https://tekton-releases.appspot.com/build/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-build-tests/1334826416000208896/) | `/test pull-tekton-operator-build-tests`

[Full PR test history](https://tekton-releases.appspot.com/pr/tektoncd_operator/179). [Your PR dashboard](https://prow.tekton.dev/pr/savitaashture).

<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_lifecycle.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1335097074445717506/artifacts/line-cov.html#file1) | Do not exist | 76.5% | 
[pkg/apis/operator/v1alpha1/tektonconfig_types.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1335097074445717506/artifacts/line-cov.html#file2) | Do not exist | 0.0% | 
[pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1335097074445717506/artifacts/line-cov.html#file6) | Do not exist | 75.6% | 
[pkg/reconciler/kubernetes/tektonconfig/pipeline/pipeline.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1335097074445717506/artifacts/line-cov.html#file7) | Do not exist | 76.1% | 
[pkg/reconciler/kubernetes/tektonconfig/trigger/trigger.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1335097074445717506/artifacts/line-cov.html#file8) | Do not exist | 75.6% | 
[pkg/reconciler/openshift/tektonconfig/extension/addon.go](https://storage.cloud.google.com/tekton-prow/pr-logs/pull/tektoncd_operator/179/pull-tekton-operator-go-coverage/1335097074445717506/artifacts/line-cov.html#file9) | Do not exist | 75.6% | 

savitaashture: Thank you :+1: 

PR is ready for another review
tekton-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

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

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?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":[]} -->
nikhil-thomas: /lgtm

Patch:
diff --git a/README.md b/README.md
index 6ca4fa282..bd8629570 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,6 @@ on any Kubernetes Cluster.
 
 ## Install Tektoncd Operator
 
-
-
 # References
 
 - [Development Guide](docs/README.md)
diff --git a/cmd/kubernetes/main.go b/cmd/kubernetes/main.go
index 8489b405e..fef4b179e 100644
--- a/cmd/kubernetes/main.go
+++ b/cmd/kubernetes/main.go
@@ -17,6 +17,7 @@ limitations under the License.
 package main
 
 import (
+	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektonconfig"
 	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektondashboard"
 	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektonpipeline"
 	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektontrigger"
@@ -28,5 +29,6 @@ func main() {
 		tektonpipeline.NewController,
 		tektontrigger.NewController,
 		tektondashboard.NewController,
+		tektonconfig.NewController,
 	)
 }
diff --git a/cmd/openshift/main.go b/cmd/openshift/main.go
index bc58ee3ed..c64846d94 100644
--- a/cmd/openshift/main.go
+++ b/cmd/openshift/main.go
@@ -19,6 +19,7 @@ package main
 import (
 	"github.com/tektoncd/operator/pkg/reconciler/openshift/rbac"
 	"github.com/tektoncd/operator/pkg/reconciler/openshift/tektonaddon"
+	"github.com/tektoncd/operator/pkg/reconciler/openshift/tektonconfig"
 	"github.com/tektoncd/operator/pkg/reconciler/openshift/tektonpipeline"
 	"github.com/tektoncd/operator/pkg/reconciler/openshift/tektontrigger"
 	"knative.dev/pkg/injection/sharedmain"
@@ -29,6 +30,7 @@ func main() {
 		tektonpipeline.NewController,
 		tektontrigger.NewController,
 		tektonaddon.NewController,
+		tektonconfig.NewController,
 		rbac.NewController,
 	)
 }
diff --git a/config/base/300-operator_v1alpha1_config_crd.yaml b/config/base/300-operator_v1alpha1_config_crd.yaml
new file mode 100644
index 000000000..33bd3832d
--- /dev/null
+++ b/config/base/300-operator_v1alpha1_config_crd.yaml
@@ -0,0 +1,119 @@
+# Copyright 2020 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.
+
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: tektonconfigs.operator.tekton.dev
+  labels:
+    version: "devel"
+    operator.tekton.dev/release: "devel"
+spec:
+  additionalPrinterColumns:
+  - JSONPath: .status.conditions[?(@.type=="Ready")].status
+    name: Ready
+    type: string
+  - JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason"
+    name: Reason
+    type: string
+  group: operator.tekton.dev
+  names:
+    kind: TektonConfig
+    listKind: TektonConfigList
+    plural: tektonconfigs
+    singular: tektonconfig
+  preserveUnknownFields: false
+  scope: Cluster
+  subresources:
+    status: {}
+  validation:
+    openAPIV3Schema:
+      type: object
+      description: Schema for the tektonconfigs API
+      properties:
+        apiVersion:
+          description: 'APIVersion defines the versioned schema of this representation
+            of an object. Servers should convert recognized schemas to the latest
+            internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources'
+          type: string
+        kind:
+          description: 'Kind is a string value representing the REST resource this
+            object represents. Servers may infer this from the endpoint the client
+            submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds'
+          type: string
+        metadata:
+          type: object
+        spec:
+          description: Spec defines the desired state of TektonConfig
+          properties:
+            profile:
+              description: based on the type of profile where tekton components will be installed
+              type: string
+            targetNamespace:
+              description: namespace where tekton components will be installed
+              type: string
+          type: object
+        status:
+          description: Status defines the observed state of TektonConfig
+          properties:
+            observedGeneration:
+              description: The generation last processed by the controller
+              type: integer
+            conditions:
+              description: The latest available observations of a resource's current
+                state.
+              items:
+                properties:
+                  lastTransitionTime:
+                    description: LastTransitionTime is the last time the condition
+                      transitioned from one status to another. We use VolatileTime
+                      in place of metav1.Time to exclude this from creating equality.Semantic
+                      differences (all other things held constant).
+                    type: string
+                  message:
+                    description: A human readable message indicating details about
+                      the transition.
+                    type: string
+                  reason:
+                    description: The reason for the condition's last transition.
+                    type: string
+                  severity:
+                    description: Severity with which to treat failures of this type
+                      of condition. When this is not specified, it defaults to Error.
+                    type: string
+                  status:
+                    description: Status of the condition, one of True, False, Unknown.
+                    type: string
+                  type:
+                    description: Type of condition.
+                    type: string
+                required:
+                - type
+                - status
+                type: object
+              type: array
+            version:
+              description: The version of the installed release
+              type: string
+            manifests:
+              description: The list of serving manifests, which have been installed by the operator
+              type: array
+              items:
+                type: string
+          type: object
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
diff --git a/config/base/kustomization.yaml b/config/base/kustomization.yaml
index 42b41c5d4..9c21d7b90 100644
--- a/config/base/kustomization.yaml
+++ b/config/base/kustomization.yaml
@@ -19,6 +19,7 @@ resources:
 - 300-operator_v1alpha1_trigger_crd.yaml
 - 300-operator_v1alpha1_dashboard_crd.yaml
 - 300-operator_v1alpha1_addon_crd.yaml
+- 300-operator_v1alpha1_config_crd.yaml
 - role.yaml
 - role_binding.yaml
 - service_account.yaml
diff --git a/config/crs/operator_v1alpha1_config_cr.yaml b/config/crs/operator_v1alpha1_config_cr.yaml
new file mode 100644
index 000000000..6a38d3584
--- /dev/null
+++ b/config/crs/operator_v1alpha1_config_cr.yaml
@@ -0,0 +1,7 @@
+apiVersion: operator.tekton.dev/v1alpha1
+kind: TektonConfig
+metadata:
+  name: config
+spec:
+  profile: default
+  targetNamespace: tekton-pipelines
diff --git a/docs/README.md b/docs/README.md
index aa0d502eb..9856dc7b6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -44,6 +44,26 @@ If the files in `pkg/apis` are updated we need to run `codegen` scripts
 ```shell script
     make TARGET=openshift apply
 ```
+### Install Tekton components
+Operator provides an option to choose which components needs to be installed by specifying `profile`.
+
+`profile` is an optional field and supported `profile` are
+* **basic** 
+* **default**
+* **all**
+
+1. If profile is `basic` **TektonPipeline** will be installed
+1. If profile is `default` or `" "` **TektonPipeline** and **TektonTrigger** will be installed
+1. If profile is `all` then all the Tekton Components installed
+
+To create Tekton Components run
+```shell script
+kubectl apply -f config/crs/operator_v1alpha1_config_cr.yaml
+```
+To delete installed Tekton Components run
+```shell script
+kubectl delete -f config/crs/operator_v1alpha1_config_cr.yaml
+```
 
 ## Running Tests
 
diff --git a/pkg/apis/operator/v1alpha1/register.go b/pkg/apis/operator/v1alpha1/register.go
index d8818e3c0..aaafd67c8 100644
--- a/pkg/apis/operator/v1alpha1/register.go
+++ b/pkg/apis/operator/v1alpha1/register.go
@@ -40,6 +40,9 @@ const (
 
 	// KindTektonAddon is the Kind of Tekton Addon in a GVK context.
 	KindTektonAddon = "TektonAddon"
+
+	// KindTektonConfig is the Kind of Tekton Config in a GVK context.
+	KindTektonConfig = "TektonConfig"
 )
 
 // Resource takes an unqualified resource and returns a Group qualified GroupResource
@@ -59,6 +62,8 @@ func addKnownTypes(s *runtime.Scheme) error {
 		&TektonDashboardList{},
 		&TektonAddon{},
 		&TektonAddonList{},
+		&TektonConfig{},
+		&TektonConfigList{},
 	)
 	metav1.AddToGroupVersion(s, SchemeGroupVersion)
 	return nil
diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_lifecycle.go b/pkg/apis/operator/v1alpha1/tektonconfig_lifecycle.go
new file mode 100644
index 000000000..56fb73cbe
--- /dev/null
+++ b/pkg/apis/operator/v1alpha1/tektonconfig_lifecycle.go
@@ -0,0 +1,127 @@
+/*
+Copyright 2020 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 v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"knative.dev/pkg/apis"
+)
+
+var (
+	_ TektonComponentStatus = (*TektonConfigStatus)(nil)
+
+	configCondSet = apis.NewLivingConditionSet(
+		DependenciesInstalled,
+		DeploymentsAvailable,
+		InstallSucceeded,
+	)
+)
+
+// GroupVersionKind returns SchemeGroupVersion of a TektonConfig
+func (tp *TektonConfig) GroupVersionKind() schema.GroupVersionKind {
+	return SchemeGroupVersion.WithKind(KindTektonConfig)
+}
+
+// GetCondition returns the current condition of a given condition type
+func (tps *TektonConfigStatus) GetCondition(t apis.ConditionType) *apis.Condition {
+	return configCondSet.Manage(tps).GetCondition(t)
+}
+
+// InitializeConditions initializes conditions of an TektonConfigStatus
+func (tps *TektonConfigStatus) InitializeConditions() {
+	configCondSet.Manage(tps).InitializeConditions()
+}
+
+// IsReady looks at the conditions returns true if they are all true.
+func (tps *TektonConfigStatus) IsReady() bool {
+	return configCondSet.Manage(tps).IsHappy()
+}
+
+// MarkInstallSucceeded marks the InstallationSucceeded status as true.
+func (tps *TektonConfigStatus) MarkInstallSucceeded() {
+	configCondSet.Manage(tps).MarkTrue(InstallSucceeded)
+	if tps.GetCondition(DependenciesInstalled).IsUnknown() {
+		// Assume deps are installed if we're not sure
+		tps.MarkDependenciesInstalled()
+	}
+}
+
+// MarkInstallFailed marks the InstallationSucceeded status as false with the given
+// message.
+func (tps *TektonConfigStatus) MarkInstallFailed(msg string) {
+	configCondSet.Manage(tps).MarkFalse(
+		InstallSucceeded,
+		"Error",
+		"Install failed with message: %s", msg)
+}
+
+// MarkDeploymentsAvailable marks the DeploymentsAvailable status as true.
+func (tps *TektonConfigStatus) MarkDeploymentsAvailable() {
+	configCondSet.Manage(tps).MarkTrue(DeploymentsAvailable)
+}
+
+// MarkDeploymentsNotReady marks the DeploymentsAvailable status as false and calls out
+// it's waiting for deployments.
+func (tps *TektonConfigStatus) MarkDeploymentsNotReady() {
+	configCondSet.Manage(tps).MarkFalse(
+		DeploymentsAvailable,
+		"NotReady",
+		"Waiting on deployments")
+}
+
+// MarkDependenciesInstalled marks the DependenciesInstalled status as true.
+func (tps *TektonConfigStatus) MarkDependenciesInstalled() {
+	configCondSet.Manage(tps).MarkTrue(DependenciesInstalled)
+}
+
+// MarkDependencyInstalling marks the DependenciesInstalled status as false with the
+// given message.
+func (tps *TektonConfigStatus) MarkDependencyInstalling(msg string) {
+	configCondSet.Manage(tps).MarkFalse(
+		DependenciesInstalled,
+		"Installing",
+		"Dependency installing: %s", msg)
+}
+
+// MarkDependencyMissing marks the DependenciesInstalled status as false with the
+// given message.
+func (tps *TektonConfigStatus) MarkDependencyMissing(msg string) {
+	configCondSet.Manage(tps).MarkFalse(
+		DependenciesInstalled,
+		"Error",
+		"Dependency missing: %s", msg)
+}
+
+// GetVersion gets the currently installed version of the component.
+func (tps *TektonConfigStatus) GetVersion() string {
+	return tps.Version
+}
+
+// SetVersion sets the currently installed version of the component.
+func (tps *TektonConfigStatus) SetVersion(version string) {
+	tps.Version = version
+}
+
+// GetManifests gets the url links of the manifests.
+func (tps *TektonConfigStatus) GetManifests() []string {
+	return tps.Manifests
+}
+
+// SetVersion sets the url links of the manifests.
+func (tps *TektonConfigStatus) SetManifests(manifests []string) {
+	tps.Manifests = manifests
+}
diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_types.go b/pkg/apis/operator/v1alpha1/tektonconfig_types.go
new file mode 100644
index 000000000..022ec65aa
--- /dev/null
+++ b/pkg/apis/operator/v1alpha1/tektonconfig_types.go
@@ -0,0 +1,77 @@
+/*
+Copyright 2020 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 v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	duckv1 "knative.dev/pkg/apis/duck/v1"
+)
+
+var (
+	_ TektonComponent     = (*TektonConfig)(nil)
+	_ TektonComponentSpec = (*TektonConfigSpec)(nil)
+)
+
+// TektonConfig is the Schema for the TektonConfigs API
+// +genclient
+// +genreconciler:krshapedlogic=false
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +genclient:nonNamespaced
+type TektonConfig struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   TektonConfigSpec   `json:"spec,omitempty"`
+	Status TektonConfigStatus `json:"status,omitempty"`
+}
+
+// GetSpec implements TektonComponent
+func (tp *TektonConfig) GetSpec() TektonComponentSpec {
+	return &tp.Spec
+}
+
+// GetStatus implements TektonComponent
+func (tp *TektonConfig) GetStatus() TektonComponentStatus {
+	return &tp.Status
+}
+
+// TektonConfigSpec defines the desired state of TektonConfig
+type TektonConfigSpec struct {
+	Profile    string `json:"profile,omitempty"`
+	CommonSpec `json:",inline"`
+}
+
+// TektonConfigStatus defines the observed state of TektonConfig
+type TektonConfigStatus struct {
+	duckv1.Status `json:",inline"`
+
+	// The version of the installed release
+	// +optional
+	Version string `json:"version,omitempty"`
+
+	// The url links of the manifests, separated by comma
+	// +optional
+	Manifests []string `json:"manifests,omitempty"`
+}
+
+// TektonConfigList contains a list of TektonConfig
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type TektonConfigList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []TektonConfig `json:"items"`
+}
diff --git a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go
index 3da4ce3e3..6dce81812 100644
--- a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go
@@ -140,6 +140,106 @@ func (in *TektonAddonStatus) DeepCopy() *TektonAddonStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TektonConfig) DeepCopyInto(out *TektonConfig) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	out.Spec = in.Spec
+	in.Status.DeepCopyInto(&out.Status)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TektonConfig.
+func (in *TektonConfig) DeepCopy() *TektonConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(TektonConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *TektonConfig) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TektonConfigList) DeepCopyInto(out *TektonConfigList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]TektonConfig, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TektonConfigList.
+func (in *TektonConfigList) DeepCopy() *TektonConfigList {
+	if in == nil {
+		return nil
+	}
+	out := new(TektonConfigList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *TektonConfigList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TektonConfigSpec) DeepCopyInto(out *TektonConfigSpec) {
+	*out = *in
+	out.CommonSpec = in.CommonSpec
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TektonConfigSpec.
+func (in *TektonConfigSpec) DeepCopy() *TektonConfigSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(TektonConfigSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TektonConfigStatus) DeepCopyInto(out *TektonConfigStatus) {
+	*out = *in
+	in.Status.DeepCopyInto(&out.Status)
+	if in.Manifests != nil {
+		in, out := &in.Manifests, &out.Manifests
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TektonConfigStatus.
+func (in *TektonConfigStatus) DeepCopy() *TektonConfigStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(TektonConfigStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *TektonDashboard) DeepCopyInto(out *TektonDashboard) {
 	*out = *in
diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go
index c904fcfd9..5848aba74 100644
--- a/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go
+++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go
@@ -32,6 +32,10 @@ func (c *FakeOperatorV1alpha1) TektonAddons() v1alpha1.TektonAddonInterface {
 	return &FakeTektonAddons{c}
 }
 
+func (c *FakeOperatorV1alpha1) TektonConfigs() v1alpha1.TektonConfigInterface {
+	return &FakeTektonConfigs{c}
+}
+
 func (c *FakeOperatorV1alpha1) TektonDashboards() v1alpha1.TektonDashboardInterface {
 	return &FakeTektonDashboards{c}
 }
diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_tektonconfig.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_tektonconfig.go
new file mode 100644
index 000000000..de465975e
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/fake/fake_tektonconfig.go
@@ -0,0 +1,133 @@
+/*
+Copyright 2020 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1alpha1 "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeTektonConfigs implements TektonConfigInterface
+type FakeTektonConfigs struct {
+	Fake *FakeOperatorV1alpha1
+}
+
+var tektonconfigsResource = schema.GroupVersionResource{Group: "operator.tekton.dev", Version: "v1alpha1", Resource: "tektonconfigs"}
+
+var tektonconfigsKind = schema.GroupVersionKind{Group: "operator.tekton.dev", Version: "v1alpha1", Kind: "TektonConfig"}
+
+// Get takes name of the tektonConfig, and returns the corresponding tektonConfig object, and an error if there is any.
+func (c *FakeTektonConfigs) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.TektonConfig, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootGetAction(tektonconfigsResource, name), &v1alpha1.TektonConfig{})
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.TektonConfig), err
+}
+
+// List takes label and field selectors, and returns the list of TektonConfigs that match those selectors.
+func (c *FakeTektonConfigs) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.TektonConfigList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootListAction(tektonconfigsResource, tektonconfigsKind, opts), &v1alpha1.TektonConfigList{})
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.TektonConfigList{ListMeta: obj.(*v1alpha1.TektonConfigList).ListMeta}
+	for _, item := range obj.(*v1alpha1.TektonConfigList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested tektonConfigs.
+func (c *FakeTektonConfigs) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewRootWatchAction(tektonconfigsResource, opts))
+}
+
+// Create takes the representation of a tektonConfig and creates it.  Returns the server's representation of the tektonConfig, and an error, if there is any.
+func (c *FakeTektonConfigs) Create(ctx context.Context, tektonConfig *v1alpha1.TektonConfig, opts v1.CreateOptions) (result *v1alpha1.TektonConfig, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootCreateAction(tektonconfigsResource, tektonConfig), &v1alpha1.TektonConfig{})
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.TektonConfig), err
+}
+
+// Update takes the representation of a tektonConfig and updates it. Returns the server's representation of the tektonConfig, and an error, if there is any.
+func (c *FakeTektonConfigs) Update(ctx context.Context, tektonConfig *v1alpha1.TektonConfig, opts v1.UpdateOptions) (result *v1alpha1.TektonConfig, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootUpdateAction(tektonconfigsResource, tektonConfig), &v1alpha1.TektonConfig{})
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.TektonConfig), err
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *FakeTektonConfigs) UpdateStatus(ctx context.Context, tektonConfig *v1alpha1.TektonConfig, opts v1.UpdateOptions) (*v1alpha1.TektonConfig, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootUpdateSubresourceAction(tektonconfigsResource, "status", tektonConfig), &v1alpha1.TektonConfig{})
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.TektonConfig), err
+}
+
+// Delete takes name of the tektonConfig and deletes it. Returns an error if one occurs.
+func (c *FakeTektonConfigs) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewRootDeleteAction(tektonconfigsResource, name), &v1alpha1.TektonConfig{})
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeTektonConfigs) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewRootDeleteCollectionAction(tektonconfigsResource, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.TektonConfigList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched tektonConfig.
+func (c *FakeTektonConfigs) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.TektonConfig, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewRootPatchSubresourceAction(tektonconfigsResource, name, pt, data, subresources...), &v1alpha1.TektonConfig{})
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.TektonConfig), err
+}
diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go
index 9adc8bb2a..6d5a9b6a6 100644
--- a/pkg/client/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go
+++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go
@@ -20,6 +20,8 @@ package v1alpha1
 
 type TektonAddonExpansion interface{}
 
+type TektonConfigExpansion interface{}
+
 type TektonDashboardExpansion interface{}
 
 type TektonPipelineExpansion interface{}
diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/operator_client.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/operator_client.go
index a147fac2e..a76b2c2e8 100644
--- a/pkg/client/clientset/versioned/typed/operator/v1alpha1/operator_client.go
+++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/operator_client.go
@@ -27,6 +27,7 @@ import (
 type OperatorV1alpha1Interface interface {
 	RESTClient() rest.Interface
 	TektonAddonsGetter
+	TektonConfigsGetter
 	TektonDashboardsGetter
 	TektonPipelinesGetter
 	TektonTriggersGetter
@@ -41,6 +42,10 @@ func (c *OperatorV1alpha1Client) TektonAddons() TektonAddonInterface {
 	return newTektonAddons(c)
 }
 
+func (c *OperatorV1alpha1Client) TektonConfigs() TektonConfigInterface {
+	return newTektonConfigs(c)
+}
+
 func (c *OperatorV1alpha1Client) TektonDashboards() TektonDashboardInterface {
 	return newTektonDashboards(c)
 }
diff --git a/pkg/client/clientset/versioned/typed/operator/v1alpha1/tektonconfig.go b/pkg/client/clientset/versioned/typed/operator/v1alpha1/tektonconfig.go
new file mode 100644
index 000000000..67facc513
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/operator/v1alpha1/tektonconfig.go
@@ -0,0 +1,184 @@
+/*
+Copyright 2020 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	"time"
+
+	v1alpha1 "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	scheme "github.com/tektoncd/operator/pkg/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// TektonConfigsGetter has a method to return a TektonConfigInterface.
+// A group's client should implement this interface.
+type TektonConfigsGetter interface {
+	TektonConfigs() TektonConfigInterface
+}
+
+// TektonConfigInterface has methods to work with TektonConfig resources.
+type TektonConfigInterface interface {
+	Create(ctx context.Context, tektonConfig *v1alpha1.TektonConfig, opts v1.CreateOptions) (*v1alpha1.TektonConfig, error)
+	Update(ctx context.Context, tektonConfig *v1alpha1.TektonConfig, opts v1.UpdateOptions) (*v1alpha1.TektonConfig, error)
+	UpdateStatus(ctx context.Context, tektonConfig *v1alpha1.TektonConfig, opts v1.UpdateOptions) (*v1alpha1.TektonConfig, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.TektonConfig, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.TektonConfigList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.TektonConfig, err error)
+	TektonConfigExpansion
+}
+
+// tektonConfigs implements TektonConfigInterface
+type tektonConfigs struct {
+	client rest.Interface
+}
+
+// newTektonConfigs returns a TektonConfigs
+func newTektonConfigs(c *OperatorV1alpha1Client) *tektonConfigs {
+	return &tektonConfigs{
+		client: c.RESTClient(),
+	}
+}
+
+// Get takes name of the tektonConfig, and returns the corresponding tektonConfig object, and an error if there is any.
+func (c *tektonConfigs) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.TektonConfig, err error) {
+	result = &v1alpha1.TektonConfig{}
+	err = c.client.Get().
+		Resource("tektonconfigs").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of TektonConfigs that match those selectors.
+func (c *tektonConfigs) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.TektonConfigList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1alpha1.TektonConfigList{}
+	err = c.client.Get().
+		Resource("tektonconfigs").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested tektonConfigs.
+func (c *tektonConfigs) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Resource("tektonconfigs").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a tektonConfig and creates it.  Returns the server's representation of the tektonConfig, and an error, if there is any.
+func (c *tektonConfigs) Create(ctx context.Context, tektonConfig *v1alpha1.TektonConfig, opts v1.CreateOptions) (result *v1alpha1.TektonConfig, err error) {
+	result = &v1alpha1.TektonConfig{}
+	err = c.client.Post().
+		Resource("tektonconfigs").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(tektonConfig).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a tektonConfig and updates it. Returns the server's representation of the tektonConfig, and an error, if there is any.
+func (c *tektonConfigs) Update(ctx context.Context, tektonConfig *v1alpha1.TektonConfig, opts v1.UpdateOptions) (result *v1alpha1.TektonConfig, err error) {
+	result = &v1alpha1.TektonConfig{}
+	err = c.client.Put().
+		Resource("tektonconfigs").
+		Name(tektonConfig.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(tektonConfig).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *tektonConfigs) UpdateStatus(ctx context.Context, tektonConfig *v1alpha1.TektonConfig, opts v1.UpdateOptions) (result *v1alpha1.TektonConfig, err error) {
+	result = &v1alpha1.TektonConfig{}
+	err = c.client.Put().
+		Resource("tektonconfigs").
+		Name(tektonConfig.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(tektonConfig).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the tektonConfig and deletes it. Returns an error if one occurs.
+func (c *tektonConfigs) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Resource("tektonconfigs").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *tektonConfigs) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Resource("tektonconfigs").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched tektonConfig.
+func (c *tektonConfigs) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.TektonConfig, err error) {
+	result = &v1alpha1.TektonConfig{}
+	err = c.client.Patch(pt).
+		Resource("tektonconfigs").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go
index 6473014a6..547396d4e 100644
--- a/pkg/client/informers/externalversions/generic.go
+++ b/pkg/client/informers/externalversions/generic.go
@@ -55,6 +55,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 	// Group=operator.tekton.dev, Version=v1alpha1
 	case v1alpha1.SchemeGroupVersion.WithResource("tektonaddons"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1alpha1().TektonAddons().Informer()}, nil
+	case v1alpha1.SchemeGroupVersion.WithResource("tektonconfigs"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1alpha1().TektonConfigs().Informer()}, nil
 	case v1alpha1.SchemeGroupVersion.WithResource("tektondashboards"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1alpha1().TektonDashboards().Informer()}, nil
 	case v1alpha1.SchemeGroupVersion.WithResource("tektonpipelines"):
diff --git a/pkg/client/informers/externalversions/operator/v1alpha1/interface.go b/pkg/client/informers/externalversions/operator/v1alpha1/interface.go
index 0ee480de4..b18fcafc7 100644
--- a/pkg/client/informers/externalversions/operator/v1alpha1/interface.go
+++ b/pkg/client/informers/externalversions/operator/v1alpha1/interface.go
@@ -26,6 +26,8 @@ import (
 type Interface interface {
 	// TektonAddons returns a TektonAddonInformer.
 	TektonAddons() TektonAddonInformer
+	// TektonConfigs returns a TektonConfigInformer.
+	TektonConfigs() TektonConfigInformer
 	// TektonDashboards returns a TektonDashboardInformer.
 	TektonDashboards() TektonDashboardInformer
 	// TektonPipelines returns a TektonPipelineInformer.
@@ -50,6 +52,11 @@ func (v *version) TektonAddons() TektonAddonInformer {
 	return &tektonAddonInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
 }
 
+// TektonConfigs returns a TektonConfigInformer.
+func (v *version) TektonConfigs() TektonConfigInformer {
+	return &tektonConfigInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
+}
+
 // TektonDashboards returns a TektonDashboardInformer.
 func (v *version) TektonDashboards() TektonDashboardInformer {
 	return &tektonDashboardInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
diff --git a/pkg/client/informers/externalversions/operator/v1alpha1/tektonconfig.go b/pkg/client/informers/externalversions/operator/v1alpha1/tektonconfig.go
new file mode 100644
index 000000000..bc7ca6fc0
--- /dev/null
+++ b/pkg/client/informers/externalversions/operator/v1alpha1/tektonconfig.go
@@ -0,0 +1,89 @@
+/*
+Copyright 2020 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.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	time "time"
+
+	operatorv1alpha1 "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	versioned "github.com/tektoncd/operator/pkg/client/clientset/versioned"
+	internalinterfaces "github.com/tektoncd/operator/pkg/client/informers/externalversions/internalinterfaces"
+	v1alpha1 "github.com/tektoncd/operator/pkg/client/listers/operator/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// TektonConfigInformer provides access to a shared informer and lister for
+// TektonConfigs.
+type TektonConfigInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1alpha1.TektonConfigLister
+}
+
+type tektonConfigInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// NewTektonConfigInformer constructs a new informer for TektonConfig type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewTektonConfigInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredTektonConfigInformer(client, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredTektonConfigInformer constructs a new informer for TektonConfig type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredTektonConfigInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.OperatorV1alpha1().TektonConfigs().List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.OperatorV1alpha1().TektonConfigs().Watch(context.TODO(), options)
+			},
+		},
+		&operatorv1alpha1.TektonConfig{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *tektonConfigInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredTektonConfigInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *tektonConfigInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&operatorv1alpha1.TektonConfig{}, f.defaultInformer)
+}
+
+func (f *tektonConfigInformer) Lister() v1alpha1.TektonConfigLister {
+	return v1alpha1.NewTektonConfigLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/injection/informers/operator/v1alpha1/tektonconfig/fake/fake.go b/pkg/client/injection/informers/operator/v1alpha1/tektonconfig/fake/fake.go
new file mode 100644
index 000000000..219ffe712
--- /dev/null
+++ b/pkg/client/injection/informers/operator/v1alpha1/tektonconfig/fake/fake.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2020 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.
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	context "context"
+
+	fake "github.com/tektoncd/operator/pkg/client/injection/informers/factory/fake"
+	tektonconfig "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonconfig"
+	controller "knative.dev/pkg/controller"
+	injection "knative.dev/pkg/injection"
+)
+
+var Get = tektonconfig.Get
+
+func init() {
+	injection.Fake.RegisterInformer(withInformer)
+}
+
+func withInformer(ctx context.Context) (context.Context, controller.Informer) {
+	f := fake.Get(ctx)
+	inf := f.Operator().V1alpha1().TektonConfigs()
+	return context.WithValue(ctx, tektonconfig.Key{}, inf), inf.Informer()
+}
diff --git a/pkg/client/injection/informers/operator/v1alpha1/tektonconfig/tektonconfig.go b/pkg/client/injection/informers/operator/v1alpha1/tektonconfig/tektonconfig.go
new file mode 100644
index 000000000..a5e617bf6
--- /dev/null
+++ b/pkg/client/injection/informers/operator/v1alpha1/tektonconfig/tektonconfig.go
@@ -0,0 +1,52 @@
+/*
+Copyright 2020 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.
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package tektonconfig
+
+import (
+	context "context"
+
+	v1alpha1 "github.com/tektoncd/operator/pkg/client/informers/externalversions/operator/v1alpha1"
+	factory "github.com/tektoncd/operator/pkg/client/injection/informers/factory"
+	controller "knative.dev/pkg/controller"
+	injection "knative.dev/pkg/injection"
+	logging "knative.dev/pkg/logging"
+)
+
+func init() {
+	injection.Default.RegisterInformer(withInformer)
+}
+
+// Key is used for associating the Informer inside the context.Context.
+type Key struct{}
+
+func withInformer(ctx context.Context) (context.Context, controller.Informer) {
+	f := factory.Get(ctx)
+	inf := f.Operator().V1alpha1().TektonConfigs()
+	return context.WithValue(ctx, Key{}, inf), inf.Informer()
+}
+
+// Get extracts the typed informer from the context.
+func Get(ctx context.Context) v1alpha1.TektonConfigInformer {
+	untyped := ctx.Value(Key{})
+	if untyped == nil {
+		logging.FromContext(ctx).Panic(
+			"Unable to fetch github.com/tektoncd/operator/pkg/client/informers/externalversions/operator/v1alpha1.TektonConfigInformer from context.")
+	}
+	return untyped.(v1alpha1.TektonConfigInformer)
+}
diff --git a/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig/controller.go b/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig/controller.go
new file mode 100644
index 000000000..542c93b38
--- /dev/null
+++ b/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig/controller.go
@@ -0,0 +1,142 @@
+/*
+Copyright 2020 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.
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package tektonconfig
+
+import (
+	context "context"
+	fmt "fmt"
+	reflect "reflect"
+	strings "strings"
+
+	versionedscheme "github.com/tektoncd/operator/pkg/client/clientset/versioned/scheme"
+	client "github.com/tektoncd/operator/pkg/client/injection/client"
+	tektonconfig "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonconfig"
+	corev1 "k8s.io/api/core/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	scheme "k8s.io/client-go/kubernetes/scheme"
+	v1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	record "k8s.io/client-go/tools/record"
+	kubeclient "knative.dev/pkg/client/injection/kube/client"
+	controller "knative.dev/pkg/controller"
+	logging "knative.dev/pkg/logging"
+	reconciler "knative.dev/pkg/reconciler"
+)
+
+const (
+	defaultControllerAgentName = "tektonconfig-controller"
+	defaultFinalizerName       = "tektonconfigs.operator.tekton.dev"
+)
+
+// NewImpl returns a controller.Impl that handles queuing and feeding work from
+// the queue through an implementation of controller.Reconciler, delegating to
+// the provided Interface and optional Finalizer methods. OptionsFn is used to return
+// controller.Options to be used but the internal reconciler.
+func NewImpl(ctx context.Context, r Interface, optionsFns ...controller.OptionsFn) *controller.Impl {
+	logger := logging.FromContext(ctx)
+
+	// Check the options function input. It should be 0 or 1.
+	if len(optionsFns) > 1 {
+		logger.Fatalf("up to one options function is supported, found %d", len(optionsFns))
+	}
+
+	tektonconfigInformer := tektonconfig.Get(ctx)
+
+	lister := tektonconfigInformer.Lister()
+
+	rec := &reconcilerImpl{
+		LeaderAwareFuncs: reconciler.LeaderAwareFuncs{
+			PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error {
+				all, err := lister.List(labels.Everything())
+				if err != nil {
+					return err
+				}
+				for _, elt := range all {
+					// TODO: Consider letting users specify a filter in options.
+					enq(bkt, types.NamespacedName{
+						Namespace: elt.GetNamespace(),
+						Name:      elt.GetName(),
+					})
+				}
+				return nil
+			},
+		},
+		Client:        client.Get(ctx),
+		Lister:        lister,
+		reconciler:    r,
+		finalizerName: defaultFinalizerName,
+	}
+
+	t := reflect.TypeOf(r).Elem()
+	queueName := fmt.Sprintf("%s.%s", strings.ReplaceAll(t.PkgPath(), "/", "-"), t.Name())
+
+	impl := controller.NewImpl(rec, logger, queueName)
+	agentName := defaultControllerAgentName
+
+	// Pass impl to the options. Save any optional results.
+	for _, fn := range optionsFns {
+		opts := fn(impl)
+		if opts.ConfigStore != nil {
+			rec.configStore = opts.ConfigStore
+		}
+		if opts.FinalizerName != "" {
+			rec.finalizerName = opts.FinalizerName
+		}
+		if opts.AgentName != "" {
+			agentName = opts.AgentName
+		}
+		if opts.SkipStatusUpdates {
+			rec.skipStatusUpdates = true
+		}
+	}
+
+	rec.Recorder = createRecorder(ctx, agentName)
+
+	return impl
+}
+
+func createRecorder(ctx context.Context, agentName string) record.EventRecorder {
+	logger := logging.FromContext(ctx)
+
+	recorder := controller.GetEventRecorder(ctx)
+	if recorder == nil {
+		// Create event broadcaster
+		logger.Debug("Creating event broadcaster")
+		eventBroadcaster := record.NewBroadcaster()
+		watches := []watch.Interface{
+			eventBroadcaster.StartLogging(logger.Named("event-broadcaster").Infof),
+			eventBroadcaster.StartRecordingToSink(
+				&v1.EventSinkImpl{Interface: kubeclient.Get(ctx).CoreV1().Events("")}),
+		}
+		recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: agentName})
+		go func() {
+			<-ctx.Done()
+			for _, w := range watches {
+				w.Stop()
+			}
+		}()
+	}
+
+	return recorder
+}
+
+func init() {
+	versionedscheme.AddToScheme(scheme.Scheme)
+}
diff --git a/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig/reconciler.go b/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig/reconciler.go
new file mode 100644
index 000000000..c7ad9c799
--- /dev/null
+++ b/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig/reconciler.go
@@ -0,0 +1,441 @@
+/*
+Copyright 2020 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.
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package tektonconfig
+
+import (
+	context "context"
+	json "encoding/json"
+	fmt "fmt"
+	reflect "reflect"
+
+	v1alpha1 "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	versioned "github.com/tektoncd/operator/pkg/client/clientset/versioned"
+	operatorv1alpha1 "github.com/tektoncd/operator/pkg/client/listers/operator/v1alpha1"
+	zap "go.uber.org/zap"
+	v1 "k8s.io/api/core/v1"
+	equality "k8s.io/apimachinery/pkg/api/equality"
+	errors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	types "k8s.io/apimachinery/pkg/types"
+	sets "k8s.io/apimachinery/pkg/util/sets"
+	record "k8s.io/client-go/tools/record"
+	controller "knative.dev/pkg/controller"
+	kmp "knative.dev/pkg/kmp"
+	logging "knative.dev/pkg/logging"
+	reconciler "knative.dev/pkg/reconciler"
+)
+
+// Interface defines the strongly typed interfaces to be implemented by a
+// controller reconciling v1alpha1.TektonConfig.
+type Interface interface {
+	// ReconcileKind implements custom logic to reconcile v1alpha1.TektonConfig. Any changes
+	// to the objects .Status or .Finalizers will be propagated to the stored
+	// object. It is recommended that implementors do not call any update calls
+	// for the Kind inside of ReconcileKind, it is the responsibility of the calling
+	// controller to propagate those properties. The resource passed to ReconcileKind
+	// will always have an empty deletion timestamp.
+	ReconcileKind(ctx context.Context, o *v1alpha1.TektonConfig) reconciler.Event
+}
+
+// Finalizer defines the strongly typed interfaces to be implemented by a
+// controller finalizing v1alpha1.TektonConfig.
+type Finalizer interface {
+	// FinalizeKind implements custom logic to finalize v1alpha1.TektonConfig. Any changes
+	// to the objects .Status or .Finalizers will be ignored. Returning a nil or
+	// Normal type reconciler.Event will allow the finalizer to be deleted on
+	// the resource. The resource passed to FinalizeKind will always have a set
+	// deletion timestamp.
+	FinalizeKind(ctx context.Context, o *v1alpha1.TektonConfig) reconciler.Event
+}
+
+// ReadOnlyInterface defines the strongly typed interfaces to be implemented by a
+// controller reconciling v1alpha1.TektonConfig if they want to process resources for which
+// they are not the leader.
+type ReadOnlyInterface interface {
+	// ObserveKind implements logic to observe v1alpha1.TektonConfig.
+	// This method should not write to the API.
+	ObserveKind(ctx context.Context, o *v1alpha1.TektonConfig) reconciler.Event
+}
+
+// ReadOnlyFinalizer defines the strongly typed interfaces to be implemented by a
+// controller finalizing v1alpha1.TektonConfig if they want to process tombstoned resources
+// even when they are not the leader.  Due to the nature of how finalizers are handled
+// there are no guarantees that this will be called.
+type ReadOnlyFinalizer interface {
+	// ObserveFinalizeKind implements custom logic to observe the final state of v1alpha1.TektonConfig.
+	// This method should not write to the API.
+	ObserveFinalizeKind(ctx context.Context, o *v1alpha1.TektonConfig) reconciler.Event
+}
+
+type doReconcile func(ctx context.Context, o *v1alpha1.TektonConfig) reconciler.Event
+
+// reconcilerImpl implements controller.Reconciler for v1alpha1.TektonConfig resources.
+type reconcilerImpl struct {
+	// LeaderAwareFuncs is inlined to help us implement reconciler.LeaderAware
+	reconciler.LeaderAwareFuncs
+
+	// Client is used to write back status updates.
+	Client versioned.Interface
+
+	// Listers index properties about resources
+	Lister operatorv1alpha1.TektonConfigLister
+
+	// Recorder is an event recorder for recording Event resources to the
+	// Kubernetes API.
+	Recorder record.EventRecorder
+
+	// configStore allows for decorating a context with config maps.
+	// +optional
+	configStore reconciler.ConfigStore
+
+	// reconciler is the implementation of the business logic of the resource.
+	reconciler Interface
+
+	// finalizerName is the name of the finalizer to reconcile.
+	finalizerName string
+
+	// skipStatusUpdates configures whether or not this reconciler automatically updates
+	// the status of the reconciled resource.
+	skipStatusUpdates bool
+}
+
+// Check that our Reconciler implements controller.Reconciler
+var _ controller.Reconciler = (*reconcilerImpl)(nil)
+
+// Check that our generated Reconciler is always LeaderAware.
+var _ reconciler.LeaderAware = (*reconcilerImpl)(nil)
+
+func NewReconciler(ctx context.Context, logger *zap.SugaredLogger, client versioned.Interface, lister operatorv1alpha1.TektonConfigLister, recorder record.EventRecorder, r Interface, options ...controller.Options) controller.Reconciler {
+	// Check the options function input. It should be 0 or 1.
+	if len(options) > 1 {
+		logger.Fatalf("up to one options struct is supported, found %d", len(options))
+	}
+
+	// Fail fast when users inadvertently implement the other LeaderAware interface.
+	// For the typed reconcilers, Promote shouldn't take any arguments.
+	if _, ok := r.(reconciler.LeaderAware); ok {
+		logger.Fatalf("%T implements the incorrect LeaderAware interface.  Promote() should not take an argument as genreconciler handles the enqueuing automatically.", r)
+	}
+	// TODO: Consider validating when folks implement ReadOnlyFinalizer, but not Finalizer.
+
+	rec := &reconcilerImpl{
+		LeaderAwareFuncs: reconciler.LeaderAwareFuncs{
+			PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error {
+				all, err := lister.List(labels.Everything())
+				if err != nil {
+					return err
+				}
+				for _, elt := range all {
+					// TODO: Consider letting users specify a filter in options.
+					enq(bkt, types.NamespacedName{
+						Namespace: elt.GetNamespace(),
+						Name:      elt.GetName(),
+					})
+				}
+				return nil
+			},
+		},
+		Client:        client,
+		Lister:        lister,
+		Recorder:      recorder,
+		reconciler:    r,
+		finalizerName: defaultFinalizerName,
+	}
+
+	for _, opts := range options {
+		if opts.ConfigStore != nil {
+			rec.configStore = opts.ConfigStore
+		}
+		if opts.FinalizerName != "" {
+			rec.finalizerName = opts.FinalizerName
+		}
+		if opts.SkipStatusUpdates {
+			rec.skipStatusUpdates = true
+		}
+	}
+
+	return rec
+}
+
+// Reconcile implements controller.Reconciler
+func (r *reconcilerImpl) Reconcile(ctx context.Context, key string) error {
+	logger := logging.FromContext(ctx)
+
+	// Initialize the reconciler state. This will convert the namespace/name
+	// string into a distinct namespace and name, determin if this instance of
+	// the reconciler is the leader, and any additional interfaces implemented
+	// by the reconciler. Returns an error is the resource key is invalid.
+	s, err := newState(key, r)
+	if err != nil {
+		logger.Errorf("invalid resource key: %s", key)
+		return nil
+	}
+
+	// If we are not the leader, and we don't implement either ReadOnly
+	// observer interfaces, then take a fast-path out.
+	if s.isNotLeaderNorObserver() {
+		return nil
+	}
+
+	// If configStore is set, attach the frozen configuration to the context.
+	if r.configStore != nil {
+		ctx = r.configStore.ToContext(ctx)
+	}
+
+	// Add the recorder to context.
+	ctx = controller.WithEventRecorder(ctx, r.Recorder)
+
+	// Get the resource with this namespace/name.
+
+	getter := r.Lister
+
+	original, err := getter.Get(s.name)
+
+	if errors.IsNotFound(err) {
+		// The resource may no longer exist, in which case we stop processing.
+		logger.Debugf("resource %q no longer exists", key)
+		return nil
+	} else if err != nil {
+		return err
+	}
+
+	// Don't modify the informers copy.
+	resource := original.DeepCopy()
+
+	var reconcileEvent reconciler.Event
+
+	name, do := s.reconcileMethodFor(resource)
+	// Append the target method to the logger.
+	logger = logger.With(zap.String("targetMethod", name))
+	switch name {
+	case reconciler.DoReconcileKind:
+		// Append the target method to the logger.
+		logger = logger.With(zap.String("targetMethod", "ReconcileKind"))
+
+		// Set and update the finalizer on resource if r.reconciler
+		// implements Finalizer.
+		if resource, err = r.setFinalizerIfFinalizer(ctx, resource); err != nil {
+			return fmt.Errorf("failed to set finalizers: %w", err)
+		}
+
+		// Reconcile this copy of the resource and then write back any status
+		// updates regardless of whether the reconciliation errored out.
+		reconcileEvent = do(ctx, resource)
+
+	case reconciler.DoFinalizeKind:
+		// For finalizing reconcilers, if this resource being marked for deletion
+		// and reconciled cleanly (nil or normal event), remove the finalizer.
+		reconcileEvent = do(ctx, resource)
+
+		if resource, err = r.clearFinalizer(ctx, resource, reconcileEvent); err != nil {
+			return fmt.Errorf("failed to clear finalizers: %w", err)
+		}
+
+	case reconciler.DoObserveKind, reconciler.DoObserveFinalizeKind:
+		// Observe any changes to this resource, since we are not the leader.
+		reconcileEvent = do(ctx, resource)
+
+	}
+
+	// Synchronize the status.
+	switch {
+	case r.skipStatusUpdates:
+		// This reconciler implementation is configured to skip resource updates.
+		// This may mean this reconciler does not observe spec, but reconciles external changes.
+	case equality.Semantic.DeepEqual(original.Status, resource.Status):
+		// If we didn't change anything then don't call updateStatus.
+		// This is important because the copy we loaded from the injectionInformer's
+		// cache may be stale and we don't want to overwrite a prior update
+		// to status with this stale state.
+	case !s.isLeader:
+		// High-availability reconcilers may have many replicas watching the resource, but only
+		// the elected leader is expected to write modifications.
+		logger.Warn("Saw status changes when we aren't the leader!")
+	default:
+		if err = r.updateStatus(ctx, original, resource); err != nil {
+			logger.Warnw("Failed to update resource status", zap.Error(err))
+			r.Recorder.Eventf(resource, v1.EventTypeWarning, "UpdateFailed",
+				"Failed to update status for %q: %v", resource.Name, err)
+			return err
+		}
+	}
+
+	// Report the reconciler event, if any.
+	if reconcileEvent != nil {
+		var event *reconciler.ReconcilerEvent
+		if reconciler.EventAs(reconcileEvent, &event) {
+			logger.Infow("Returned an event", zap.Any("event", reconcileEvent))
+			r.Recorder.Eventf(resource, event.EventType, event.Reason, event.Format, event.Args...)
+
+			// the event was wrapped inside an error, consider the reconciliation as failed
+			if _, isEvent := reconcileEvent.(*reconciler.ReconcilerEvent); !isEvent {
+				return reconcileEvent
+			}
+			return nil
+		}
+
+		logger.Errorw("Returned an error", zap.Error(reconcileEvent))
+		r.Recorder.Event(resource, v1.EventTypeWarning, "InternalError", reconcileEvent.Error())
+		return reconcileEvent
+	}
+
+	return nil
+}
+
+func (r *reconcilerImpl) updateStatus(ctx context.Context, existing *v1alpha1.TektonConfig, desired *v1alpha1.TektonConfig) error {
+	existing = existing.DeepCopy()
+	return reconciler.RetryUpdateConflicts(func(attempts int) (err error) {
+		// The first iteration tries to use the injectionInformer's state, subsequent attempts fetch the latest state via API.
+		if attempts > 0 {
+
+			getter := r.Client.OperatorV1alpha1().TektonConfigs()
+
+			existing, err = getter.Get(ctx, desired.Name, metav1.GetOptions{})
+			if err != nil {
+				return err
+			}
+		}
+
+		// If there's nothing to update, just return.
+		if reflect.DeepEqual(existing.Status, desired.Status) {
+			return nil
+		}
+
+		if diff, err := kmp.SafeDiff(existing.Status, desired.Status); err == nil && diff != "" {
+			logging.FromContext(ctx).Debugf("Updating status with: %s", diff)
+		}
+
+		existing.Status = desired.Status
+
+		updater := r.Client.OperatorV1alpha1().TektonConfigs()
+
+		_, err = updater.UpdateStatus(ctx, existing, metav1.UpdateOptions{})
+		return err
+	})
+}
+
+// updateFinalizersFiltered will update the Finalizers of the resource.
+// TODO: this method could be generic and sync all finalizers. For now it only
+// updates defaultFinalizerName or its override.
+func (r *reconcilerImpl) updateFinalizersFiltered(ctx context.Context, resource *v1alpha1.TektonConfig) (*v1alpha1.TektonConfig, error) {
+
+	getter := r.Lister
+
+	actual, err := getter.Get(resource.Name)
+	if err != nil {
+		return resource, err
+	}
+
+	// Don't modify the informers copy.
+	existing := actual.DeepCopy()
+
+	var finalizers []string
+
+	// If there's nothing to update, just return.
+	existingFinalizers := sets.NewString(existing.Finalizers...)
+	desiredFinalizers := sets.NewString(resource.Finalizers...)
+
+	if desiredFinalizers.Has(r.finalizerName) {
+		if existingFinalizers.Has(r.finalizerName) {
+			// Nothing to do.
+			return resource, nil
+		}
+		// Add the finalizer.
+		finalizers = append(existing.Finalizers, r.finalizerName)
+	} else {
+		if !existingFinalizers.Has(r.finalizerName) {
+			// Nothing to do.
+			return resource, nil
+		}
+		// Remove the finalizer.
+		existingFinalizers.Delete(r.finalizerName)
+		finalizers = existingFinalizers.List()
+	}
+
+	mergePatch := map[string]interface{}{
+		"metadata": map[string]interface{}{
+			"finalizers":      finalizers,
+			"resourceVersion": existing.ResourceVersion,
+		},
+	}
+
+	patch, err := json.Marshal(mergePatch)
+	if err != nil {
+		return resource, err
+	}
+
+	patcher := r.Client.OperatorV1alpha1().TektonConfigs()
+
+	resourceName := resource.Name
+	updated, err := patcher.Patch(ctx, resourceName, types.MergePatchType, patch, metav1.PatchOptions{})
+	if err != nil {
+		r.Recorder.Eventf(existing, v1.EventTypeWarning, "FinalizerUpdateFailed",
+			"Failed to update finalizers for %q: %v", resourceName, err)
+	} else {
+		r.Recorder.Eventf(updated, v1.EventTypeNormal, "FinalizerUpdate",
+			"Updated %q finalizers", resource.GetName())
+	}
+	return updated, err
+}
+
+func (r *reconcilerImpl) setFinalizerIfFinalizer(ctx context.Context, resource *v1alpha1.TektonConfig) (*v1alpha1.TektonConfig, error) {
+	if _, ok := r.reconciler.(Finalizer); !ok {
+		return resource, nil
+	}
+
+	finalizers := sets.NewString(resource.Finalizers...)
+
+	// If this resource is not being deleted, mark the finalizer.
+	if resource.GetDeletionTimestamp().IsZero() {
+		finalizers.Insert(r.finalizerName)
+	}
+
+	resource.Finalizers = finalizers.List()
+
+	// Synchronize the finalizers filtered by r.finalizerName.
+	return r.updateFinalizersFiltered(ctx, resource)
+}
+
+func (r *reconcilerImpl) clearFinalizer(ctx context.Context, resource *v1alpha1.TektonConfig, reconcileEvent reconciler.Event) (*v1alpha1.TektonConfig, error) {
+	if _, ok := r.reconciler.(Finalizer); !ok {
+		return resource, nil
+	}
+	if resource.GetDeletionTimestamp().IsZero() {
+		return resource, nil
+	}
+
+	finalizers := sets.NewString(resource.Finalizers...)
+
+	if reconcileEvent != nil {
+		var event *reconciler.ReconcilerEvent
+		if reconciler.EventAs(reconcileEvent, &event) {
+			if event.EventType == v1.EventTypeNormal {
+				finalizers.Delete(r.finalizerName)
+			}
+		}
+	} else {
+		finalizers.Delete(r.finalizerName)
+	}
+
+	resource.Finalizers = finalizers.List()
+
+	// Synchronize the finalizers filtered by r.finalizerName.
+	return r.updateFinalizersFiltered(ctx, resource)
+}
diff --git a/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig/state.go b/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig/state.go
new file mode 100644
index 000000000..ac3e3e68a
--- /dev/null
+++ b/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig/state.go
@@ -0,0 +1,106 @@
+/*
+Copyright 2020 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.
+*/
+
+// Code generated by injection-gen. DO NOT EDIT.
+
+package tektonconfig
+
+import (
+	fmt "fmt"
+
+	v1alpha1 "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	types "k8s.io/apimachinery/pkg/types"
+	cache "k8s.io/client-go/tools/cache"
+	reconciler "knative.dev/pkg/reconciler"
+)
+
+// state is used to track the state of a reconciler in a single run.
+type state struct {
+	// Key is the original reconciliation key from the queue.
+	key string
+	// Namespace is the namespace split from the reconciliation key.
+	namespace string
+	// Namespace is the name split from the reconciliation key.
+	name string
+	// reconciler is the reconciler.
+	reconciler Interface
+	// rof is the read only interface cast of the reconciler.
+	roi ReadOnlyInterface
+	// IsROI (Read Only Interface) the reconciler only observes reconciliation.
+	isROI bool
+	// rof is the read only finalizer cast of the reconciler.
+	rof ReadOnlyFinalizer
+	// IsROF (Read Only Finalizer) the reconciler only observes finalize.
+	isROF bool
+	// IsLeader the instance of the reconciler is the elected leader.
+	isLeader bool
+}
+
+func newState(key string, r *reconcilerImpl) (*state, error) {
+	// Convert the namespace/name string into a distinct namespace and name
+	namespace, name, err := cache.SplitMetaNamespaceKey(key)
+	if err != nil {
+		return nil, fmt.Errorf("invalid resource key: %s", key)
+	}
+
+	roi, isROI := r.reconciler.(ReadOnlyInterface)
+	rof, isROF := r.reconciler.(ReadOnlyFinalizer)
+
+	isLeader := r.IsLeaderFor(types.NamespacedName{
+		Namespace: namespace,
+		Name:      name,
+	})
+
+	return &state{
+		key:        key,
+		namespace:  namespace,
+		name:       name,
+		reconciler: r.reconciler,
+		roi:        roi,
+		isROI:      isROI,
+		rof:        rof,
+		isROF:      isROF,
+		isLeader:   isLeader,
+	}, nil
+}
+
+// isNotLeaderNorObserver checks to see if this reconciler with the current
+// state is enabled to do any work or not.
+// isNotLeaderNorObserver returns true when there is no work possible for the
+// reconciler.
+func (s *state) isNotLeaderNorObserver() bool {
+	if !s.isLeader && !s.isROI && !s.isROF {
+		// If we are not the leader, and we don't implement either ReadOnly
+		// interface, then take a fast-path out.
+		return true
+	}
+	return false
+}
+
+func (s *state) reconcileMethodFor(o *v1alpha1.TektonConfig) (string, doReconcile) {
+	if o.GetDeletionTimestamp().IsZero() {
+		if s.isLeader {
+			return reconciler.DoReconcileKind, s.reconciler.ReconcileKind
+		} else if s.isROI {
+			return reconciler.DoObserveKind, s.roi.ObserveKind
+		}
+	} else if fin, ok := s.reconciler.(Finalizer); s.isLeader && ok {
+		return reconciler.DoFinalizeKind, fin.FinalizeKind
+	} else if !s.isLeader && s.isROF {
+		return reconciler.DoObserveFinalizeKind, s.rof.ObserveFinalizeKind
+	}
+	return "unknown", nil
+}
diff --git a/pkg/client/listers/operator/v1alpha1/expansion_generated.go b/pkg/client/listers/operator/v1alpha1/expansion_generated.go
index bbe5207be..a3dc2e67f 100644
--- a/pkg/client/listers/operator/v1alpha1/expansion_generated.go
+++ b/pkg/client/listers/operator/v1alpha1/expansion_generated.go
@@ -22,6 +22,10 @@ package v1alpha1
 // TektonAddonLister.
 type TektonAddonListerExpansion interface{}
 
+// TektonConfigListerExpansion allows custom methods to be added to
+// TektonConfigLister.
+type TektonConfigListerExpansion interface{}
+
 // TektonDashboardListerExpansion allows custom methods to be added to
 // TektonDashboardLister.
 type TektonDashboardListerExpansion interface{}
diff --git a/pkg/client/listers/operator/v1alpha1/tektonconfig.go b/pkg/client/listers/operator/v1alpha1/tektonconfig.go
new file mode 100644
index 000000000..77a0199df
--- /dev/null
+++ b/pkg/client/listers/operator/v1alpha1/tektonconfig.go
@@ -0,0 +1,65 @@
+/*
+Copyright 2020 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.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1alpha1 "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// TektonConfigLister helps list TektonConfigs.
+type TektonConfigLister interface {
+	// List lists all TektonConfigs in the indexer.
+	List(selector labels.Selector) (ret []*v1alpha1.TektonConfig, err error)
+	// Get retrieves the TektonConfig from the index for a given name.
+	Get(name string) (*v1alpha1.TektonConfig, error)
+	TektonConfigListerExpansion
+}
+
+// tektonConfigLister implements the TektonConfigLister interface.
+type tektonConfigLister struct {
+	indexer cache.Indexer
+}
+
+// NewTektonConfigLister returns a new TektonConfigLister.
+func NewTektonConfigLister(indexer cache.Indexer) TektonConfigLister {
+	return &tektonConfigLister{indexer: indexer}
+}
+
+// List lists all TektonConfigs in the indexer.
+func (s *tektonConfigLister) List(selector labels.Selector) (ret []*v1alpha1.TektonConfig, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.TektonConfig))
+	})
+	return ret, err
+}
+
+// Get retrieves the TektonConfig from the index for a given name.
+func (s *tektonConfigLister) Get(name string) (*v1alpha1.TektonConfig, error) {
+	obj, exists, err := s.indexer.GetByKey(name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1alpha1.Resource("tektonconfig"), name)
+	}
+	return obj.(*v1alpha1.TektonConfig), nil
+}
diff --git a/pkg/reconciler/common/common.go b/pkg/reconciler/common/common.go
index 274ba4458..571f0d67e 100644
--- a/pkg/reconciler/common/common.go
+++ b/pkg/reconciler/common/common.go
@@ -18,6 +18,7 @@ package common
 
 import (
 	"fmt"
+	"time"
 
 	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
 	informer "github.com/tektoncd/operator/pkg/client/informers/externalversions/operator/v1alpha1"
@@ -26,8 +27,16 @@ import (
 )
 
 var (
-	PipelineResourceName = "pipeline"
-	TriggerResourceName  = "trigger"
+	PipelineResourceName  = "pipeline"
+	TriggerResourceName   = "trigger"
+	DashboardResourceName = "dashboard"
+	AddonResourceName     = "addon"
+	ConfigResourceName    = "config"
+	ProfileBasic          = "basic"
+	ProfileDefault        = "default"
+	ProfileAll            = "all"
+	Interval              = 10 * time.Second
+	Timeout               = 1 * time.Minute
 )
 
 const (
diff --git a/pkg/reconciler/common/extensions.go b/pkg/reconciler/common/extensions.go
index 70286411d..d28871db4 100644
--- a/pkg/reconciler/common/extensions.go
+++ b/pkg/reconciler/common/extensions.go
@@ -26,7 +26,8 @@ import (
 // Extension enables platform-specific features
 type Extension interface {
 	Transformers(v1alpha1.TektonComponent) []mf.Transformer
-	Reconcile(context.Context, v1alpha1.TektonComponent) error
+	PreReconcile(context.Context, v1alpha1.TektonComponent) error
+	PostReconcile(context.Context, v1alpha1.TektonComponent) error
 	Finalize(context.Context, v1alpha1.TektonComponent) error
 }
 
@@ -43,7 +44,10 @@ type nilExtension struct{}
 func (nilExtension) Transformers(v1alpha1.TektonComponent) []mf.Transformer {
 	return nil
 }
-func (nilExtension) Reconcile(context.Context, v1alpha1.TektonComponent) error {
+func (nilExtension) PreReconcile(context.Context, v1alpha1.TektonComponent) error {
+	return nil
+}
+func (nilExtension) PostReconcile(context.Context, v1alpha1.TektonComponent) error {
 	return nil
 }
 func (nilExtension) Finalize(context.Context, v1alpha1.TektonComponent) error {
diff --git a/pkg/reconciler/kubernetes/tektonconfig/controller.go b/pkg/reconciler/kubernetes/tektonconfig/controller.go
new file mode 100644
index 000000000..01c1d4035
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonconfig/controller.go
@@ -0,0 +1,80 @@
+/*
+Copyright 2020 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 tektonconfig
+
+import (
+	"context"
+	"github.com/go-logr/zapr"
+	mfc "github.com/manifestival/client-go-client"
+	mf "github.com/manifestival/manifestival"
+	"go.uber.org/zap"
+	"k8s.io/client-go/tools/cache"
+
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	operatorclient "github.com/tektoncd/operator/pkg/client/injection/client"
+	tektonConfiginformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonconfig"
+	tektonConfigreconciler "github.com/tektoncd/operator/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	kubeclient "knative.dev/pkg/client/injection/kube/client"
+	deploymentinformer "knative.dev/pkg/client/injection/kube/informers/apps/v1/deployment"
+	"knative.dev/pkg/configmap"
+	"knative.dev/pkg/controller"
+	"knative.dev/pkg/injection"
+	"knative.dev/pkg/logging"
+)
+
+// NewController initializes the controller and is called by the generated code
+// Registers eventhandlers to enqueue events
+func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
+	return NewExtendedController(KubernetesExtension)(ctx, cmw)
+}
+
+// NewExtendedController returns a controller extended to a specific platform
+func NewExtendedController(generator common.ExtensionGenerator) injection.ControllerConstructor {
+	return func(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
+		tektonConfigInformer := tektonConfiginformer.Get(ctx)
+		deploymentInformer := deploymentinformer.Get(ctx)
+		kubeClient := kubeclient.Get(ctx)
+		logger := logging.FromContext(ctx)
+
+		mfclient, err := mfc.NewClient(injection.GetConfig(ctx))
+		if err != nil {
+			logger.Fatalw("Error creating client from injected config", zap.Error(err))
+		}
+		mflogger := zapr.NewLogger(logger.Named("manifestival").Desugar())
+		manifest, _ := mf.ManifestFrom(mf.Slice{}, mf.UseClient(mfclient), mf.UseLogger(mflogger))
+
+		c := &Reconciler{
+			kubeClientSet:     kubeClient,
+			operatorClientSet: operatorclient.Get(ctx),
+			extension:         generator(ctx),
+			manifest:          manifest,
+		}
+		impl := tektonConfigreconciler.NewImpl(ctx, c)
+
+		logger.Info("Setting up event handlers")
+
+		tektonConfigInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue))
+
+		deploymentInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{
+			FilterFunc: controller.FilterControllerGVK(v1alpha1.SchemeGroupVersion.WithKind("TektonConfig")),
+			Handler:    controller.HandleAll(impl.EnqueueControllerOf),
+		})
+
+		return impl
+	}
+}
diff --git a/pkg/reconciler/kubernetes/tektonconfig/extension.go b/pkg/reconciler/kubernetes/tektonconfig/extension.go
new file mode 100644
index 000000000..a648ac71d
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonconfig/extension.go
@@ -0,0 +1,59 @@
+/*
+Copyright 2020 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 tektonconfig
+
+import (
+	"context"
+
+	mf "github.com/manifestival/manifestival"
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	"github.com/tektoncd/operator/pkg/client/clientset/versioned"
+	operatorclient "github.com/tektoncd/operator/pkg/client/injection/client"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektonconfig/extension"
+)
+
+func KubernetesExtension(ctx context.Context) common.Extension {
+	return kubernetesExtension{
+		operatorClientSet: operatorclient.Get(ctx),
+	}
+}
+
+type kubernetesExtension struct {
+	operatorClientSet versioned.Interface
+}
+
+func (oe kubernetesExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer {
+	return []mf.Transformer{}
+}
+func (oe kubernetesExtension) PreReconcile(context.Context, v1alpha1.TektonComponent) error {
+	return nil
+}
+func (oe kubernetesExtension) PostReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error {
+	configInstance := comp.(*v1alpha1.TektonConfig)
+	if configInstance.Spec.Profile == common.ProfileAll {
+		return extension.CreateDashboardCR(comp, oe.operatorClientSet.OperatorV1alpha1())
+	}
+	return nil
+}
+func (oe kubernetesExtension) Finalize(ctx context.Context, comp v1alpha1.TektonComponent) error {
+	configInstance := comp.(*v1alpha1.TektonConfig)
+	if configInstance.Spec.Profile == common.ProfileAll {
+		return extension.TektonDashboardCRDelete(oe.operatorClientSet.OperatorV1alpha1().TektonDashboards(), common.DashboardResourceName)
+	}
+	return nil
+}
diff --git a/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go b/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go
new file mode 100644
index 000000000..ed2eb711d
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go
@@ -0,0 +1,130 @@
+/*
+Copyright 2020 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 extension
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	"log"
+
+	op "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	operatorv1alpha1 "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	apierrs "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"knative.dev/pkg/test/logging"
+)
+
+func CreateDashboardCR(instance v1alpha1.TektonComponent, client operatorv1alpha1.OperatorV1alpha1Interface) error {
+	configInstance := instance.(*v1alpha1.TektonConfig)
+	if _, err := ensureTektonDashboardExists(client.TektonDashboards(), configInstance.Spec.TargetNamespace); err != nil {
+		return errors.New(err.Error())
+	}
+	if _, err := waitForTektonDashboardState(client.TektonDashboards(), common.DashboardResourceName,
+		isTektonDashboardReady); err != nil {
+		log.Println("TektonDashboard is not in ready state: ", err)
+		return err
+	}
+	return nil
+}
+
+func ensureTektonDashboardExists(clients op.TektonDashboardInterface, targetNS string) (*v1alpha1.TektonDashboard, error) {
+	tdCR, err := GetDashboard(clients, common.DashboardResourceName)
+	if err == nil {
+		return tdCR, err
+	}
+	if apierrs.IsNotFound(err) {
+		tdCR = &v1alpha1.TektonDashboard{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: common.DashboardResourceName,
+			},
+			Spec: v1alpha1.TektonDashboardSpec{
+				CommonSpec: v1alpha1.CommonSpec{
+					TargetNamespace: targetNS,
+				},
+			},
+		}
+		return clients.Create(context.TODO(), tdCR, metav1.CreateOptions{})
+	}
+	return tdCR, err
+}
+
+func GetDashboard(clients op.TektonDashboardInterface, name string) (*v1alpha1.TektonDashboard, error) {
+	return clients.Get(context.TODO(), name, metav1.GetOptions{})
+}
+
+// waitForTektonDashboardState polls the status of the TektonDashboard called name
+// from client every `interval` until `inState` returns `true` indicating it
+// is done, returns an error or timeout.
+func waitForTektonDashboardState(clients op.TektonDashboardInterface, name string,
+	inState func(s *v1alpha1.TektonDashboard, err error) (bool, error)) (*v1alpha1.TektonDashboard, error) {
+	span := logging.GetEmitableSpan(context.Background(), fmt.Sprintf("WaitForTektonDashboardState/%s/%s", name, "TektonDashboardIsReady"))
+	defer span.End()
+
+	var lastState *v1alpha1.TektonDashboard
+	waitErr := wait.PollImmediate(common.Interval, common.Timeout, func() (bool, error) {
+		lastState, err := clients.Get(context.TODO(), name, metav1.GetOptions{})
+		return inState(lastState, err)
+	})
+	if waitErr != nil {
+		return lastState, fmt.Errorf("TektonDashboard %s is not in desired state, got: %+v: %w: For more info Please check TektonDashboard CR status", name, lastState, waitErr)
+	}
+	return lastState, nil
+}
+
+// isTektonDashboardReady will check the status conditions of the TektonDashboard and return true if the TektonDashboard is ready.
+func isTektonDashboardReady(s *v1alpha1.TektonDashboard, err error) (bool, error) {
+	return s.Status.IsReady(), err
+}
+
+// TektonDashboardCRDelete deletes tha TektonDashboard to see if all resources will be deleted
+func TektonDashboardCRDelete(clients op.TektonDashboardInterface, name string) error {
+	if _, err := GetDashboard(clients, common.DashboardResourceName); err != nil {
+		if apierrs.IsNotFound(err) {
+			return nil
+		}
+		return err
+	}
+	if err := clients.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
+		return fmt.Errorf("TektonDashboard %q failed to delete: %v", name, err)
+	}
+	err := wait.PollImmediate(common.Interval, common.Timeout, func() (bool, error) {
+		_, err := clients.Get(context.TODO(), name, metav1.GetOptions{})
+		if apierrs.IsNotFound(err) {
+			return true, nil
+		}
+		return false, err
+	})
+	if err != nil {
+		return fmt.Errorf("Timed out waiting on TektonDashboard to delete %v", err)
+	}
+	return verifyNoTektonDashboardCR(clients)
+}
+
+func verifyNoTektonDashboardCR(clients op.TektonDashboardInterface) error {
+	dashboards, err := clients.List(context.TODO(), metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+	if len(dashboards.Items) > 0 {
+		return errors.New("Unable to verify cluster-scoped resources are deleted if any TektonDashboard exists")
+	}
+	return nil
+}
diff --git a/pkg/reconciler/kubernetes/tektonconfig/pipeline/pipeline.go b/pkg/reconciler/kubernetes/tektonconfig/pipeline/pipeline.go
new file mode 100644
index 000000000..cff033efb
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonconfig/pipeline/pipeline.go
@@ -0,0 +1,145 @@
+/*
+Copyright 2020 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 pipeline
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	"log"
+
+	op "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	operatorv1alpha1 "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	apierrs "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"knative.dev/pkg/test/logging"
+)
+
+func CreatePipelineCR(instance v1alpha1.TektonComponent, client operatorv1alpha1.OperatorV1alpha1Interface) error {
+	configInstance := instance.(*v1alpha1.TektonConfig)
+	if _, err := ensureTektonPipelineExists(client.TektonPipelines(), configInstance.Spec.TargetNamespace); err != nil {
+		return errors.New(err.Error())
+	}
+	if _, err := waitForTektonPipelineState(client.TektonPipelines(), common.PipelineResourceName,
+		isTektonPipelineReady); err != nil {
+		log.Println("TektonPipeline is not in ready state: ", err)
+		return err
+	}
+	return nil
+}
+
+func ensureTektonPipelineExists(clients op.TektonPipelineInterface, targetNS string) (*v1alpha1.TektonPipeline, error) {
+	tpCR, err := GetPipeline(clients, common.PipelineResourceName)
+	if err == nil {
+		return tpCR, err
+	}
+	if apierrs.IsNotFound(err) {
+		tpCR = &v1alpha1.TektonPipeline{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: common.PipelineResourceName,
+			},
+			Spec: v1alpha1.TektonPipelineSpec{
+				CommonSpec: v1alpha1.CommonSpec{
+					TargetNamespace: targetNS,
+				},
+			},
+		}
+		return clients.Create(context.TODO(), tpCR, metav1.CreateOptions{})
+	}
+	return tpCR, err
+}
+
+func GetPipeline(clients op.TektonPipelineInterface, name string) (*v1alpha1.TektonPipeline, error) {
+	return clients.Get(context.TODO(), name, metav1.GetOptions{})
+}
+
+// WaitForTektonPipelineState polls the status of the TektonPipeline called name
+// from client every `interval` until `inState` returns `true` indicating it
+// is done, returns an error or timeout.
+func waitForTektonPipelineState(clients op.TektonPipelineInterface, name string,
+	inState func(s *v1alpha1.TektonPipeline, err error) (bool, error)) (*v1alpha1.TektonPipeline, error) {
+	span := logging.GetEmitableSpan(context.Background(), fmt.Sprintf("WaitForTektonPipelineState/%s/%s", name, "TektonPipelineIsReady"))
+	defer span.End()
+
+	var lastState *v1alpha1.TektonPipeline
+	waitErr := wait.PollImmediate(common.Interval, common.Timeout, func() (bool, error) {
+		lastState, err := clients.Get(context.TODO(), name, metav1.GetOptions{})
+		return inState(lastState, err)
+	})
+
+	if waitErr != nil {
+		return lastState, fmt.Errorf("tektonpipeline %s is not in desired state, got: %+v: %w: For more info Please check TektonPipeline CR status", name, lastState, waitErr)
+	}
+	return lastState, nil
+}
+
+// IsTektonPipelineReady will check the status conditions of the TektonPipeline and return true if the TektonPipeline is ready.
+func isTektonPipelineReady(s *v1alpha1.TektonPipeline, err error) (bool, error) {
+	return s.Status.IsReady(), err
+}
+
+// TektonPipelineCRDelete deletes tha TektonPipeline to see if all resources will be deleted
+func TektonPipelineCRDelete(clients op.TektonPipelineInterface, name string) error {
+	if _, err := GetPipeline(clients, common.PipelineResourceName); err != nil {
+		if apierrs.IsNotFound(err) {
+			return nil
+		}
+		return err
+	}
+	if err := clients.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
+		return fmt.Errorf("TektonPipeline %q failed to delete: %v", name, err)
+	}
+	err := wait.PollImmediate(common.Interval, common.Timeout, func() (bool, error) {
+		_, err := clients.Get(context.TODO(), name, metav1.GetOptions{})
+		if apierrs.IsNotFound(err) {
+			return true, nil
+		}
+		return false, err
+	})
+	if err != nil {
+		return fmt.Errorf("Timed out waiting on TektonPipeline to delete %v", err)
+	}
+	return verifyNoTektonPipelineCR(clients)
+}
+
+func verifyNoTektonPipelineCR(clients op.TektonPipelineInterface) error {
+	pipelines, err := clients.List(context.TODO(), metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+	if len(pipelines.Items) > 0 {
+		return errors.New("TektonPipeline still exists")
+	}
+	return nil
+}
+
+func GetTektonConfig() *v1alpha1.TektonConfig {
+	return &v1alpha1.TektonConfig{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: common.ConfigResourceName,
+		},
+		Spec: v1alpha1.TektonConfigSpec{
+			Profile: "all",
+			CommonSpec: v1alpha1.CommonSpec{
+				TargetNamespace: "tekton-pipelines",
+			},
+		},
+	}
+}
diff --git a/pkg/reconciler/kubernetes/tektonconfig/tektonconfig.go b/pkg/reconciler/kubernetes/tektonconfig/tektonconfig.go
new file mode 100644
index 000000000..6388f975d
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonconfig/tektonconfig.go
@@ -0,0 +1,143 @@
+/*
+Copyright 2020 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 tektonconfig
+
+import (
+	"context"
+	"fmt"
+
+	mf "github.com/manifestival/manifestival"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	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/kubernetes/tektonconfig/pipeline"
+	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektonconfig/trigger"
+	"knative.dev/pkg/logging"
+	pkgreconciler "knative.dev/pkg/reconciler"
+)
+
+// Reconciler implements controller.Reconciler for TektonConfig resources.
+type Reconciler struct {
+	// kubeClientSet allows us to talk to the k8s for core APIs
+	kubeClientSet kubernetes.Interface
+	// operatorClientSet allows us to configure operator objects
+	operatorClientSet clientset.Interface
+	// manifest is empty, but with a valid client and logger. all
+	// manifests are immutable, and any created during reconcile are
+	// expected to be appended to this one, obviating the passing of
+	// client & logger
+	manifest mf.Manifest
+	// Platform-specific behavior to affect the transform
+	extension common.Extension
+}
+
+// Check that our Reconciler implements controller.Reconciler
+var _ tektonConfigreconciler.Interface = (*Reconciler)(nil)
+var _ tektonConfigreconciler.Finalizer = (*Reconciler)(nil)
+
+// FinalizeKind removes all resources after deletion of a TektonConfig.
+func (r *Reconciler) FinalizeKind(ctx context.Context, original *v1alpha1.TektonConfig) pkgreconciler.Event {
+	logger := logging.FromContext(ctx)
+
+	// List all TektonConfigs to determine if cluster-scoped resources should be deleted.
+	tps, err := r.operatorClientSet.OperatorV1alpha1().TektonConfigs().List(ctx, metav1.ListOptions{})
+	if err != nil {
+		return fmt.Errorf("failed to list all TektonConfigs: %w", err)
+	}
+
+	for _, tp := range tps.Items {
+		if tp.GetDeletionTimestamp().IsZero() {
+			// Not deleting all TektonPipelines. Nothing to do here.
+			return nil
+		}
+	}
+
+	if original.Spec.Profile == common.ProfileBasic {
+		return pipeline.TektonPipelineCRDelete(r.operatorClientSet.OperatorV1alpha1().TektonPipelines(), common.PipelineResourceName)
+	} else {
+		// TektonPipeline and TektonTrigger is common for profile type default and all
+		if err := pipeline.TektonPipelineCRDelete(r.operatorClientSet.OperatorV1alpha1().TektonPipelines(), common.PipelineResourceName); err != nil {
+			return err
+		}
+		if err := trigger.TektonTriggerCRDelete(r.operatorClientSet.OperatorV1alpha1().TektonTriggers(), common.TriggerResourceName); err != nil {
+			return err
+		}
+	}
+
+	if err := r.extension.Finalize(ctx, original); err != nil {
+		logger.Error("Failed to finalize platform resources", err)
+	}
+
+	return nil
+}
+
+// ReconcileKind compares the actual state with the desired, and attempts to
+// converge the two.
+func (r *Reconciler) ReconcileKind(ctx context.Context, tc *v1alpha1.TektonConfig) pkgreconciler.Event {
+	logger := logging.FromContext(ctx)
+	tc.Status.InitializeConditions()
+	tc.Status.ObservedGeneration = tc.Generation
+
+	logger.Infow("Reconciling TektonConfig", "status", tc.Status)
+	if tc.GetName() != common.ConfigResourceName {
+		msg := fmt.Sprintf("Resource ignored, Expected Name: %s, Got Name: %s",
+			common.ConfigResourceName,
+			tc.GetName(),
+		)
+		logger.Error(msg)
+		tc.GetStatus().MarkInstallFailed(msg)
+		return nil
+	}
+
+	var stages common.Stages
+	if tc.Spec.Profile == common.ProfileBasic {
+		stages = common.Stages{
+			r.createPipelineCR,
+		}
+	} else {
+		// TektonPipeline and TektonTrigger is common for profile type default and all
+		stages = common.Stages{
+			r.createPipelineCR,
+			r.createTriggerCR,
+		}
+	}
+
+	manifest := r.manifest.Append()
+	if err := stages.Execute(ctx, &manifest, tc); err != nil {
+		tc.GetStatus().MarkInstallFailed(err.Error())
+		return err
+	}
+	if err := r.extension.PostReconcile(ctx, tc); err != nil {
+		tc.GetStatus().MarkInstallFailed(err.Error())
+		return err
+	}
+	tc.Status.MarkInstallSucceeded()
+	tc.Status.MarkDeploymentsAvailable()
+	return nil
+}
+
+func (r *Reconciler) createPipelineCR(ctx context.Context, manifest *mf.Manifest, comp v1alpha1.TektonComponent) error {
+	return pipeline.CreatePipelineCR(comp, r.operatorClientSet.OperatorV1alpha1())
+}
+
+func (r *Reconciler) createTriggerCR(ctx context.Context, manifest *mf.Manifest, comp v1alpha1.TektonComponent) error {
+	return trigger.CreateTriggerCR(comp, r.operatorClientSet.OperatorV1alpha1())
+}
diff --git a/pkg/reconciler/kubernetes/tektonconfig/trigger/trigger.go b/pkg/reconciler/kubernetes/tektonconfig/trigger/trigger.go
new file mode 100644
index 000000000..1f761c5ab
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonconfig/trigger/trigger.go
@@ -0,0 +1,132 @@
+/*
+Copyright 2020 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 trigger
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	"log"
+
+	apierrs "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"knative.dev/pkg/test/logging"
+
+	op "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	operatorv1alpha1 "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+)
+
+func CreateTriggerCR(instance v1alpha1.TektonComponent, client operatorv1alpha1.OperatorV1alpha1Interface) error {
+	configInstance := instance.(*v1alpha1.TektonConfig)
+	if _, err := ensureTektonTriggerExists(client.TektonTriggers(), configInstance.Spec.TargetNamespace); err != nil {
+		return errors.New(err.Error())
+	}
+	if _, err := waitForTektonTriggerState(client.TektonTriggers(), common.TriggerResourceName,
+		isTektonTriggerReady); err != nil {
+		log.Println("TektonTrigger is not in ready state: ", err)
+		return err
+	}
+	return nil
+}
+
+func ensureTektonTriggerExists(clients op.TektonTriggerInterface, targetNS string) (*v1alpha1.TektonTrigger, error) {
+	ttCR, err := GetTrigger(clients, common.TriggerResourceName)
+	if err == nil {
+		return ttCR, err
+	}
+	if apierrs.IsNotFound(err) {
+		ttCR = &v1alpha1.TektonTrigger{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: common.TriggerResourceName,
+			},
+			Spec: v1alpha1.TektonTriggerSpec{
+				CommonSpec: v1alpha1.CommonSpec{
+					TargetNamespace: targetNS,
+				},
+			},
+		}
+		return clients.Create(context.TODO(), ttCR, metav1.CreateOptions{})
+	}
+	return ttCR, err
+}
+
+func GetTrigger(clients op.TektonTriggerInterface, name string) (*v1alpha1.TektonTrigger, error) {
+	return clients.Get(context.TODO(), name, metav1.GetOptions{})
+}
+
+// waitForTektonTriggerState polls the status of the TektonTrigger called name
+// from client every `interval` until `inState` returns `true` indicating it
+// is done, returns an error or timeout.
+func waitForTektonTriggerState(clients op.TektonTriggerInterface, name string,
+	inState func(s *v1alpha1.TektonTrigger, err error) (bool, error)) (*v1alpha1.TektonTrigger, error) {
+	span := logging.GetEmitableSpan(context.Background(), fmt.Sprintf("WaitForTektonTriggerState/%s/%s", name, "TektonTriggerIsReady"))
+	defer span.End()
+
+	var lastState *v1alpha1.TektonTrigger
+	waitErr := wait.PollImmediate(common.Interval, common.Timeout, func() (bool, error) {
+		lastState, err := clients.Get(context.TODO(), name, metav1.GetOptions{})
+		return inState(lastState, err)
+	})
+
+	if waitErr != nil {
+		return lastState, fmt.Errorf("tektontrigger %s is not in desired state, got: %+v: %w: For more info Please check TektonTrigger CR status", name, lastState, waitErr)
+	}
+	return lastState, nil
+}
+
+// isTektonTriggerReady will check the status conditions of the TektonTrigger and return true if the TektonTrigger is ready.
+func isTektonTriggerReady(s *v1alpha1.TektonTrigger, err error) (bool, error) {
+	return s.Status.IsReady(), err
+}
+
+// TektonTriggerCRDelete deletes tha TektonTrigger to see if all resources will be deleted
+func TektonTriggerCRDelete(clients op.TektonTriggerInterface, name string) error {
+	if _, err := GetTrigger(clients, common.TriggerResourceName); err != nil {
+		if apierrs.IsNotFound(err) {
+			return nil
+		}
+		return err
+	}
+	if err := clients.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
+		return fmt.Errorf("TektonTrigger %q failed to delete: %v", name, err)
+	}
+	err := wait.PollImmediate(common.Interval, common.Timeout, func() (bool, error) {
+		_, err := clients.Get(context.TODO(), name, metav1.GetOptions{})
+		if apierrs.IsNotFound(err) {
+			return true, nil
+		}
+		return false, err
+	})
+	if err != nil {
+		return fmt.Errorf("Timed out waiting on TektonTrigger to delete %v", err)
+	}
+	return verifyNoTektonTriggerCR(clients)
+}
+
+func verifyNoTektonTriggerCR(clients op.TektonTriggerInterface) error {
+	triggers, err := clients.List(context.TODO(), metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+	if len(triggers.Items) > 0 {
+		return errors.New("Unable to verify cluster-scoped resources are deleted if any TektonTrigger exists")
+	}
+	return nil
+}
diff --git a/pkg/reconciler/kubernetes/tektondashboard/tektondashboard.go b/pkg/reconciler/kubernetes/tektondashboard/tektondashboard.go
index f609e30c1..e0e1d7e85 100644
--- a/pkg/reconciler/kubernetes/tektondashboard/tektondashboard.go
+++ b/pkg/reconciler/kubernetes/tektondashboard/tektondashboard.go
@@ -120,7 +120,7 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tt *v1alpha1.TektonDashb
 	}
 	tt.Status.MarkDependenciesInstalled()
 
-	if err := r.extension.Reconcile(ctx, tt); err != nil {
+	if err := r.extension.PreReconcile(ctx, tt); err != nil {
 		return err
 	}
 	stages := common.Stages{
diff --git a/pkg/reconciler/kubernetes/tektonpipeline/tektonpipeline.go b/pkg/reconciler/kubernetes/tektonpipeline/tektonpipeline.go
index 0255e30b7..c7686c5be 100644
--- a/pkg/reconciler/kubernetes/tektonpipeline/tektonpipeline.go
+++ b/pkg/reconciler/kubernetes/tektonpipeline/tektonpipeline.go
@@ -101,7 +101,7 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tp *v1alpha1.TektonPipel
 		return nil
 	}
 
-	if err := r.extension.Reconcile(ctx, tp); err != nil {
+	if err := r.extension.PreReconcile(ctx, tp); err != nil {
 		return err
 	}
 	stages := common.Stages{
diff --git a/pkg/reconciler/kubernetes/tektontrigger/tektontrigger.go b/pkg/reconciler/kubernetes/tektontrigger/tektontrigger.go
index 4fd8ea009..77a1c536c 100644
--- a/pkg/reconciler/kubernetes/tektontrigger/tektontrigger.go
+++ b/pkg/reconciler/kubernetes/tektontrigger/tektontrigger.go
@@ -118,7 +118,7 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tt *v1alpha1.TektonTrigg
 	}
 	tt.Status.MarkDependenciesInstalled()
 
-	if err := r.extension.Reconcile(ctx, tt); err != nil {
+	if err := r.extension.PreReconcile(ctx, tt); err != nil {
 		return err
 	}
 	stages := common.Stages{
diff --git a/pkg/reconciler/openshift/tektonaddon/extension.go b/pkg/reconciler/openshift/tektonaddon/extension.go
index 83c55506a..2a3fe6cd5 100644
--- a/pkg/reconciler/openshift/tektonaddon/extension.go
+++ b/pkg/reconciler/openshift/tektonaddon/extension.go
@@ -34,7 +34,10 @@ type openshiftExtension struct{}
 func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer {
 	return []mf.Transformer{}
 }
-func (oe openshiftExtension) Reconcile(context.Context, v1alpha1.TektonComponent) error {
+func (oe openshiftExtension) PreReconcile(context.Context, v1alpha1.TektonComponent) error {
+	return nil
+}
+func (oe openshiftExtension) PostReconcile(context.Context, v1alpha1.TektonComponent) error {
 	return nil
 }
 func (oe openshiftExtension) Finalize(context.Context, v1alpha1.TektonComponent) error {
diff --git a/pkg/reconciler/openshift/tektonaddon/tektonaddon.go b/pkg/reconciler/openshift/tektonaddon/tektonaddon.go
index a84c6bf59..47e100af3 100644
--- a/pkg/reconciler/openshift/tektonaddon/tektonaddon.go
+++ b/pkg/reconciler/openshift/tektonaddon/tektonaddon.go
@@ -133,7 +133,7 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tt *v1alpha1.TektonAddon
 	}
 	tt.Status.MarkDependenciesInstalled()
 
-	if err := r.extension.Reconcile(ctx, tt); err != nil {
+	if err := r.extension.PreReconcile(ctx, tt); err != nil {
 		return err
 	}
 	stages := common.Stages{
diff --git a/pkg/reconciler/openshift/tektonconfig/controller.go b/pkg/reconciler/openshift/tektonconfig/controller.go
new file mode 100644
index 000000000..8a6ce600f
--- /dev/null
+++ b/pkg/reconciler/openshift/tektonconfig/controller.go
@@ -0,0 +1,31 @@
+/*
+Copyright 2020 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 tektonconfig
+
+import (
+	"context"
+
+	k8s_ctrl "github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektonconfig"
+	"knative.dev/pkg/configmap"
+	"knative.dev/pkg/controller"
+)
+
+// NewController initializes the controller and is called by the generated code
+// Registers eventhandlers to enqueue events
+func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
+	return k8s_ctrl.NewExtendedController(OpenShiftExtension)(ctx, cmw)
+}
diff --git a/pkg/reconciler/openshift/tektonconfig/extension.go b/pkg/reconciler/openshift/tektonconfig/extension.go
new file mode 100644
index 000000000..882c373d2
--- /dev/null
+++ b/pkg/reconciler/openshift/tektonconfig/extension.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2020 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 tektonconfig
+
+import (
+	"context"
+
+	mf "github.com/manifestival/manifestival"
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	"github.com/tektoncd/operator/pkg/client/clientset/versioned"
+	operatorclient "github.com/tektoncd/operator/pkg/client/injection/client"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	"github.com/tektoncd/operator/pkg/reconciler/openshift/tektonconfig/extension"
+)
+
+// NoPlatform "generates" a NilExtension
+func OpenShiftExtension(ctx context.Context) common.Extension {
+	return openshiftExtension{
+		operatorClientSet: operatorclient.Get(ctx),
+	}
+}
+
+type openshiftExtension struct {
+	operatorClientSet versioned.Interface
+}
+
+func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer {
+	return []mf.Transformer{}
+}
+func (oe openshiftExtension) PreReconcile(context.Context, v1alpha1.TektonComponent) error {
+	return nil
+}
+func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error {
+	configInstance := comp.(*v1alpha1.TektonConfig)
+	if configInstance.Spec.Profile == common.ProfileAll {
+		return extension.CreateAddonCR(comp, oe.operatorClientSet.OperatorV1alpha1())
+	}
+	return nil
+}
+func (oe openshiftExtension) Finalize(ctx context.Context, comp v1alpha1.TektonComponent) error {
+	configInstance := comp.(*v1alpha1.TektonConfig)
+	if configInstance.Spec.Profile == common.ProfileAll {
+		return extension.TektonAddonCRDelete(oe.operatorClientSet.OperatorV1alpha1().TektonAddons(), common.AddonResourceName)
+	}
+	return nil
+}
diff --git a/pkg/reconciler/openshift/tektonconfig/extension/addon.go b/pkg/reconciler/openshift/tektonconfig/extension/addon.go
new file mode 100644
index 000000000..59b189f93
--- /dev/null
+++ b/pkg/reconciler/openshift/tektonconfig/extension/addon.go
@@ -0,0 +1,131 @@
+/*
+Copyright 2020 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 extension
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log"
+
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	op "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	operatorv1alpha1 "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	apierrs "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"knative.dev/pkg/test/logging"
+)
+
+func CreateAddonCR(instance v1alpha1.TektonComponent, client operatorv1alpha1.OperatorV1alpha1Interface) error {
+	configInstance := instance.(*v1alpha1.TektonConfig)
+	if _, err := ensureTektonAddonExists(client.TektonAddons(), configInstance.Spec.TargetNamespace); err != nil {
+		return errors.New(err.Error())
+	}
+	if _, err := waitForTektonAddonState(client.TektonAddons(), common.AddonResourceName,
+		isTektonAddonReady); err != nil {
+		log.Println("TektonAddon is not in ready state: ", err)
+		return err
+	}
+	return nil
+}
+
+func ensureTektonAddonExists(clients op.TektonAddonInterface, targetNS string) (*v1alpha1.TektonAddon, error) {
+	taCR, err := GetAddon(clients, common.AddonResourceName)
+	if err == nil {
+		return taCR, err
+	}
+	if apierrs.IsNotFound(err) {
+		taCR = &v1alpha1.TektonAddon{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: common.AddonResourceName,
+			},
+			Spec: v1alpha1.TektonAddonSpec{
+				CommonSpec: v1alpha1.CommonSpec{
+					TargetNamespace: targetNS,
+				},
+			},
+		}
+		return clients.Create(context.TODO(), taCR, metav1.CreateOptions{})
+	}
+	return taCR, err
+}
+
+func GetAddon(clients op.TektonAddonInterface, name string) (*v1alpha1.TektonAddon, error) {
+	return clients.Get(context.TODO(), name, metav1.GetOptions{})
+}
+
+// waitForTektonAddonState polls the status of the TektonAddon called name
+// from client every `interval` until `inState` returns `true` indicating it
+// is done, returns an error or timeout.
+func waitForTektonAddonState(clients op.TektonAddonInterface, name string,
+	inState func(s *v1alpha1.TektonAddon, err error) (bool, error)) (*v1alpha1.TektonAddon, error) {
+	span := logging.GetEmitableSpan(context.Background(), fmt.Sprintf("WaitForTektonAddonState/%s/%s", name, "TektonAddonIsReady"))
+	defer span.End()
+
+	var lastState *v1alpha1.TektonAddon
+	waitErr := wait.PollImmediate(common.Interval, common.Timeout, func() (bool, error) {
+		lastState, err := clients.Get(context.TODO(), name, metav1.GetOptions{})
+		return inState(lastState, err)
+	})
+
+	if waitErr != nil {
+		return lastState, fmt.Errorf("TektonAddon %s is not in desired state, got: %+v: %w: For more info Please check TektonAddon CR status", name, lastState, waitErr)
+	}
+	return lastState, nil
+}
+
+// isTektonAddonReady will check the status conditions of the TektonAddon and return true if the TektonAddon is ready.
+func isTektonAddonReady(s *v1alpha1.TektonAddon, err error) (bool, error) {
+	return s.Status.IsReady(), err
+}
+
+// TektonAddonCRDelete deletes tha TektonAddon to see if all resources will be deleted
+func TektonAddonCRDelete(clients op.TektonAddonInterface, name string) error {
+	if _, err := GetAddon(clients, common.AddonResourceName); err != nil {
+		if apierrs.IsNotFound(err) {
+			return nil
+		}
+		return err
+	}
+	if err := clients.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
+		return fmt.Errorf("TektonAddon %q failed to delete: %v", name, err)
+	}
+	err := wait.PollImmediate(common.Interval, common.Timeout, func() (bool, error) {
+		_, err := clients.Get(context.TODO(), name, metav1.GetOptions{})
+		if apierrs.IsNotFound(err) {
+			return true, nil
+		}
+		return false, err
+	})
+	if err != nil {
+		return fmt.Errorf("Timed out waiting on TektonAddon to delete %v", err)
+	}
+	return verifyNoTektonAddonCR(clients)
+}
+
+func verifyNoTektonAddonCR(clients op.TektonAddonInterface) error {
+	addons, err := clients.List(context.TODO(), metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+	if len(addons.Items) > 0 {
+		return errors.New("Unable to verify cluster-scoped resources are deleted if any TektonAddon exists")
+	}
+	return nil
+}
diff --git a/pkg/reconciler/openshift/tektonpipeline/extension.go b/pkg/reconciler/openshift/tektonpipeline/extension.go
index 2906be338..01d2d1976 100644
--- a/pkg/reconciler/openshift/tektonpipeline/extension.go
+++ b/pkg/reconciler/openshift/tektonpipeline/extension.go
@@ -34,7 +34,10 @@ type openshiftExtension struct{}
 func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer {
 	return []mf.Transformer{}
 }
-func (oe openshiftExtension) Reconcile(context.Context, v1alpha1.TektonComponent) error {
+func (oe openshiftExtension) PreReconcile(context.Context, v1alpha1.TektonComponent) error {
+	return nil
+}
+func (oe openshiftExtension) PostReconcile(context.Context, v1alpha1.TektonComponent) error {
 	return nil
 }
 func (oe openshiftExtension) Finalize(context.Context, v1alpha1.TektonComponent) error {
diff --git a/pkg/reconciler/openshift/tektontrigger/extension.go b/pkg/reconciler/openshift/tektontrigger/extension.go
index ad55bd80d..736bfa236 100644
--- a/pkg/reconciler/openshift/tektontrigger/extension.go
+++ b/pkg/reconciler/openshift/tektontrigger/extension.go
@@ -34,7 +34,10 @@ type openshiftExtension struct{}
 func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer {
 	return []mf.Transformer{}
 }
-func (oe openshiftExtension) Reconcile(context.Context, v1alpha1.TektonComponent) error {
+func (oe openshiftExtension) PreReconcile(context.Context, v1alpha1.TektonComponent) error {
+	return nil
+}
+func (oe openshiftExtension) PostReconcile(context.Context, v1alpha1.TektonComponent) error {
 	return nil
 }
 func (oe openshiftExtension) Finalize(context.Context, v1alpha1.TektonComponent) error {
diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go
new file mode 100644
index 000000000..8667908cf
--- /dev/null
+++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go
@@ -0,0 +1,156 @@
+// Copyright 2017, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+// Package cmpopts provides common options for the cmp package.
+package cmpopts
+
+import (
+	"math"
+	"reflect"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/xerrors"
+)
+
+func equateAlways(_, _ interface{}) bool { return true }
+
+// EquateEmpty returns a Comparer option that determines all maps and slices
+// with a length of zero to be equal, regardless of whether they are nil.
+//
+// EquateEmpty can be used in conjunction with SortSlices and SortMaps.
+func EquateEmpty() cmp.Option {
+	return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways))
+}
+
+func isEmpty(x, y interface{}) bool {
+	vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
+	return (x != nil && y != nil && vx.Type() == vy.Type()) &&
+		(vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) &&
+		(vx.Len() == 0 && vy.Len() == 0)
+}
+
+// EquateApprox returns a Comparer option that determines float32 or float64
+// values to be equal if they are within a relative fraction or absolute margin.
+// This option is not used when either x or y is NaN or infinite.
+//
+// The fraction determines that the difference of two values must be within the
+// smaller fraction of the two values, while the margin determines that the two
+// values must be within some absolute margin.
+// To express only a fraction or only a margin, use 0 for the other parameter.
+// The fraction and margin must be non-negative.
+//
+// The mathematical expression used is equivalent to:
+//	|x-y| ≤ max(fraction*min(|x|, |y|), margin)
+//
+// EquateApprox can be used in conjunction with EquateNaNs.
+func EquateApprox(fraction, margin float64) cmp.Option {
+	if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) {
+		panic("margin or fraction must be a non-negative number")
+	}
+	a := approximator{fraction, margin}
+	return cmp.Options{
+		cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)),
+		cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)),
+	}
+}
+
+type approximator struct{ frac, marg float64 }
+
+func areRealF64s(x, y float64) bool {
+	return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0)
+}
+func areRealF32s(x, y float32) bool {
+	return areRealF64s(float64(x), float64(y))
+}
+func (a approximator) compareF64(x, y float64) bool {
+	relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y))
+	return math.Abs(x-y) <= math.Max(a.marg, relMarg)
+}
+func (a approximator) compareF32(x, y float32) bool {
+	return a.compareF64(float64(x), float64(y))
+}
+
+// EquateNaNs returns a Comparer option that determines float32 and float64
+// NaN values to be equal.
+//
+// EquateNaNs can be used in conjunction with EquateApprox.
+func EquateNaNs() cmp.Option {
+	return cmp.Options{
+		cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)),
+		cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)),
+	}
+}
+
+func areNaNsF64s(x, y float64) bool {
+	return math.IsNaN(x) && math.IsNaN(y)
+}
+func areNaNsF32s(x, y float32) bool {
+	return areNaNsF64s(float64(x), float64(y))
+}
+
+// EquateApproxTime returns a Comparer option that determines two non-zero
+// time.Time values to be equal if they are within some margin of one another.
+// If both times have a monotonic clock reading, then the monotonic time
+// difference will be used. The margin must be non-negative.
+func EquateApproxTime(margin time.Duration) cmp.Option {
+	if margin < 0 {
+		panic("margin must be a non-negative number")
+	}
+	a := timeApproximator{margin}
+	return cmp.FilterValues(areNonZeroTimes, cmp.Comparer(a.compare))
+}
+
+func areNonZeroTimes(x, y time.Time) bool {
+	return !x.IsZero() && !y.IsZero()
+}
+
+type timeApproximator struct {
+	margin time.Duration
+}
+
+func (a timeApproximator) compare(x, y time.Time) bool {
+	// Avoid subtracting times to avoid overflow when the
+	// difference is larger than the largest representible duration.
+	if x.After(y) {
+		// Ensure x is always before y
+		x, y = y, x
+	}
+	// We're within the margin if x+margin >= y.
+	// Note: time.Time doesn't have AfterOrEqual method hence the negation.
+	return !x.Add(a.margin).Before(y)
+}
+
+// AnyError is an error that matches any non-nil error.
+var AnyError anyError
+
+type anyError struct{}
+
+func (anyError) Error() string     { return "any error" }
+func (anyError) Is(err error) bool { return err != nil }
+
+// EquateErrors returns a Comparer option that determines errors to be equal
+// if errors.Is reports them to match. The AnyError error can be used to
+// match any non-nil error.
+func EquateErrors() cmp.Option {
+	return cmp.FilterValues(areConcreteErrors, cmp.Comparer(compareErrors))
+}
+
+// areConcreteErrors reports whether x and y are types that implement error.
+// The input types are deliberately of the interface{} type rather than the
+// error type so that we can handle situations where the current type is an
+// interface{}, but the underlying concrete types both happen to implement
+// the error interface.
+func areConcreteErrors(x, y interface{}) bool {
+	_, ok1 := x.(error)
+	_, ok2 := y.(error)
+	return ok1 && ok2
+}
+
+func compareErrors(x, y interface{}) bool {
+	xe := x.(error)
+	ye := y.(error)
+	// TODO(≥go1.13): Use standard definition of errors.Is.
+	return xerrors.Is(xe, ye) || xerrors.Is(ye, xe)
+}
diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go
new file mode 100644
index 000000000..48787dd1a
--- /dev/null
+++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go
@@ -0,0 +1,206 @@
+// Copyright 2017, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package cmpopts
+
+import (
+	"fmt"
+	"reflect"
+	"unicode"
+	"unicode/utf8"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/internal/function"
+)
+
+// IgnoreFields returns an Option that ignores fields of the
+// given names on a single struct type. It respects the names of exported fields
+// that are forwarded due to struct embedding.
+// The struct type is specified by passing in a value of that type.
+//
+// The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a
+// specific sub-field that is embedded or nested within the parent struct.
+func IgnoreFields(typ interface{}, names ...string) cmp.Option {
+	sf := newStructFilter(typ, names...)
+	return cmp.FilterPath(sf.filter, cmp.Ignore())
+}
+
+// IgnoreTypes returns an Option that ignores all values assignable to
+// certain types, which are specified by passing in a value of each type.
+func IgnoreTypes(typs ...interface{}) cmp.Option {
+	tf := newTypeFilter(typs...)
+	return cmp.FilterPath(tf.filter, cmp.Ignore())
+}
+
+type typeFilter []reflect.Type
+
+func newTypeFilter(typs ...interface{}) (tf typeFilter) {
+	for _, typ := range typs {
+		t := reflect.TypeOf(typ)
+		if t == nil {
+			// This occurs if someone tries to pass in sync.Locker(nil)
+			panic("cannot determine type; consider using IgnoreInterfaces")
+		}
+		tf = append(tf, t)
+	}
+	return tf
+}
+func (tf typeFilter) filter(p cmp.Path) bool {
+	if len(p) < 1 {
+		return false
+	}
+	t := p.Last().Type()
+	for _, ti := range tf {
+		if t.AssignableTo(ti) {
+			return true
+		}
+	}
+	return false
+}
+
+// IgnoreInterfaces returns an Option that ignores all values or references of
+// values assignable to certain interface types. These interfaces are specified
+// by passing in an anonymous struct with the interface types embedded in it.
+// For example, to ignore sync.Locker, pass in struct{sync.Locker}{}.
+func IgnoreInterfaces(ifaces interface{}) cmp.Option {
+	tf := newIfaceFilter(ifaces)
+	return cmp.FilterPath(tf.filter, cmp.Ignore())
+}
+
+type ifaceFilter []reflect.Type
+
+func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) {
+	t := reflect.TypeOf(ifaces)
+	if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct {
+		panic("input must be an anonymous struct")
+	}
+	for i := 0; i < t.NumField(); i++ {
+		fi := t.Field(i)
+		switch {
+		case !fi.Anonymous:
+			panic("struct cannot have named fields")
+		case fi.Type.Kind() != reflect.Interface:
+			panic("embedded field must be an interface type")
+		case fi.Type.NumMethod() == 0:
+			// This matches everything; why would you ever want this?
+			panic("cannot ignore empty interface")
+		default:
+			tf = append(tf, fi.Type)
+		}
+	}
+	return tf
+}
+func (tf ifaceFilter) filter(p cmp.Path) bool {
+	if len(p) < 1 {
+		return false
+	}
+	t := p.Last().Type()
+	for _, ti := range tf {
+		if t.AssignableTo(ti) {
+			return true
+		}
+		if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) {
+			return true
+		}
+	}
+	return false
+}
+
+// IgnoreUnexported returns an Option that only ignores the immediate unexported
+// fields of a struct, including anonymous fields of unexported types.
+// In particular, unexported fields within the struct's exported fields
+// of struct types, including anonymous fields, will not be ignored unless the
+// type of the field itself is also passed to IgnoreUnexported.
+//
+// Avoid ignoring unexported fields of a type which you do not control (i.e. a
+// type from another repository), as changes to the implementation of such types
+// may change how the comparison behaves. Prefer a custom Comparer instead.
+func IgnoreUnexported(typs ...interface{}) cmp.Option {
+	ux := newUnexportedFilter(typs...)
+	return cmp.FilterPath(ux.filter, cmp.Ignore())
+}
+
+type unexportedFilter struct{ m map[reflect.Type]bool }
+
+func newUnexportedFilter(typs ...interface{}) unexportedFilter {
+	ux := unexportedFilter{m: make(map[reflect.Type]bool)}
+	for _, typ := range typs {
+		t := reflect.TypeOf(typ)
+		if t == nil || t.Kind() != reflect.Struct {
+			panic(fmt.Sprintf("%T must be a non-pointer struct", typ))
+		}
+		ux.m[t] = true
+	}
+	return ux
+}
+func (xf unexportedFilter) filter(p cmp.Path) bool {
+	sf, ok := p.Index(-1).(cmp.StructField)
+	if !ok {
+		return false
+	}
+	return xf.m[p.Index(-2).Type()] && !isExported(sf.Name())
+}
+
+// isExported reports whether the identifier is exported.
+func isExported(id string) bool {
+	r, _ := utf8.DecodeRuneInString(id)
+	return unicode.IsUpper(r)
+}
+
+// IgnoreSliceElements returns an Option that ignores elements of []V.
+// The discard function must be of the form "func(T) bool" which is used to
+// ignore slice elements of type V, where V is assignable to T.
+// Elements are ignored if the function reports true.
+func IgnoreSliceElements(discardFunc interface{}) cmp.Option {
+	vf := reflect.ValueOf(discardFunc)
+	if !function.IsType(vf.Type(), function.ValuePredicate) || vf.IsNil() {
+		panic(fmt.Sprintf("invalid discard function: %T", discardFunc))
+	}
+	return cmp.FilterPath(func(p cmp.Path) bool {
+		si, ok := p.Index(-1).(cmp.SliceIndex)
+		if !ok {
+			return false
+		}
+		if !si.Type().AssignableTo(vf.Type().In(0)) {
+			return false
+		}
+		vx, vy := si.Values()
+		if vx.IsValid() && vf.Call([]reflect.Value{vx})[0].Bool() {
+			return true
+		}
+		if vy.IsValid() && vf.Call([]reflect.Value{vy})[0].Bool() {
+			return true
+		}
+		return false
+	}, cmp.Ignore())
+}
+
+// IgnoreMapEntries returns an Option that ignores entries of map[K]V.
+// The discard function must be of the form "func(T, R) bool" which is used to
+// ignore map entries of type K and V, where K and V are assignable to T and R.
+// Entries are ignored if the function reports true.
+func IgnoreMapEntries(discardFunc interface{}) cmp.Option {
+	vf := reflect.ValueOf(discardFunc)
+	if !function.IsType(vf.Type(), function.KeyValuePredicate) || vf.IsNil() {
+		panic(fmt.Sprintf("invalid discard function: %T", discardFunc))
+	}
+	return cmp.FilterPath(func(p cmp.Path) bool {
+		mi, ok := p.Index(-1).(cmp.MapIndex)
+		if !ok {
+			return false
+		}
+		if !mi.Key().Type().AssignableTo(vf.Type().In(0)) || !mi.Type().AssignableTo(vf.Type().In(1)) {
+			return false
+		}
+		k := mi.Key()
+		vx, vy := mi.Values()
+		if vx.IsValid() && vf.Call([]reflect.Value{k, vx})[0].Bool() {
+			return true
+		}
+		if vy.IsValid() && vf.Call([]reflect.Value{k, vy})[0].Bool() {
+			return true
+		}
+		return false
+	}, cmp.Ignore())
+}
diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go
new file mode 100644
index 000000000..3a4804621
--- /dev/null
+++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go
@@ -0,0 +1,147 @@
+// Copyright 2017, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package cmpopts
+
+import (
+	"fmt"
+	"reflect"
+	"sort"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/internal/function"
+)
+
+// SortSlices returns a Transformer option that sorts all []V.
+// The less function must be of the form "func(T, T) bool" which is used to
+// sort any slice with element type V that is assignable to T.
+//
+// The less function must be:
+//	• Deterministic: less(x, y) == less(x, y)
+//	• Irreflexive: !less(x, x)
+//	• Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
+//
+// The less function does not have to be "total". That is, if !less(x, y) and
+// !less(y, x) for two elements x and y, their relative order is maintained.
+//
+// SortSlices can be used in conjunction with EquateEmpty.
+func SortSlices(lessFunc interface{}) cmp.Option {
+	vf := reflect.ValueOf(lessFunc)
+	if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
+		panic(fmt.Sprintf("invalid less function: %T", lessFunc))
+	}
+	ss := sliceSorter{vf.Type().In(0), vf}
+	return cmp.FilterValues(ss.filter, cmp.Transformer("cmpopts.SortSlices", ss.sort))
+}
+
+type sliceSorter struct {
+	in  reflect.Type  // T
+	fnc reflect.Value // func(T, T) bool
+}
+
+func (ss sliceSorter) filter(x, y interface{}) bool {
+	vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
+	if !(x != nil && y != nil && vx.Type() == vy.Type()) ||
+		!(vx.Kind() == reflect.Slice && vx.Type().Elem().AssignableTo(ss.in)) ||
+		(vx.Len() <= 1 && vy.Len() <= 1) {
+		return false
+	}
+	// Check whether the slices are already sorted to avoid an infinite
+	// recursion cycle applying the same transform to itself.
+	ok1 := sort.SliceIsSorted(x, func(i, j int) bool { return ss.less(vx, i, j) })
+	ok2 := sort.SliceIsSorted(y, func(i, j int) bool { return ss.less(vy, i, j) })
+	return !ok1 || !ok2
+}
+func (ss sliceSorter) sort(x interface{}) interface{} {
+	src := reflect.ValueOf(x)
+	dst := reflect.MakeSlice(src.Type(), src.Len(), src.Len())
+	for i := 0; i < src.Len(); i++ {
+		dst.Index(i).Set(src.Index(i))
+	}
+	sort.SliceStable(dst.Interface(), func(i, j int) bool { return ss.less(dst, i, j) })
+	ss.checkSort(dst)
+	return dst.Interface()
+}
+func (ss sliceSorter) checkSort(v reflect.Value) {
+	start := -1 // Start of a sequence of equal elements.
+	for i := 1; i < v.Len(); i++ {
+		if ss.less(v, i-1, i) {
+			// Check that first and last elements in v[start:i] are equal.
+			if start >= 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) {
+				panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i)))
+			}
+			start = -1
+		} else if start == -1 {
+			start = i
+		}
+	}
+}
+func (ss sliceSorter) less(v reflect.Value, i, j int) bool {
+	vx, vy := v.Index(i), v.Index(j)
+	return ss.fnc.Call([]reflect.Value{vx, vy})[0].Bool()
+}
+
+// SortMaps returns a Transformer option that flattens map[K]V types to be a
+// sorted []struct{K, V}. The less function must be of the form
+// "func(T, T) bool" which is used to sort any map with key K that is
+// assignable to T.
+//
+// Flattening the map into a slice has the property that cmp.Equal is able to
+// use Comparers on K or the K.Equal method if it exists.
+//
+// The less function must be:
+//	• Deterministic: less(x, y) == less(x, y)
+//	• Irreflexive: !less(x, x)
+//	• Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
+//	• Total: if x != y, then either less(x, y) or less(y, x)
+//
+// SortMaps can be used in conjunction with EquateEmpty.
+func SortMaps(lessFunc interface{}) cmp.Option {
+	vf := reflect.ValueOf(lessFunc)
+	if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
+		panic(fmt.Sprintf("invalid less function: %T", lessFunc))
+	}
+	ms := mapSorter{vf.Type().In(0), vf}
+	return cmp.FilterValues(ms.filter, cmp.Transformer("cmpopts.SortMaps", ms.sort))
+}
+
+type mapSorter struct {
+	in  reflect.Type  // T
+	fnc reflect.Value // func(T, T) bool
+}
+
+func (ms mapSorter) filter(x, y interface{}) bool {
+	vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
+	return (x != nil && y != nil && vx.Type() == vy.Type()) &&
+		(vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) &&
+		(vx.Len() != 0 || vy.Len() != 0)
+}
+func (ms mapSorter) sort(x interface{}) interface{} {
+	src := reflect.ValueOf(x)
+	outType := reflect.StructOf([]reflect.StructField{
+		{Name: "K", Type: src.Type().Key()},
+		{Name: "V", Type: src.Type().Elem()},
+	})
+	dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len())
+	for i, k := range src.MapKeys() {
+		v := reflect.New(outType).Elem()
+		v.Field(0).Set(k)
+		v.Field(1).Set(src.MapIndex(k))
+		dst.Index(i).Set(v)
+	}
+	sort.Slice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) })
+	ms.checkSort(dst)
+	return dst.Interface()
+}
+func (ms mapSorter) checkSort(v reflect.Value) {
+	for i := 1; i < v.Len(); i++ {
+		if !ms.less(v, i-1, i) {
+			panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i)))
+		}
+	}
+}
+func (ms mapSorter) less(v reflect.Value, i, j int) bool {
+	vx, vy := v.Index(i).Field(0), v.Index(j).Field(0)
+	return ms.fnc.Call([]reflect.Value{vx, vy})[0].Bool()
+}
diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go
new file mode 100644
index 000000000..fe8d1b9cc
--- /dev/null
+++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go
@@ -0,0 +1,187 @@
+// Copyright 2017, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package cmpopts
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+// filterField returns a new Option where opt is only evaluated on paths that
+// include a specific exported field on a single struct type.
+// The struct type is specified by passing in a value of that type.
+//
+// The name may be a dot-delimited string (e.g., "Foo.Bar") to select a
+// specific sub-field that is embedded or nested within the parent struct.
+func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option {
+	// TODO: This is currently unexported over concerns of how helper filters
+	// can be composed together easily.
+	// TODO: Add tests for FilterField.
+
+	sf := newStructFilter(typ, name)
+	return cmp.FilterPath(sf.filter, opt)
+}
+
+type structFilter struct {
+	t  reflect.Type // The root struct type to match on
+	ft fieldTree    // Tree of fields to match on
+}
+
+func newStructFilter(typ interface{}, names ...string) structFilter {
+	// TODO: Perhaps allow * as a special identifier to allow ignoring any
+	// number of path steps until the next field match?
+	// This could be useful when a concrete struct gets transformed into
+	// an anonymous struct where it is not possible to specify that by type,
+	// but the transformer happens to provide guarantees about the names of
+	// the transformed fields.
+
+	t := reflect.TypeOf(typ)
+	if t == nil || t.Kind() != reflect.Struct {
+		panic(fmt.Sprintf("%T must be a non-pointer struct", typ))
+	}
+	var ft fieldTree
+	for _, name := range names {
+		cname, err := canonicalName(t, name)
+		if err != nil {
+			panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err))
+		}
+		ft.insert(cname)
+	}
+	return structFilter{t, ft}
+}
+
+func (sf structFilter) filter(p cmp.Path) bool {
+	for i, ps := range p {
+		if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) {
+			return true
+		}
+	}
+	return false
+}
+
+// fieldTree represents a set of dot-separated identifiers.
+//
+// For example, inserting the following selectors:
+//	Foo
+//	Foo.Bar.Baz
+//	Foo.Buzz
+//	Nuka.Cola.Quantum
+//
+// Results in a tree of the form:
+//	{sub: {
+//		"Foo": {ok: true, sub: {
+//			"Bar": {sub: {
+//				"Baz": {ok: true},
+//			}},
+//			"Buzz": {ok: true},
+//		}},
+//		"Nuka": {sub: {
+//			"Cola": {sub: {
+//				"Quantum": {ok: true},
+//			}},
+//		}},
+//	}}
+type fieldTree struct {
+	ok  bool                 // Whether this is a specified node
+	sub map[string]fieldTree // The sub-tree of fields under this node
+}
+
+// insert inserts a sequence of field accesses into the tree.
+func (ft *fieldTree) insert(cname []string) {
+	if ft.sub == nil {
+		ft.sub = make(map[string]fieldTree)
+	}
+	if len(cname) == 0 {
+		ft.ok = true
+		return
+	}
+	sub := ft.sub[cname[0]]
+	sub.insert(cname[1:])
+	ft.sub[cname[0]] = sub
+}
+
+// matchPrefix reports whether any selector in the fieldTree matches
+// the start of path p.
+func (ft fieldTree) matchPrefix(p cmp.Path) bool {
+	for _, ps := range p {
+		switch ps := ps.(type) {
+		case cmp.StructField:
+			ft = ft.sub[ps.Name()]
+			if ft.ok {
+				return true
+			}
+			if len(ft.sub) == 0 {
+				return false
+			}
+		case cmp.Indirect:
+		default:
+			return false
+		}
+	}
+	return false
+}
+
+// canonicalName returns a list of identifiers where any struct field access
+// through an embedded field is expanded to include the names of the embedded
+// types themselves.
+//
+// For example, suppose field "Foo" is not directly in the parent struct,
+// but actually from an embedded struct of type "Bar". Then, the canonical name
+// of "Foo" is actually "Bar.Foo".
+//
+// Suppose field "Foo" is not directly in the parent struct, but actually
+// a field in two different embedded structs of types "Bar" and "Baz".
+// Then the selector "Foo" causes a panic since it is ambiguous which one it
+// refers to. The user must specify either "Bar.Foo" or "Baz.Foo".
+func canonicalName(t reflect.Type, sel string) ([]string, error) {
+	var name string
+	sel = strings.TrimPrefix(sel, ".")
+	if sel == "" {
+		return nil, fmt.Errorf("name must not be empty")
+	}
+	if i := strings.IndexByte(sel, '.'); i < 0 {
+		name, sel = sel, ""
+	} else {
+		name, sel = sel[:i], sel[i:]
+	}
+
+	// Type must be a struct or pointer to struct.
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+	}
+	if t.Kind() != reflect.Struct {
+		return nil, fmt.Errorf("%v must be a struct", t)
+	}
+
+	// Find the canonical name for this current field name.
+	// If the field exists in an embedded struct, then it will be expanded.
+	sf, _ := t.FieldByName(name)
+	if !isExported(name) {
+		// Avoid using reflect.Type.FieldByName for unexported fields due to
+		// buggy behavior with regard to embeddeding and unexported fields.
+		// See https://golang.org/issue/4876 for details.
+		sf = reflect.StructField{}
+		for i := 0; i < t.NumField() && sf.Name == ""; i++ {
+			if t.Field(i).Name == name {
+				sf = t.Field(i)
+			}
+		}
+	}
+	if sf.Name == "" {
+		return []string{name}, fmt.Errorf("does not exist")
+	}
+	var ss []string
+	for i := range sf.Index {
+		ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name)
+	}
+	if sel == "" {
+		return ss, nil
+	}
+	ssPost, err := canonicalName(sf.Type, sel)
+	return append(ss, ssPost...), err
+}
diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go
new file mode 100644
index 000000000..9d651553d
--- /dev/null
+++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/xform.go
@@ -0,0 +1,35 @@
+// Copyright 2018, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package cmpopts
+
+import (
+	"github.com/google/go-cmp/cmp"
+)
+
+type xformFilter struct{ xform cmp.Option }
+
+func (xf xformFilter) filter(p cmp.Path) bool {
+	for _, ps := range p {
+		if t, ok := ps.(cmp.Transform); ok && t.Option() == xf.xform {
+			return false
+		}
+	}
+	return true
+}
+
+// AcyclicTransformer returns a Transformer with a filter applied that ensures
+// that the transformer cannot be recursively applied upon its own output.
+//
+// An example use case is a transformer that splits a string by lines:
+//	AcyclicTransformer("SplitLines", func(s string) []string{
+//		return strings.Split(s, "\n")
+//	})
+//
+// Had this been an unfiltered Transformer instead, this would result in an
+// infinite cycle converting a string to []string to [][]string and so on.
+func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option {
+	xf := xformFilter{cmp.Transformer(name, xformFunc)}
+	return cmp.FilterPath(xf.filter, xf.xform)
+}
diff --git a/vendor/knative.dev/pkg/tracker/doc.go b/vendor/knative.dev/pkg/tracker/doc.go
new file mode 100644
index 000000000..5e0dfba60
--- /dev/null
+++ b/vendor/knative.dev/pkg/tracker/doc.go
@@ -0,0 +1,23 @@
+/*
+Copyright 2018 The Knative 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.
+*/
+
+// +k8s:deepcopy-gen=package
+
+// Package tracker defines a utility to enable Reconcilers to trigger
+// reconciliations when objects that are cross-referenced change, so
+// that the level-based reconciliation can react to the change.  The
+// prototypical cross-reference in Kubernetes is corev1.ObjectReference.
+package tracker
diff --git a/vendor/knative.dev/pkg/tracker/enqueue.go b/vendor/knative.dev/pkg/tracker/enqueue.go
new file mode 100644
index 000000000..2e001470c
--- /dev/null
+++ b/vendor/knative.dev/pkg/tracker/enqueue.go
@@ -0,0 +1,306 @@
+/*
+Copyright 2018 The Knative 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 tracker
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+	"sync"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/validation"
+
+	"knative.dev/pkg/kmeta"
+)
+
+// New returns an implementation of Interface that lets a Reconciler
+// register a particular resource as watching an ObjectReference for
+// a particular lease duration.  This watch must be refreshed
+// periodically (e.g. by a controller resync) or it will expire.
+//
+// When OnChanged is called by the informer for a particular
+// GroupVersionKind, the provided callback is called with the "key"
+// of each object actively watching the changed object.
+func New(callback func(types.NamespacedName), lease time.Duration) Interface {
+	return &impl{
+		leaseDuration: lease,
+		cb:            callback,
+	}
+}
+
+type impl struct {
+	m sync.Mutex
+	// exact maps from an object reference to the set of
+	// keys for objects watching it.
+	exact map[Reference]set
+	// inexact maps from a partial object reference (no name/selector) to
+	// a map from watcher keys to the compiled selector and expiry.
+	inexact map[Reference]matchers
+
+	// The amount of time that an object may watch another
+	// before having to renew the lease.
+	leaseDuration time.Duration
+
+	cb func(types.NamespacedName)
+}
+
+// Check that impl implements Interface.
+var _ Interface = (*impl)(nil)
+
+// set is a map from keys to expirations
+type set map[types.NamespacedName]time.Time
+
+// matchers maps the tracker's key to the matcher.
+type matchers map[types.NamespacedName]matcher
+
+// matcher holds the selector and expiry for matching tracked objects.
+type matcher struct {
+	// The selector to complete the match.
+	selector labels.Selector
+
+	// When this lease expires.
+	expiry time.Time
+}
+
+// Track implements Interface.
+func (i *impl) Track(ref corev1.ObjectReference, obj interface{}) error {
+	return i.TrackReference(Reference{
+		APIVersion: ref.APIVersion,
+		Kind:       ref.Kind,
+		Namespace:  ref.Namespace,
+		Name:       ref.Name,
+	}, obj)
+}
+
+func (i *impl) TrackReference(ref Reference, obj interface{}) error {
+	invalidFields := map[string][]string{
+		"APIVersion": validation.IsQualifiedName(ref.APIVersion),
+		"Kind":       validation.IsCIdentifier(ref.Kind),
+		"Namespace":  validation.IsDNS1123Label(ref.Namespace),
+	}
+	var selector labels.Selector
+	fieldErrors := []string{}
+	switch {
+	case ref.Selector != nil && ref.Name != "":
+		fieldErrors = append(fieldErrors, "cannot provide both Name and Selector")
+	case ref.Name != "":
+		invalidFields["Name"] = validation.IsDNS1123Subdomain(ref.Name)
+	case ref.Selector != nil:
+		ls, err := metav1.LabelSelectorAsSelector(ref.Selector)
+		if err != nil {
+			invalidFields["Selector"] = []string{err.Error()}
+		}
+		selector = ls
+	default:
+		fieldErrors = append(fieldErrors, "must provide either Name or Selector")
+	}
+	for k, v := range invalidFields {
+		for _, msg := range v {
+			fieldErrors = append(fieldErrors, fmt.Sprintf("%s: %s", k, msg))
+		}
+	}
+	if len(fieldErrors) > 0 {
+		sort.Strings(fieldErrors)
+		return fmt.Errorf("invalid Reference:\n%s", strings.Join(fieldErrors, "\n"))
+	}
+
+	// Determine the key of the object tracking this reference.
+	object, err := kmeta.DeletionHandlingAccessor(obj)
+	if err != nil {
+		return err
+	}
+	key := types.NamespacedName{Namespace: object.GetNamespace(), Name: object.GetName()}
+
+	i.m.Lock()
+	// Call the callback without the lock held.
+	var keys []types.NamespacedName
+	defer func(cb func(types.NamespacedName)) {
+		for _, key := range keys {
+			cb(key)
+		}
+	}(i.cb) // read i.cb with the lock held
+	defer i.m.Unlock()
+	if i.exact == nil {
+		i.exact = make(map[Reference]set)
+	}
+	if i.inexact == nil {
+		i.inexact = make(map[Reference]matchers)
+	}
+
+	// If the reference uses Name then it is an exact match.
+	if selector == nil {
+		l, ok := i.exact[ref]
+		if !ok {
+			l = set{}
+		}
+
+		if expiry, ok := l[key]; !ok || isExpired(expiry) {
+			// When covering an uncovered key, immediately call the
+			// registered callback to ensure that the following pattern
+			// doesn't create problems:
+			//    foo, err := lister.Get(key)
+			//    // Later...
+			//    err := tracker.TrackReference(fooRef, parent)
+			// In this example, "Later" represents a window where "foo" may
+			// have changed or been created while the Track is not active.
+			// The simplest way of eliminating such a window is to call the
+			// callback to "catch up" immediately following new
+			// registrations.
+			keys = append(keys, key)
+		}
+		// Overwrite the key with a new expiration.
+		l[key] = time.Now().Add(i.leaseDuration)
+
+		i.exact[ref] = l
+		return nil
+	}
+
+	// Otherwise, it is an inexact match by selector.
+	partialRef := Reference{
+		APIVersion: ref.APIVersion,
+		Kind:       ref.Kind,
+		Namespace:  ref.Namespace,
+		// Exclude the selector.
+	}
+	l, ok := i.inexact[partialRef]
+	if !ok {
+		l = matchers{}
+	}
+
+	if m, ok := l[key]; !ok || isExpired(m.expiry) {
+		// When covering an uncovered key, immediately call the
+		// registered callback to ensure that the following pattern
+		// doesn't create problems:
+		//    foo, err := lister.Get(key)
+		//    // Later...
+		//    err := tracker.TrackReference(fooRef, parent)
+		// In this example, "Later" represents a window where "foo" may
+		// have changed or been created while the Track is not active.
+		// The simplest way of eliminating such a window is to call the
+		// callback to "catch up" immediately following new
+		// registrations.
+		keys = append(keys, key)
+	}
+	// Overwrite the key with a new expiration.
+	l[key] = matcher{
+		selector: selector,
+		expiry:   time.Now().Add(i.leaseDuration),
+	}
+
+	i.inexact[partialRef] = l
+	return nil
+}
+
+func isExpired(expiry time.Time) bool {
+	return time.Now().After(expiry)
+}
+
+// OnChanged implements Interface.
+func (i *impl) OnChanged(obj interface{}) {
+	item, err := kmeta.DeletionHandlingAccessor(obj)
+	if err != nil {
+		return
+	}
+
+	or := kmeta.ObjectReference(item)
+	ref := Reference{
+		APIVersion: or.APIVersion,
+		Kind:       or.Kind,
+		Namespace:  or.Namespace,
+		Name:       or.Name,
+	}
+
+	i.m.Lock()
+	// Call the callbacks without the lock held.
+	var keys []types.NamespacedName
+	defer func(cb func(types.NamespacedName)) {
+		for _, key := range keys {
+			cb(key)
+		}
+	}(i.cb) // read i.cb with the lock held
+	defer i.m.Unlock()
+
+	// Handle exact matches.
+	s, ok := i.exact[ref]
+	if ok {
+		for key, expiry := range s {
+			// If the expiration has lapsed, then delete the key.
+			if isExpired(expiry) {
+				delete(s, key)
+				continue
+			}
+			keys = append(keys, key)
+		}
+		if len(s) == 0 {
+			delete(i.exact, ref)
+		}
+	}
+
+	// Handle inexact matches.
+	ref.Name = ""
+	ms, ok := i.inexact[ref]
+	if ok {
+		ls := labels.Set(item.GetLabels())
+		for key, m := range ms {
+			// If the expiration has lapsed, then delete the key.
+			if isExpired(m.expiry) {
+				delete(ms, key)
+				continue
+			}
+			if m.selector.Matches(ls) {
+				keys = append(keys, key)
+			}
+		}
+		if len(s) == 0 {
+			delete(i.exact, ref)
+		}
+	}
+}
+
+// OnChanged implements Interface.
+func (i *impl) OnDeletedObserver(obj interface{}) {
+	item, err := kmeta.DeletionHandlingAccessor(obj)
+	if err != nil {
+		return
+	}
+
+	key := types.NamespacedName{Namespace: item.GetNamespace(), Name: item.GetName()}
+
+	i.m.Lock()
+	defer i.m.Unlock()
+
+	// Remove exact matches.
+	for ref, matchers := range i.exact {
+		delete(matchers, key)
+		if len(matchers) == 0 {
+			delete(i.exact, ref)
+		}
+	}
+
+	// Remove inexact matches.
+	for ref, matchers := range i.inexact {
+		delete(matchers, key)
+		if len(matchers) == 0 {
+			delete(i.exact, ref)
+		}
+	}
+}
diff --git a/vendor/knative.dev/pkg/tracker/interface.go b/vendor/knative.dev/pkg/tracker/interface.go
new file mode 100644
index 000000000..c8ab479fa
--- /dev/null
+++ b/vendor/knative.dev/pkg/tracker/interface.go
@@ -0,0 +1,175 @@
+/*
+Copyright 2018 The Knative 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 tracker
+
+import (
+	"context"
+	"strings"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/util/validation"
+
+	"knative.dev/pkg/apis"
+)
+
+// Reference is modeled after corev1.ObjectReference, but omits fields
+// unsupported by the tracker, and permits us to extend things in
+// divergent ways.
+type Reference struct {
+	// API version of the referent.
+	// +optional
+	APIVersion string `json:"apiVersion,omitempty"`
+
+	// Kind of the referent.
+	// +optional
+	Kind string `json:"kind,omitempty"`
+
+	// Namespace of the referent.
+	// +optional
+	Namespace string `json:"namespace,omitempty"`
+
+	// Name of the referent.
+	// Mutually exclusive with Selector.
+	// +optional
+	Name string `json:"name,omitempty"`
+
+	// Selector of the referents.
+	// Mutually exclusive with Name.
+	// +optional
+	Selector *metav1.LabelSelector `json:"selector,omitempty"`
+}
+
+// Interface defines the interface through which an object can register
+// that it is tracking another object by reference.
+type Interface interface {
+	// Track tells us that "obj" is tracking changes to the
+	// referenced object.
+	// DEPRECATED: use TrackReference
+	Track(ref corev1.ObjectReference, obj interface{}) error
+
+	// Track tells us that "obj" is tracking changes to the
+	// referenced object.
+	TrackReference(ref Reference, obj interface{}) error
+
+	// OnChanged is a callback to register with the InformerFactory
+	// so that we are notified for appropriate object changes.
+	OnChanged(obj interface{})
+
+	// OnDeletedObserver is a callback to register with the InformerFactory
+	// so that we are notified for deletions of a watching parent to
+	// remove the respective tracking.
+	OnDeletedObserver(obj interface{})
+}
+
+// GroupVersionKind returns the GroupVersion of the object referenced.
+func (ref *Reference) GroupVersionKind() schema.GroupVersionKind {
+	gv, _ := schema.ParseGroupVersion(ref.APIVersion)
+	return schema.GroupVersionKind{
+		Group:   gv.Group,
+		Version: gv.Version,
+		Kind:    ref.Kind,
+	}
+}
+
+// ObjectReference returns the tracker Reference as an ObjectReference.
+func (ref *Reference) ObjectReference() corev1.ObjectReference {
+	return corev1.ObjectReference{
+		APIVersion: ref.APIVersion,
+		Kind:       ref.Kind,
+		Namespace:  ref.Namespace,
+		Name:       ref.Name,
+	}
+}
+
+// ValidateObjectReference validates that the Reference uses a subset suitable for
+// translation to a corev1.ObjectReference.  This helper is intended to simplify
+// validating a particular (narrow) use of tracker.Reference.
+func (ref *Reference) ValidateObjectReference(ctx context.Context) *apis.FieldError {
+	var errs *apis.FieldError
+
+	// Required fields
+	if ref.APIVersion == "" {
+		errs = errs.Also(apis.ErrMissingField("apiVersion"))
+	} else if verrs := validation.IsQualifiedName(ref.APIVersion); len(verrs) != 0 {
+		errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "apiVersion"))
+	}
+	if ref.Kind == "" {
+		errs = errs.Also(apis.ErrMissingField("kind"))
+	} else if verrs := validation.IsCIdentifier(ref.Kind); len(verrs) != 0 {
+		errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "kind"))
+	}
+	if ref.Name == "" {
+		errs = errs.Also(apis.ErrMissingField("name"))
+	} else if verrs := validation.IsDNS1123Label(ref.Name); len(verrs) != 0 {
+		errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "name"))
+	}
+	if ref.Namespace == "" {
+		errs = errs.Also(apis.ErrMissingField("namespace"))
+	} else if verrs := validation.IsDNS1123Label(ref.Namespace); len(verrs) != 0 {
+		errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "namespace"))
+	}
+
+	// Disallowed fields in ObjectReference-compatible context.
+	if ref.Selector != nil {
+		errs = errs.Also(apis.ErrDisallowedFields("selector"))
+	}
+
+	return errs
+}
+
+func (ref *Reference) Validate(ctx context.Context) *apis.FieldError {
+	var errs *apis.FieldError
+
+	// Required fields
+	if ref.APIVersion == "" {
+		errs = errs.Also(apis.ErrMissingField("apiVersion"))
+	} else if verrs := validation.IsQualifiedName(ref.APIVersion); len(verrs) != 0 {
+		errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "apiVersion"))
+	}
+	if ref.Kind == "" {
+		errs = errs.Also(apis.ErrMissingField("kind"))
+	} else if verrs := validation.IsCIdentifier(ref.Kind); len(verrs) != 0 {
+		errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "kind"))
+	}
+	if ref.Namespace == "" {
+		errs = errs.Also(apis.ErrMissingField("namespace"))
+	} else if verrs := validation.IsDNS1123Label(ref.Namespace); len(verrs) != 0 {
+		errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "namespace"))
+	}
+
+	switch {
+	case ref.Selector != nil && ref.Name != "":
+		errs = errs.Also(apis.ErrMultipleOneOf("selector", "name"))
+	case ref.Selector != nil:
+		_, err := metav1.LabelSelectorAsSelector(ref.Selector)
+		if err != nil {
+			errs = errs.Also(apis.ErrInvalidValue(err.Error(), "selector"))
+		}
+
+	case ref.Name != "":
+		if verrs := validation.IsDNS1123Label(ref.Name); len(verrs) != 0 {
+			errs = errs.Also(apis.ErrInvalidValue(strings.Join(verrs, ", "), "name"))
+		}
+	default:
+		errs = errs.Also(apis.ErrMissingOneOf("selector", "name"))
+	}
+
+	return errs
+
+}
diff --git a/vendor/knative.dev/pkg/tracker/zz_generated.deepcopy.go b/vendor/knative.dev/pkg/tracker/zz_generated.deepcopy.go
new file mode 100644
index 000000000..9494168c6
--- /dev/null
+++ b/vendor/knative.dev/pkg/tracker/zz_generated.deepcopy.go
@@ -0,0 +1,46 @@
+// +build !ignore_autogenerated
+
+/*
+Copyright 2020 The Knative 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.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package tracker
+
+import (
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Reference) DeepCopyInto(out *Reference) {
+	*out = *in
+	if in.Selector != nil {
+		in, out := &in.Selector, &out.Selector
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Reference.
+func (in *Reference) DeepCopy() *Reference {
+	if in == nil {
+		return nil
+	}
+	out := new(Reference)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index a5c78cd3e..588b2d864 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -98,6 +98,7 @@ github.com/golang/protobuf/ptypes/wrappers
 # github.com/google/go-cmp v0.5.2
 ## explicit
 github.com/google/go-cmp/cmp
+github.com/google/go-cmp/cmp/cmpopts
 github.com/google/go-cmp/cmp/internal/diff
 github.com/google/go-cmp/cmp/internal/flags
 github.com/google/go-cmp/cmp/internal/function
@@ -213,7 +214,9 @@ go.uber.org/zap/buffer
 go.uber.org/zap/internal/bufferpool
 go.uber.org/zap/internal/color
 go.uber.org/zap/internal/exit
+go.uber.org/zap/internal/ztest
 go.uber.org/zap/zapcore
+go.uber.org/zap/zaptest
 # golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
 ## explicit
 golang.org/x/crypto/ssh/terminal
@@ -783,6 +786,7 @@ knative.dev/pkg/kmp
 knative.dev/pkg/leaderelection
 knative.dev/pkg/logging
 knative.dev/pkg/logging/logkey
+knative.dev/pkg/logging/testing
 knative.dev/pkg/metrics
 knative.dev/pkg/metrics/metricskey
 knative.dev/pkg/network
@@ -790,8 +794,10 @@ knative.dev/pkg/network/handlers
 knative.dev/pkg/profiling
 knative.dev/pkg/ptr
 knative.dev/pkg/reconciler
+knative.dev/pkg/reconciler/testing
 knative.dev/pkg/signals
 knative.dev/pkg/system
+knative.dev/pkg/system/testing
 knative.dev/pkg/test
 knative.dev/pkg/test/ingress
 knative.dev/pkg/test/logging
@@ -801,6 +807,7 @@ knative.dev/pkg/test/zipkin
 knative.dev/pkg/tracing/config
 knative.dev/pkg/tracing/propagation
 knative.dev/pkg/tracing/propagation/tracecontextb3
+knative.dev/pkg/tracker
 knative.dev/pkg/version
 knative.dev/pkg/webhook
 knative.dev/pkg/webhook/certificates/resources


Test patch:
diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_lifecycle_test.go b/pkg/apis/operator/v1alpha1/tektonconfig_lifecycle_test.go
new file mode 100644
index 000000000..3c1b12047
--- /dev/null
+++ b/pkg/apis/operator/v1alpha1/tektonconfig_lifecycle_test.go
@@ -0,0 +1,138 @@
+/*
+Copyright 2020 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 v1alpha1
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	apistest "knative.dev/pkg/apis/testing"
+)
+
+func TestTektonConfigGroupVersionKind(t *testing.T) {
+	r := &TektonConfig{}
+	want := schema.GroupVersionKind{
+		Group:   GroupName,
+		Version: SchemaVersion,
+		Kind:    KindTektonConfig,
+	}
+	if got := r.GroupVersionKind(); got != want {
+		t.Errorf("got: %v, want: %v", got, want)
+	}
+}
+
+func TestTektonConfigHappyPath(t *testing.T) {
+	tp := &TektonConfigStatus{}
+	tp.InitializeConditions()
+
+	apistest.CheckConditionOngoing(tp, DependenciesInstalled, t)
+	apistest.CheckConditionOngoing(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionOngoing(tp, InstallSucceeded, t)
+
+	// Install succeeds.
+	tp.MarkInstallSucceeded()
+	// Dependencies are assumed successful too.
+	apistest.CheckConditionSucceeded(tp, DependenciesInstalled, t)
+	apistest.CheckConditionOngoing(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionSucceeded(tp, InstallSucceeded, t)
+
+	// Deployments are not available at first.
+	tp.MarkDeploymentsNotReady()
+	apistest.CheckConditionSucceeded(tp, DependenciesInstalled, t)
+	apistest.CheckConditionFailed(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionSucceeded(tp, InstallSucceeded, t)
+	if ready := tp.IsReady(); ready {
+		t.Errorf("tp.IsReady() = %v, want false", ready)
+	}
+
+	// Deployments become ready and we're good.
+	tp.MarkDeploymentsAvailable()
+	apistest.CheckConditionSucceeded(tp, DependenciesInstalled, t)
+	apistest.CheckConditionSucceeded(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionSucceeded(tp, InstallSucceeded, t)
+	if ready := tp.IsReady(); !ready {
+		t.Errorf("tp.IsReady() = %v, want true", ready)
+	}
+}
+
+func TestTektonConfigErrorPath(t *testing.T) {
+	tp := &TektonConfigStatus{}
+	tp.InitializeConditions()
+
+	apistest.CheckConditionOngoing(tp, DependenciesInstalled, t)
+	apistest.CheckConditionOngoing(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionOngoing(tp, InstallSucceeded, t)
+
+	// Install fails.
+	tp.MarkInstallFailed("test")
+	apistest.CheckConditionOngoing(tp, DependenciesInstalled, t)
+	apistest.CheckConditionOngoing(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionFailed(tp, InstallSucceeded, t)
+
+	// Dependencies are installing.
+	tp.MarkDependencyInstalling("testing")
+	apistest.CheckConditionFailed(tp, DependenciesInstalled, t)
+	apistest.CheckConditionOngoing(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionFailed(tp, InstallSucceeded, t)
+
+	// Install now succeeds.
+	tp.MarkInstallSucceeded()
+	apistest.CheckConditionFailed(tp, DependenciesInstalled, t)
+	apistest.CheckConditionOngoing(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionSucceeded(tp, InstallSucceeded, t)
+	if ready := tp.IsReady(); ready {
+		t.Errorf("tp.IsReady() = %v, want false", ready)
+	}
+
+	// Deployments become ready
+	tp.MarkDeploymentsAvailable()
+	apistest.CheckConditionFailed(tp, DependenciesInstalled, t)
+	apistest.CheckConditionSucceeded(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionSucceeded(tp, InstallSucceeded, t)
+	if ready := tp.IsReady(); ready {
+		t.Errorf("tp.IsReady() = %v, want false", ready)
+	}
+
+	// Finally, dependencies become available.
+	tp.MarkDependenciesInstalled()
+	apistest.CheckConditionSucceeded(tp, DependenciesInstalled, t)
+	apistest.CheckConditionSucceeded(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionSucceeded(tp, InstallSucceeded, t)
+	if ready := tp.IsReady(); !ready {
+		t.Errorf("tp.IsReady() = %v, want true", ready)
+	}
+}
+
+func TestTektonConfigExternalDependency(t *testing.T) {
+	tp := &TektonConfigStatus{}
+	tp.InitializeConditions()
+
+	// External marks dependency as failed.
+	tp.MarkDependencyMissing("test")
+
+	// Install succeeds.
+	tp.MarkInstallSucceeded()
+	apistest.CheckConditionFailed(tp, DependenciesInstalled, t)
+	apistest.CheckConditionOngoing(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionSucceeded(tp, InstallSucceeded, t)
+
+	// Dependencies are now ready.
+	tp.MarkDependenciesInstalled()
+	apistest.CheckConditionSucceeded(tp, DependenciesInstalled, t)
+	apistest.CheckConditionOngoing(tp, DeploymentsAvailable, t)
+	apistest.CheckConditionSucceeded(tp, InstallSucceeded, t)
+}
diff --git a/pkg/reconciler/common/extensions_test.go b/pkg/reconciler/common/extensions_test.go
index d78ec51fd..5a86b1f66 100644
--- a/pkg/reconciler/common/extensions_test.go
+++ b/pkg/reconciler/common/extensions_test.go
@@ -33,7 +33,10 @@ func (t TestExtension) Transformers(v1alpha1.TektonComponent) []mf.Transformer {
 	return []mf.Transformer{mf.InjectNamespace(string(t))}
 }
 
-func (t TestExtension) Reconcile(context.Context, v1alpha1.TektonComponent) error {
+func (t TestExtension) PreReconcile(context.Context, v1alpha1.TektonComponent) error {
+	return nil
+}
+func (t TestExtension) PostReconcile(context.Context, v1alpha1.TektonComponent) error {
 	return nil
 }
 func (t TestExtension) Finalize(context.Context, v1alpha1.TektonComponent) error {
@@ -71,12 +74,15 @@ func TestExtensions(t *testing.T) {
 				if len(transformers) != test.length {
 					t.Error("Unexpected result")
 				}
-				if ext.Reconcile(context.TODO(), nil) != nil {
+				if ext.PreReconcile(context.TODO(), nil) != nil {
 					t.Error("Unexpected result")
 				}
 				if ext.Finalize(context.TODO(), nil) != nil {
 					t.Error("Unexpected result")
 				}
+				if ext.PostReconcile(context.TODO(), nil) != nil {
+					t.Error("Unexpected result")
+				}
 			}
 		})
 	}
diff --git a/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard_test.go b/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard_test.go
new file mode 100644
index 000000000..548466ff2
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard_test.go
@@ -0,0 +1,44 @@
+/*
+Copyright 2020 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 extension
+
+import (
+	"testing"
+
+	"github.com/tektoncd/operator/pkg/client/injection/client/fake"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	util "github.com/tektoncd/operator/pkg/reconciler/common/testing"
+	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektonconfig/pipeline"
+	ts "knative.dev/pkg/reconciler/testing"
+)
+
+func TestTektonDashboardCreateAndDeleteCR(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+	tConfig := pipeline.GetTektonConfig()
+	err := CreateDashboardCR(tConfig, c.OperatorV1alpha1())
+	util.AssertNotEqual(t, err, nil)
+	err = TektonDashboardCRDelete(c.OperatorV1alpha1().TektonDashboards(), common.DashboardResourceName)
+	util.AssertEqual(t, err, nil)
+}
+
+func TestTektonDashboardCRDelete(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+	err := TektonDashboardCRDelete(c.OperatorV1alpha1().TektonDashboards(), common.DashboardResourceName)
+	util.AssertEqual(t, err, nil)
+}
diff --git a/pkg/reconciler/kubernetes/tektonconfig/pipeline/pipeline_test.go b/pkg/reconciler/kubernetes/tektonconfig/pipeline/pipeline_test.go
new file mode 100644
index 000000000..06a77c07b
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonconfig/pipeline/pipeline_test.go
@@ -0,0 +1,43 @@
+/*
+Copyright 2020 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 pipeline
+
+import (
+	"testing"
+
+	"github.com/tektoncd/operator/pkg/client/injection/client/fake"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	util "github.com/tektoncd/operator/pkg/reconciler/common/testing"
+	ts "knative.dev/pkg/reconciler/testing"
+)
+
+func TestTektonPipelineCreateAndDeleteCR(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+	tConfig := GetTektonConfig()
+	err := CreatePipelineCR(tConfig, c.OperatorV1alpha1())
+	util.AssertNotEqual(t, err, nil)
+	err = TektonPipelineCRDelete(c.OperatorV1alpha1().TektonPipelines(), common.PipelineResourceName)
+	util.AssertEqual(t, err, nil)
+}
+
+func TestTektonPipelineCRDelete(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+	err := TektonPipelineCRDelete(c.OperatorV1alpha1().TektonPipelines(), common.PipelineResourceName)
+	util.AssertEqual(t, err, nil)
+}
diff --git a/pkg/reconciler/kubernetes/tektonconfig/trigger/trigger_test.go b/pkg/reconciler/kubernetes/tektonconfig/trigger/trigger_test.go
new file mode 100644
index 000000000..3ac869802
--- /dev/null
+++ b/pkg/reconciler/kubernetes/tektonconfig/trigger/trigger_test.go
@@ -0,0 +1,44 @@
+/*
+Copyright 2020 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 trigger
+
+import (
+	"testing"
+
+	"github.com/tektoncd/operator/pkg/client/injection/client/fake"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	util "github.com/tektoncd/operator/pkg/reconciler/common/testing"
+	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektonconfig/pipeline"
+	ts "knative.dev/pkg/reconciler/testing"
+)
+
+func TestTektonTriggerCreateAndDeleteCR(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+	tConfig := pipeline.GetTektonConfig()
+	err := CreateTriggerCR(tConfig, c.OperatorV1alpha1())
+	util.AssertNotEqual(t, err, nil)
+	err = TektonTriggerCRDelete(c.OperatorV1alpha1().TektonTriggers(), common.TriggerResourceName)
+	util.AssertEqual(t, err, nil)
+}
+
+func TestTektonTriggerCRDelete(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+	err := TektonTriggerCRDelete(c.OperatorV1alpha1().TektonTriggers(), common.TriggerResourceName)
+	util.AssertEqual(t, err, nil)
+}
diff --git a/pkg/reconciler/openshift/tektonconfig/extension/addon_test.go b/pkg/reconciler/openshift/tektonconfig/extension/addon_test.go
new file mode 100644
index 000000000..0cf9ba404
--- /dev/null
+++ b/pkg/reconciler/openshift/tektonconfig/extension/addon_test.go
@@ -0,0 +1,44 @@
+/*
+Copyright 2020 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 extension
+
+import (
+	"testing"
+
+	"github.com/tektoncd/operator/pkg/client/injection/client/fake"
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	util "github.com/tektoncd/operator/pkg/reconciler/common/testing"
+	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektonconfig/pipeline"
+	ts "knative.dev/pkg/reconciler/testing"
+)
+
+func TestTektonAddonCreateAndDeleteCR(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+	tConfig := pipeline.GetTektonConfig()
+	err := CreateAddonCR(tConfig, c.OperatorV1alpha1())
+	util.AssertNotEqual(t, err, nil)
+	err = TektonAddonCRDelete(c.OperatorV1alpha1().TektonAddons(), common.AddonResourceName)
+	util.AssertEqual(t, err, nil)
+}
+
+func TestTektonAddonCRDelete(t *testing.T) {
+	ctx, _, _ := ts.SetupFakeContextWithCancel(t)
+	c := fake.Get(ctx)
+	err := TektonAddonCRDelete(c.OperatorV1alpha1().TektonAddons(), common.AddonResourceName)
+	util.AssertEqual(t, err, nil)
+}
diff --git a/test/e2e/common/tektonconfigdeployment_test.go b/test/e2e/common/tektonconfigdeployment_test.go
new file mode 100644
index 000000000..52eb08f02
--- /dev/null
+++ b/test/e2e/common/tektonconfigdeployment_test.go
@@ -0,0 +1,59 @@
+// +build e2e
+
+/*
+Copyright 2020 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 common
+
+import (
+	"testing"
+
+	"github.com/tektoncd/operator/pkg/reconciler/common"
+	"github.com/tektoncd/operator/test/client"
+	"github.com/tektoncd/operator/test/resources"
+	"github.com/tektoncd/operator/test/utils"
+)
+
+// TestTektonPipelinesDeployment verifies the TektonPipelines creation, deployment recreation, and TektonPipelines deletion.
+func TestTektonConfigDeployment(t *testing.T) {
+	clients := client.Setup(t)
+
+	crNames := utils.ResourceNames{
+		TektonConfig:    common.ConfigResourceName,
+		TargetNamespace: "tekton-pipelines",
+	}
+
+	utils.CleanupOnInterrupt(func() { utils.TearDownConfig(clients, crNames.TektonConfig) })
+	defer utils.TearDownPipeline(clients, crNames.TektonConfig)
+
+	// Create a TektonConfig
+	t.Run("create-config", func(t *testing.T) {
+		if _, err := resources.EnsureTektonConfigExists(clients.TektonConfig(), crNames); err != nil {
+			t.Fatalf("TektonConfig %q failed to create: %v", crNames.TektonConfig, err)
+		}
+	})
+
+	// Test if TektonConfig can reach the READY status
+	t.Run("ensure-config-ready-status", func(t *testing.T) {
+		resources.AssertTektonConfigCRReadyStatus(t, clients, crNames)
+	})
+
+	// Delete the TektonConfig CR instance to see if all resources will be removed
+	t.Run("delete-config", func(t *testing.T) {
+		resources.AssertTektonConfigCRReadyStatus(t, clients, crNames)
+		resources.TektonConfigCRDelete(t, clients, crNames)
+	})
+}
diff --git a/test/resources/tektonconfigs.go b/test/resources/tektonconfigs.go
new file mode 100644
index 000000000..57707566f
--- /dev/null
+++ b/test/resources/tektonconfigs.go
@@ -0,0 +1,144 @@
+/*
+Copyright 2020 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 resources
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	mfc "github.com/manifestival/client-go-client"
+	mf "github.com/manifestival/manifestival"
+
+	"knative.dev/pkg/test/logging"
+
+	"github.com/tektoncd/operator/test/utils"
+	"k8s.io/apimachinery/pkg/util/wait"
+
+	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
+	pipelinev1alpha1 "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"
+)
+
+// EnsureTektonConfigExists creates a TektonConfig with the name names.TektonConfig, if it does not exist.
+func EnsureTektonConfigExists(clients pipelinev1alpha1.TektonConfigInterface, names utils.ResourceNames) (*v1alpha1.TektonConfig, error) {
+	// If this function is called by the upgrade tests, we only create the custom resource, if it does not exist.
+	tcCR, err := clients.Get(context.TODO(), names.TektonConfig, metav1.GetOptions{})
+	if err == nil {
+		return tcCR, err
+	}
+	if apierrs.IsNotFound(err) {
+		tcCR = &v1alpha1.TektonConfig{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: names.TektonConfig,
+			},
+			Spec: v1alpha1.TektonConfigSpec{
+				CommonSpec: v1alpha1.CommonSpec{
+					TargetNamespace: names.TargetNamespace,
+				},
+			},
+		}
+		return clients.Create(context.TODO(), tcCR, metav1.CreateOptions{})
+	}
+	return tcCR, err
+}
+
+// WaitForTektonConfigState polls the status of the TektonConfig called name
+// from client every `interval` until `inState` returns `true` indicating it
+// is done, returns an error or timeout.
+func WaitForTektonConfigState(clients pipelinev1alpha1.TektonConfigInterface, name string,
+	inState func(s *v1alpha1.TektonConfig, err error) (bool, error)) (*v1alpha1.TektonConfig, error) {
+	span := logging.GetEmitableSpan(context.Background(), fmt.Sprintf("WaitForTektonConfigState/%s/%s", name, "TektonConfigIsReady"))
+	defer span.End()
+
+	var lastState *v1alpha1.TektonConfig
+	waitErr := wait.PollImmediate(Interval, Timeout, func() (bool, error) {
+		lastState, err := clients.Get(context.TODO(), name, metav1.GetOptions{})
+		return inState(lastState, err)
+	})
+
+	if waitErr != nil {
+		return lastState, fmt.Errorf("tektonconfig %s is not in desired state, got: %+v: %w", name, lastState, waitErr)
+	}
+	return lastState, nil
+}
+
+// IsTektonConfigReady will check the status conditions of the TektonConfig and return true if the TektonConfig is ready.
+func IsTektonConfigReady(s *v1alpha1.TektonConfig, err error) (bool, error) {
+	return s.Status.IsReady(), err
+}
+
+// AssertTektonConfigCRReadyStatus verifies if the TektonConfig reaches the READY status.
+func AssertTektonConfigCRReadyStatus(t *testing.T, clients *utils.Clients, names utils.ResourceNames) {
+	if _, err := WaitForTektonConfigState(clients.TektonConfig(), names.TektonConfig,
+		IsTektonConfigReady); err != nil {
+		t.Fatalf("TektonConfigCR %q failed to get to the READY status: %v", names.TektonConfig, err)
+	}
+}
+
+// TektonConfigCRDelete deletes tha TektonConfig to see if all resources will be deleted
+func TektonConfigCRDelete(t *testing.T, clients *utils.Clients, crNames utils.ResourceNames) {
+	if err := clients.TektonConfig().Delete(context.TODO(), crNames.TektonConfig, metav1.DeleteOptions{}); err != nil {
+		t.Fatalf("TektonConfig %q failed to delete: %v", crNames.TektonConfig, err)
+	}
+	err := wait.PollImmediate(Interval, Timeout, func() (bool, error) {
+		_, err := clients.TektonConfig().Get(context.TODO(), crNames.TektonConfig, metav1.GetOptions{})
+		if apierrs.IsNotFound(err) {
+			return true, nil
+		}
+		return false, err
+	})
+	if err != nil {
+		t.Fatal("Timed out waiting on TektonConfig to delete", err)
+	}
+	_, b, _, _ := runtime.Caller(0)
+	m, err := mfc.NewManifest(filepath.Join((filepath.Dir(b)+"/.."), "manifests/"), clients.Config)
+	if err != nil {
+		t.Fatal("Failed to load manifest", err)
+	}
+	if err := verifyNoTektonConfigCR(clients); err != nil {
+		t.Fatal(err)
+	}
+
+	// verify all but the CRD's and the Namespace are gone
+	for _, u := range m.Filter(mf.NoCRDs, mf.Not(mf.ByKind("Namespace"))).Resources() {
+		if _, err := m.Client.Get(&u); !apierrs.IsNotFound(err) {
+			t.Fatalf("The %s %s failed to be deleted: %v", u.GetKind(), u.GetName(), err)
+		}
+	}
+	// verify all the CRD's remain
+	for _, u := range m.Filter(mf.CRDs).Resources() {
+		if _, err := m.Client.Get(&u); apierrs.IsNotFound(err) {
+			t.Fatalf("The %s CRD was deleted", u.GetName())
+		}
+	}
+}
+
+func verifyNoTektonConfigCR(clients *utils.Clients) error {
+	configs, err := clients.TektonConfigAll().List(context.TODO(), metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+	if len(configs.Items) > 0 {
+		return errors.New("Unable to verify cluster-scoped resources are deleted if any TektonConfig exists")
+	}
+	return nil
+}
diff --git a/test/utils/cleanup.go b/test/utils/cleanup.go
index 1b00f19c0..194e60412 100644
--- a/test/utils/cleanup.go
+++ b/test/utils/cleanup.go
@@ -70,3 +70,10 @@ func TearDownNamespace(clients *Clients, name string) {
 		_ = clients.KubeClient.Kube.CoreV1().Namespaces().Delete(context.TODO(), name, metav1.DeleteOptions{})
 	}
 }
+
+// TearDownConfig will delete created TektonConfig CRs using clients.
+func TearDownConfig(clients *Clients, name string) {
+	if clients != nil && clients.Operator != nil {
+		_ = clients.TektonConfig().Delete(context.TODO(), name, metav1.DeleteOptions{})
+	}
+}
diff --git a/test/utils/clients.go b/test/utils/clients.go
index e0d45c7df..955d074b8 100644
--- a/test/utils/clients.go
+++ b/test/utils/clients.go
@@ -117,3 +117,11 @@ func (c *Clients) TektonAddon() operatorv1alpha1.TektonAddonInterface {
 func (c *Clients) TektonAddonAll() operatorv1alpha1.TektonAddonInterface {
 	return c.Operator.TektonAddons()
 }
+
+func (c *Clients) TektonConfig() operatorv1alpha1.TektonConfigInterface {
+	return c.Operator.TektonConfigs()
+}
+
+func (c *Clients) TektonConfigAll() operatorv1alpha1.TektonConfigInterface {
+	return c.Operator.TektonConfigs()
+}
diff --git a/test/utils/names.go b/test/utils/names.go
index d2d48e03f..f4459cb8d 100644
--- a/test/utils/names.go
+++ b/test/utils/names.go
@@ -22,6 +22,7 @@ type ResourceNames struct {
 	TektonTrigger   string
 	TektonDashboard string
 	TektonAddon     string
+	TektonConfig    string
 	Namespace       string
 	TargetNamespace string
 }
diff --git a/vendor/go.uber.org/zap/internal/ztest/doc.go b/vendor/go.uber.org/zap/internal/ztest/doc.go
new file mode 100644
index 000000000..cd4b98cbc
--- /dev/null
+++ b/vendor/go.uber.org/zap/internal/ztest/doc.go
@@ -0,0 +1,24 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// Package ztest provides low-level helpers for testing log output. These
+// utilities are helpful in zap's own unit tests, but any assertions using
+// them are strongly coupled to a single encoding.
+package ztest // import "go.uber.org/zap/internal/ztest"
diff --git a/vendor/go.uber.org/zap/internal/ztest/timeout.go b/vendor/go.uber.org/zap/internal/ztest/timeout.go
new file mode 100644
index 000000000..f7d58f316
--- /dev/null
+++ b/vendor/go.uber.org/zap/internal/ztest/timeout.go
@@ -0,0 +1,59 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package ztest
+
+import (
+	"log"
+	"os"
+	"strconv"
+	"time"
+)
+
+var _timeoutScale = 1.0
+
+// Timeout scales the provided duration by $TEST_TIMEOUT_SCALE.
+func Timeout(base time.Duration) time.Duration {
+	return time.Duration(float64(base) * _timeoutScale)
+}
+
+// Sleep scales the sleep duration by $TEST_TIMEOUT_SCALE.
+func Sleep(base time.Duration) {
+	time.Sleep(Timeout(base))
+}
+
+// Initialize checks the environment and alters the timeout scale accordingly.
+// It returns a function to undo the scaling.
+func Initialize(factor string) func() {
+	original := _timeoutScale
+	fv, err := strconv.ParseFloat(factor, 64)
+	if err != nil {
+		panic(err)
+	}
+	_timeoutScale = fv
+	return func() { _timeoutScale = original }
+}
+
+func init() {
+	if v := os.Getenv("TEST_TIMEOUT_SCALE"); v != "" {
+		Initialize(v)
+		log.Printf("Scaling timeouts by %vx.\n", _timeoutScale)
+	}
+}
diff --git a/vendor/go.uber.org/zap/internal/ztest/writer.go b/vendor/go.uber.org/zap/internal/ztest/writer.go
new file mode 100644
index 000000000..9fdd5805e
--- /dev/null
+++ b/vendor/go.uber.org/zap/internal/ztest/writer.go
@@ -0,0 +1,96 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package ztest
+
+import (
+	"bytes"
+	"errors"
+	"io/ioutil"
+	"strings"
+)
+
+// A Syncer is a spy for the Sync portion of zapcore.WriteSyncer.
+type Syncer struct {
+	err    error
+	called bool
+}
+
+// SetError sets the error that the Sync method will return.
+func (s *Syncer) SetError(err error) {
+	s.err = err
+}
+
+// Sync records that it was called, then returns the user-supplied error (if
+// any).
+func (s *Syncer) Sync() error {
+	s.called = true
+	return s.err
+}
+
+// Called reports whether the Sync method was called.
+func (s *Syncer) Called() bool {
+	return s.called
+}
+
+// A Discarder sends all writes to ioutil.Discard.
+type Discarder struct{ Syncer }
+
+// Write implements io.Writer.
+func (d *Discarder) Write(b []byte) (int, error) {
+	return ioutil.Discard.Write(b)
+}
+
+// FailWriter is a WriteSyncer that always returns an error on writes.
+type FailWriter struct{ Syncer }
+
+// Write implements io.Writer.
+func (w FailWriter) Write(b []byte) (int, error) {
+	return len(b), errors.New("failed")
+}
+
+// ShortWriter is a WriteSyncer whose write method never fails, but
+// nevertheless fails to the last byte of the input.
+type ShortWriter struct{ Syncer }
+
+// Write implements io.Writer.
+func (w ShortWriter) Write(b []byte) (int, error) {
+	return len(b) - 1, nil
+}
+
+// Buffer is an implementation of zapcore.WriteSyncer that sends all writes to
+// a bytes.Buffer. It has convenience methods to split the accumulated buffer
+// on newlines.
+type Buffer struct {
+	bytes.Buffer
+	Syncer
+}
+
+// Lines returns the current buffer contents, split on newlines.
+func (b *Buffer) Lines() []string {
+	output := strings.Split(b.String(), "\n")
+	return output[:len(output)-1]
+}
+
+// Stripped returns the current buffer contents with the last trailing newline
+// stripped.
+func (b *Buffer) Stripped() string {
+	return strings.TrimRight(b.String(), "\n")
+}
diff --git a/vendor/go.uber.org/zap/zaptest/doc.go b/vendor/go.uber.org/zap/zaptest/doc.go
new file mode 100644
index 000000000..b377859c4
--- /dev/null
+++ b/vendor/go.uber.org/zap/zaptest/doc.go
@@ -0,0 +1,22 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// Package zaptest provides a variety of helpers for testing log output.
+package zaptest // import "go.uber.org/zap/zaptest"
diff --git a/vendor/go.uber.org/zap/zaptest/logger.go b/vendor/go.uber.org/zap/zaptest/logger.go
new file mode 100644
index 000000000..1e2451c26
--- /dev/null
+++ b/vendor/go.uber.org/zap/zaptest/logger.go
@@ -0,0 +1,140 @@
+// Copyright (c) 2017 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package zaptest
+
+import (
+	"bytes"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// LoggerOption configures the test logger built by NewLogger.
+type LoggerOption interface {
+	applyLoggerOption(*loggerOptions)
+}
+
+type loggerOptions struct {
+	Level      zapcore.LevelEnabler
+	zapOptions []zap.Option
+}
+
+type loggerOptionFunc func(*loggerOptions)
+
+func (f loggerOptionFunc) applyLoggerOption(opts *loggerOptions) {
+	f(opts)
+}
+
+// Level controls which messages are logged by a test Logger built by
+// NewLogger.
+func Level(enab zapcore.LevelEnabler) LoggerOption {
+	return loggerOptionFunc(func(opts *loggerOptions) {
+		opts.Level = enab
+	})
+}
+
+// WrapOptions adds zap.Option's to a test Logger built by NewLogger.
+func WrapOptions(zapOpts ...zap.Option) LoggerOption {
+	return loggerOptionFunc(func(opts *loggerOptions) {
+		opts.zapOptions = zapOpts
+	})
+}
+
+// NewLogger builds a new Logger that logs all messages to the given
+// testing.TB.
+//
+//   logger := zaptest.NewLogger(t)
+//
+// Use this with a *testing.T or *testing.B to get logs which get printed only
+// if a test fails or if you ran go test -v.
+//
+// The returned logger defaults to logging debug level messages and above.
+// This may be changed by passing a zaptest.Level during construction.
+//
+//   logger := zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))
+//
+// You may also pass zap.Option's to customize test logger.
+//
+//   logger := zaptest.NewLogger(t, zaptest.WrapOptions(zap.AddCaller()))
+func NewLogger(t TestingT, opts ...LoggerOption) *zap.Logger {
+	cfg := loggerOptions{
+		Level: zapcore.DebugLevel,
+	}
+	for _, o := range opts {
+		o.applyLoggerOption(&cfg)
+	}
+
+	writer := newTestingWriter(t)
+	zapOptions := []zap.Option{
+		// Send zap errors to the same writer and mark the test as failed if
+		// that happens.
+		zap.ErrorOutput(writer.WithMarkFailed(true)),
+	}
+	zapOptions = append(zapOptions, cfg.zapOptions...)
+
+	return zap.New(
+		zapcore.NewCore(
+			zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
+			writer,
+			cfg.Level,
+		),
+		zapOptions...,
+	)
+}
+
+// testingWriter is a WriteSyncer that writes to the given testing.TB.
+type testingWriter struct {
+	t TestingT
+
+	// If true, the test will be marked as failed if this testingWriter is
+	// ever used.
+	markFailed bool
+}
+
+func newTestingWriter(t TestingT) testingWriter {
+	return testingWriter{t: t}
+}
+
+// WithMarkFailed returns a copy of this testingWriter with markFailed set to
+// the provided value.
+func (w testingWriter) WithMarkFailed(v bool) testingWriter {
+	w.markFailed = v
+	return w
+}
+
+func (w testingWriter) Write(p []byte) (n int, err error) {
+	n = len(p)
+
+	// Strip trailing newline because t.Log always adds one.
+	p = bytes.TrimRight(p, "\n")
+
+	// Note: t.Log is safe for concurrent use.
+	w.t.Logf("%s", p)
+	if w.markFailed {
+		w.t.Fail()
+	}
+
+	return n, nil
+}
+
+func (w testingWriter) Sync() error {
+	return nil
+}
diff --git a/vendor/go.uber.org/zap/zaptest/testingt.go b/vendor/go.uber.org/zap/zaptest/testingt.go
new file mode 100644
index 000000000..792463be3
--- /dev/null
+++ b/vendor/go.uber.org/zap/zaptest/testingt.go
@@ -0,0 +1,47 @@
+// Copyright (c) 2017 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package zaptest
+
+// TestingT is a subset of the API provided by all *testing.T and *testing.B
+// objects.
+type TestingT interface {
+	// Logs the given message without failing the test.
+	Logf(string, ...interface{})
+
+	// Logs the given message and marks the test as failed.
+	Errorf(string, ...interface{})
+
+	// Marks the test as failed.
+	Fail()
+
+	// Returns true if the test has been marked as failed.
+	Failed() bool
+
+	// Returns the name of the test.
+	Name() string
+
+	// Marks the test as failed and stops execution of that test.
+	FailNow()
+}
+
+// Note: We currently only rely on Logf. We are including Errorf and FailNow
+// in the interface in anticipation of future need since we can't extend the
+// interface without a breaking change.
diff --git a/vendor/go.uber.org/zap/zaptest/timeout.go b/vendor/go.uber.org/zap/zaptest/timeout.go
new file mode 100644
index 000000000..f0be44416
--- /dev/null
+++ b/vendor/go.uber.org/zap/zaptest/timeout.go
@@ -0,0 +1,45 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package zaptest
+
+import (
+	"time"
+
+	"go.uber.org/zap/internal/ztest"
+)
+
+// Timeout scales the provided duration by $TEST_TIMEOUT_SCALE.
+//
+// Deprecated: This function is intended for internal testing and shouldn't be
+// used outside zap itself. It was introduced before Go supported internal
+// packages.
+func Timeout(base time.Duration) time.Duration {
+	return ztest.Timeout(base)
+}
+
+// Sleep scales the sleep duration by $TEST_TIMEOUT_SCALE.
+//
+// Deprecated: This function is intended for internal testing and shouldn't be
+// used outside zap itself. It was introduced before Go supported internal
+// packages.
+func Sleep(base time.Duration) {
+	ztest.Sleep(base)
+}
diff --git a/vendor/go.uber.org/zap/zaptest/writer.go b/vendor/go.uber.org/zap/zaptest/writer.go
new file mode 100644
index 000000000..0701630e1
--- /dev/null
+++ b/vendor/go.uber.org/zap/zaptest/writer.go
@@ -0,0 +1,44 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package zaptest
+
+import "go.uber.org/zap/internal/ztest"
+
+type (
+	// A Syncer is a spy for the Sync portion of zapcore.WriteSyncer.
+	Syncer = ztest.Syncer
+
+	// A Discarder sends all writes to ioutil.Discard.
+	Discarder = ztest.Discarder
+
+	// FailWriter is a WriteSyncer that always returns an error on writes.
+	FailWriter = ztest.FailWriter
+
+	// ShortWriter is a WriteSyncer whose write method never returns an error,
+	// but always reports that it wrote one byte less than the input slice's
+	// length (thus, a "short write").
+	ShortWriter = ztest.ShortWriter
+
+	// Buffer is an implementation of zapcore.WriteSyncer that sends all writes to
+	// a bytes.Buffer. It has convenience methods to split the accumulated buffer
+	// on newlines.
+	Buffer = ztest.Buffer
+)
diff --git a/vendor/knative.dev/pkg/logging/testing/util.go b/vendor/knative.dev/pkg/logging/testing/util.go
new file mode 100644
index 000000000..3aa4d8357
--- /dev/null
+++ b/vendor/knative.dev/pkg/logging/testing/util.go
@@ -0,0 +1,41 @@
+/*
+Copyright 2018 The Knative 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 testing
+
+import (
+	"context"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zaptest"
+
+	"knative.dev/pkg/logging"
+)
+
+// TestLogger gets a logger to use in unit and end to end tests
+func TestLogger(t zaptest.TestingT) *zap.SugaredLogger {
+	opts := zaptest.WrapOptions(
+		zap.AddCaller(),
+		zap.Development(),
+	)
+
+	return zaptest.NewLogger(t, opts).Sugar()
+}
+
+// TestContextWithLogger returns a context with a logger to be used in tests
+func TestContextWithLogger(t zaptest.TestingT) context.Context {
+	return logging.WithLogger(context.Background(), TestLogger(t))
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/actions.go b/vendor/knative.dev/pkg/reconciler/testing/actions.go
new file mode 100644
index 000000000..7dc967b92
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/actions.go
@@ -0,0 +1,76 @@
+/*
+Copyright 2019 The Knative 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 testing
+
+import (
+	"fmt"
+
+	clientgotesting "k8s.io/client-go/testing"
+)
+
+// Actions stores list of Actions recorded by the reactors.
+type Actions struct {
+	Gets              []clientgotesting.GetAction
+	Creates           []clientgotesting.CreateAction
+	Updates           []clientgotesting.UpdateAction
+	Deletes           []clientgotesting.DeleteAction
+	DeleteCollections []clientgotesting.DeleteCollectionAction
+	Patches           []clientgotesting.PatchAction
+}
+
+// ActionRecorder contains list of K8s request actions.
+type ActionRecorder interface {
+	Actions() []clientgotesting.Action
+}
+
+// ActionRecorderList is a list of ActionRecorder objects.
+type ActionRecorderList []ActionRecorder
+
+// ActionsByVerb fills in Actions objects, sorting the actions
+// by verb.
+func (l ActionRecorderList) ActionsByVerb() (Actions, error) {
+	var a Actions
+
+	for _, recorder := range l {
+		for _, action := range recorder.Actions() {
+			switch action.GetVerb() {
+			case "get":
+				a.Gets = append(a.Gets,
+					action.(clientgotesting.GetAction))
+			case "create":
+				a.Creates = append(a.Creates,
+					action.(clientgotesting.CreateAction))
+			case "update":
+				a.Updates = append(a.Updates,
+					action.(clientgotesting.UpdateAction))
+			case "delete":
+				a.Deletes = append(a.Deletes,
+					action.(clientgotesting.DeleteAction))
+			case "delete-collection":
+				a.DeleteCollections = append(a.DeleteCollections,
+					action.(clientgotesting.DeleteCollectionAction))
+			case "patch":
+				a.Patches = append(a.Patches,
+					action.(clientgotesting.PatchAction))
+			case "list", "watch": // avoid 'unexpected verb list/watch' error
+			default:
+				return a, fmt.Errorf("unexpected verb %v: %+v", action.GetVerb(), action)
+			}
+		}
+	}
+	return a, nil
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/clock.go b/vendor/knative.dev/pkg/reconciler/testing/clock.go
new file mode 100644
index 000000000..44ba77cdb
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/clock.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2019 The Knative 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 testing
+
+import (
+	"time"
+)
+
+type FakeClock struct {
+	Time time.Time
+}
+
+func (c FakeClock) Now() time.Time {
+	return c.Time
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/context.go b/vendor/knative.dev/pkg/reconciler/testing/context.go
new file mode 100644
index 000000000..7c36d8ed7
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/context.go
@@ -0,0 +1,45 @@
+/*
+Copyright 2019 The Knative 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 testing
+
+import (
+	"context"
+
+	"go.uber.org/zap/zaptest"
+
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/record"
+
+	"knative.dev/pkg/controller"
+	"knative.dev/pkg/injection"
+	logtesting "knative.dev/pkg/logging/testing"
+)
+
+// SetupFakeContext sets up the the Context and the fake informers for the tests.
+func SetupFakeContext(t zaptest.TestingT) (context.Context, []controller.Informer) {
+	c, _, is := SetupFakeContextWithCancel(t)
+	return c, is
+}
+
+// SetupFakeContextWithCancel sets up the the Context and the fake informers for the tests
+// The provided context can be canceled using provided callback.
+func SetupFakeContextWithCancel(t zaptest.TestingT) (context.Context, context.CancelFunc, []controller.Informer) {
+	ctx, c := context.WithCancel(logtesting.TestContextWithLogger(t))
+	ctx = controller.WithEventRecorder(ctx, record.NewFakeRecorder(1000))
+	ctx, is := injection.Fake.SetupInformers(ctx, &rest.Config{})
+	return ctx, c, is
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/events.go b/vendor/knative.dev/pkg/reconciler/testing/events.go
new file mode 100644
index 000000000..498b04f7e
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/events.go
@@ -0,0 +1,44 @@
+/*
+Copyright 2019 The Knative 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 testing
+
+import (
+	"fmt"
+
+	"k8s.io/client-go/tools/record"
+)
+
+// EventList exports all events during reconciliation through fake event recorder
+// with event channel with buffer of given size.
+type EventList struct {
+	Recorder *record.FakeRecorder
+}
+
+// Events iterates over events received from channel in fake event recorder and returns all.
+func (l EventList) Events() []string {
+	close(l.Recorder.Events)
+	events := []string{}
+	for e := range l.Recorder.Events {
+		events = append(events, e)
+	}
+	return events
+}
+
+// Eventf formats as FakeRecorder does.
+func Eventf(eventType, reason, messageFmt string, args ...interface{}) string {
+	return fmt.Sprintf(eventType+" "+reason+" "+messageFmt, args...)
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/generate_name_reactor.go b/vendor/knative.dev/pkg/reconciler/testing/generate_name_reactor.go
new file mode 100644
index 000000000..57ea71d9e
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/generate_name_reactor.go
@@ -0,0 +1,84 @@
+/*
+Copyright 2019 The Knative 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 testing
+
+import (
+	"fmt"
+	"sync/atomic"
+
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/runtime"
+	clientgotesting "k8s.io/client-go/testing"
+)
+
+// GenerateNameReactor will simulate the k8s API server
+// and generate a name for resources who's metadata.generateName
+// property is set. This happens only for CreateAction types
+//
+// This generator is deterministic (unliked k8s) and uses a global
+// counter to help make test names predictable
+type GenerateNameReactor struct {
+	count int64
+}
+
+// Handles contains all the logic to generate the name and mutates
+// the create action object
+//
+// This is a hack as 'React' is passed a DeepCopy of the action hence
+// this is the only opportunity to 'mutate' the action in the
+// ReactionChain and have to continue executing additional reactors
+//
+// We should push changes upstream to client-go to help us with
+// mocking
+func (r *GenerateNameReactor) Handles(action clientgotesting.Action) bool {
+	create, ok := action.(clientgotesting.CreateAction)
+	if !ok {
+		return false
+	}
+
+	objMeta, err := meta.Accessor(create.GetObject())
+	if err != nil {
+		return false
+	}
+
+	if objMeta.GetName() != "" {
+		return false
+	}
+
+	if objMeta.GetGenerateName() == "" {
+		return false
+	}
+
+	val := atomic.AddInt64(&r.count, 1)
+
+	objMeta.SetName(fmt.Sprintf("%s%05d", objMeta.GetGenerateName(), val))
+
+	return false
+}
+
+// React is noop-function
+func (r *GenerateNameReactor) React(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
+	return false, nil, nil
+}
+
+var _ clientgotesting.Reactor = (*GenerateNameReactor)(nil)
+
+// PrependGenerateNameReactor will instrument a client-go testing Fake
+// with a reactor that simulates 'generateName' functionality
+func PrependGenerateNameReactor(f *clientgotesting.Fake) {
+	f.ReactionChain = append([]clientgotesting.Reactor{&GenerateNameReactor{}}, f.ReactionChain...)
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/hooks.go b/vendor/knative.dev/pkg/reconciler/testing/hooks.go
new file mode 100644
index 000000000..cde3d7d21
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/hooks.go
@@ -0,0 +1,183 @@
+/*
+Copyright 2019 The Knative 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 testing includes utilities for testing controllers.
+package testing
+
+import (
+	"errors"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	kubetesting "k8s.io/client-go/testing"
+)
+
+// HookResult is the return value of hook functions.
+type HookResult bool
+
+const (
+	// HookComplete indicates the hook function completed, and WaitForHooks should
+	// not wait for it.
+	HookComplete HookResult = true
+	// HookIncomplete indicates the hook function is incomplete, and WaitForHooks
+	// should wait for it to complete.
+	HookIncomplete HookResult = false
+)
+
+/*
+CreateHookFunc is a function for handling a Create hook. Its runtime.Object
+parameter will be the Kubernetes resource created. The resource can be cast
+to its actual type like this:
+
+		pod := obj.(*v1.Pod)
+
+A return value of true marks the hook as completed. Returning false allows
+the hook to run again when the next resource of the requested type is
+created.
+*/
+type CreateHookFunc func(runtime.Object) HookResult
+
+/*
+UpdateHookFunc is a function for handling an update hook. its runtime.Object
+parameter will be the Kubernetes resource updated. The resource can be cast
+to its actual type like this:
+
+		pod := obj.(*v1.Pod)
+
+A return value of true marks the hook as completed. Returning false allows
+the hook to run again when the next resource of the requested type is
+updated.
+*/
+type UpdateHookFunc func(runtime.Object) HookResult
+
+/*
+DeleteHookFunc is a function for handling a delete hook. Its name parameter will
+be the name of the resource deleted. The resource itself is not available to
+the reactor.
+*/
+type DeleteHookFunc func(string) HookResult
+
+/*
+Hooks is a utility struct that simplifies controller testing with fake
+clients. A Hooks struct allows attaching hook functions to actions (create,
+update, delete) on a specified resource type within a fake client and ensuring
+that all hooks complete in a timely manner.
+*/
+type Hooks struct {
+	completionCh    chan int32
+	completionIndex int32
+
+	// Denotes whether or not the registered hooks should no longer be called
+	// because they have already been waited upon.
+	// This uses a Mutex over a channel to guarantee that after WaitForHooks
+	// returns no hooked functions will be called.
+	closed bool
+	mutex  sync.RWMutex
+}
+
+// NewHooks returns a Hooks struct that can be used to attach hooks to one or
+// more fake clients and wait for all hooks to complete.
+// TODO(grantr): Allow validating that a hook never fires
+func NewHooks() *Hooks {
+	return &Hooks{
+		completionCh:    make(chan int32, 100),
+		completionIndex: -1,
+	}
+}
+
+// OnCreate attaches a create hook to the given Fake. The hook function is
+// executed every time a resource of the given type is created.
+func (h *Hooks) OnCreate(fake *kubetesting.Fake, resource string, rf CreateHookFunc) {
+	index := atomic.AddInt32(&h.completionIndex, 1)
+	fake.PrependReactor("create", resource, func(a kubetesting.Action) (bool, runtime.Object, error) {
+		obj := a.(kubetesting.CreateActionImpl).Object
+
+		h.mutex.RLock()
+		defer h.mutex.RUnlock()
+		if !h.closed && rf(obj) == HookComplete {
+			h.completionCh <- index
+		}
+		return false, nil, nil
+	})
+}
+
+// OnUpdate attaches an update hook to the given Fake. The hook function is
+// executed every time a resource of the given type is updated.
+func (h *Hooks) OnUpdate(fake *kubetesting.Fake, resource string, rf UpdateHookFunc) {
+	index := atomic.AddInt32(&h.completionIndex, 1)
+	fake.PrependReactor("update", resource, func(a kubetesting.Action) (bool, runtime.Object, error) {
+		obj := a.(kubetesting.UpdateActionImpl).Object
+
+		h.mutex.RLock()
+		defer h.mutex.RUnlock()
+		if !h.closed && rf(obj) == HookComplete {
+			h.completionCh <- index
+		}
+		return false, nil, nil
+	})
+}
+
+// OnDelete attaches a delete hook to the given Fake. The hook function is
+// executed every time a resource of the given type is deleted.
+func (h *Hooks) OnDelete(fake *kubetesting.Fake, resource string, rf DeleteHookFunc) {
+	index := atomic.AddInt32(&h.completionIndex, 1)
+	fake.PrependReactor("delete", resource, func(a kubetesting.Action) (bool, runtime.Object, error) {
+		name := a.(kubetesting.DeleteActionImpl).Name
+
+		h.mutex.RLock()
+		defer h.mutex.RUnlock()
+		if !h.closed && rf(name) == HookComplete {
+			h.completionCh <- index
+		}
+		return false, nil, nil
+	})
+}
+
+// WaitForHooks waits until all attached hooks have returned true at least once.
+// If the given timeout expires before that happens, an error is returned.
+// The registered actions will no longer be executed after WaitForHooks has
+// returned.
+func (h *Hooks) WaitForHooks(timeout time.Duration) error {
+	defer func() {
+		h.mutex.Lock()
+		defer h.mutex.Unlock()
+		h.closed = true
+	}()
+
+	ci := int(atomic.LoadInt32(&h.completionIndex))
+	if ci == -1 {
+		return nil
+	}
+
+	// Convert index to count.
+	ci++
+	timer := time.After(timeout)
+	hookCompletions := map[int32]HookResult{}
+	for {
+		select {
+		case i := <-h.completionCh:
+			hookCompletions[i] = HookComplete
+			if len(hookCompletions) == ci {
+				atomic.StoreInt32(&h.completionIndex, -1)
+				return nil
+			}
+		case <-timer:
+			return errors.New("timed out waiting for hooks to complete")
+		}
+	}
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/reactions.go b/vendor/knative.dev/pkg/reconciler/testing/reactions.go
new file mode 100644
index 000000000..09caae767
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/reactions.go
@@ -0,0 +1,66 @@
+/*
+Copyright 2019 The Knative 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 testing
+
+import (
+	"context"
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	clientgotesting "k8s.io/client-go/testing"
+
+	"knative.dev/pkg/apis"
+)
+
+// InduceFailure is used in conjunction with TableTest's WithReactors field.
+// Tests that want to induce a failure in a row of a TableTest would add:
+//   WithReactors: []clientgotesting.ReactionFunc{
+//      // Makes calls to create revisions return an error.
+//      InduceFailure("create", "revisions"),
+//   },
+func InduceFailure(verb, resource string) clientgotesting.ReactionFunc {
+	return func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
+		if !action.Matches(verb, resource) {
+			return false, nil, nil
+		}
+		return true, nil, fmt.Errorf("inducing failure for %s %s", action.GetVerb(), action.GetResource().Resource)
+	}
+}
+
+func ValidateCreates(ctx context.Context, action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
+	got := action.(clientgotesting.CreateAction).GetObject()
+	obj, ok := got.(apis.Validatable)
+	if !ok {
+		return false, nil, nil
+	}
+	if err := obj.Validate(ctx); err != nil {
+		return true, nil, err
+	}
+	return false, nil, nil
+}
+
+func ValidateUpdates(ctx context.Context, action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
+	got := action.(clientgotesting.UpdateAction).GetObject()
+	obj, ok := got.(apis.Validatable)
+	if !ok {
+		return false, nil, nil
+	}
+	if err := obj.Validate(ctx); err != nil {
+		return true, nil, err
+	}
+	return false, nil, nil
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/sorter.go b/vendor/knative.dev/pkg/reconciler/testing/sorter.go
new file mode 100644
index 000000000..2c1e91923
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/sorter.go
@@ -0,0 +1,93 @@
+/*
+Copyright 2019 The Knative 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 testing
+
+import (
+	"fmt"
+	"reflect"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	util_runtime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/client-go/tools/cache"
+)
+
+func NewObjectSorter(scheme *runtime.Scheme) ObjectSorter {
+	cache := make(map[reflect.Type]cache.Indexer)
+
+	for _, v := range scheme.AllKnownTypes() {
+		cache[v] = emptyIndexer()
+	}
+
+	ls := ObjectSorter{
+		cache: cache,
+	}
+
+	return ls
+}
+
+type ObjectSorter struct {
+	cache map[reflect.Type]cache.Indexer
+}
+
+func (o *ObjectSorter) AddObjects(objs ...runtime.Object) {
+	for _, obj := range objs {
+		t := reflect.TypeOf(obj).Elem()
+		indexer, ok := o.cache[t]
+		if !ok {
+			panic(fmt.Sprintf("Unrecognized type %T", obj))
+		}
+		indexer.Add(obj)
+	}
+}
+
+func (o *ObjectSorter) ObjectsForScheme(scheme *runtime.Scheme) []runtime.Object {
+	var objs []runtime.Object
+
+	for _, t := range scheme.AllKnownTypes() {
+		indexer := o.cache[t]
+		for _, item := range indexer.List() {
+			objs = append(objs, item.(runtime.Object))
+		}
+	}
+
+	return objs
+}
+
+func (o *ObjectSorter) ObjectsForSchemeFunc(funcs ...func(scheme *runtime.Scheme) error) []runtime.Object {
+	scheme := runtime.NewScheme()
+
+	for _, addToScheme := range funcs {
+		util_runtime.Must(addToScheme(scheme))
+	}
+
+	return o.ObjectsForScheme(scheme)
+}
+
+func (o *ObjectSorter) IndexerForObjectType(obj runtime.Object) cache.Indexer {
+	objType := reflect.TypeOf(obj).Elem()
+
+	indexer, ok := o.cache[objType]
+
+	if !ok {
+		panic(fmt.Sprintf("indexer for type %v doesn't exist", objType.Name()))
+	}
+
+	return indexer
+}
+
+func emptyIndexer() cache.Indexer {
+	return cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/table.go b/vendor/knative.dev/pkg/reconciler/testing/table.go
new file mode 100644
index 000000000..aa5a92d16
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/table.go
@@ -0,0 +1,397 @@
+/*
+Copyright 2019 The Knative 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 testing
+
+import (
+	"context"
+	"path"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"go.uber.org/zap"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/sets"
+	clientgotesting "k8s.io/client-go/testing"
+	"k8s.io/client-go/tools/cache"
+
+	"knative.dev/pkg/controller"
+	"knative.dev/pkg/kmeta"
+	"knative.dev/pkg/logging"
+	"knative.dev/pkg/logging/logkey"
+	_ "knative.dev/pkg/system/testing" // Setup system.Namespace()
+)
+
+// TableRow holds a single row of our table test.
+type TableRow struct {
+	// Name is a descriptive name for this test suitable as a first argument to t.Run()
+	Name string
+
+	// Ctx is the context to pass to Reconcile. Defaults to context.Background()
+	Ctx context.Context
+
+	// Objects holds the state of the world at the onset of reconciliation.
+	Objects []runtime.Object
+
+	// Key is the parameter to reconciliation.
+	// This has the form "namespace/name".
+	Key string
+
+	// WantErr holds whether we should expect the reconciliation to result in an error.
+	WantErr bool
+
+	// WantCreates holds the ordered list of Create calls we expect during reconciliation.
+	WantCreates []runtime.Object
+
+	// WantUpdates holds the ordered list of Update calls we expect during reconciliation.
+	WantUpdates []clientgotesting.UpdateActionImpl
+
+	// WantStatusUpdates holds the ordered list of Update calls, with `status` subresource set,
+	// that we expect during reconciliation.
+	WantStatusUpdates []clientgotesting.UpdateActionImpl
+
+	// WantDeletes holds the ordered list of Delete calls we expect during reconciliation.
+	WantDeletes []clientgotesting.DeleteActionImpl
+
+	// WantDeleteCollections holds the ordered list of DeleteCollection calls we expect during reconciliation.
+	WantDeleteCollections []clientgotesting.DeleteCollectionActionImpl
+
+	// WantPatches holds the ordered list of Patch calls we expect during reconciliation.
+	WantPatches []clientgotesting.PatchActionImpl
+
+	// WantEvents holds the ordered list of events we expect during reconciliation.
+	WantEvents []string
+
+	// WithReactors is a set of functions that are installed as Reactors for the execution
+	// of this row of the table-driven-test.
+	WithReactors []clientgotesting.ReactionFunc
+
+	// For cluster-scoped resources like ClusterIngress, it does not have to be
+	// in the same namespace with its child resources.
+	SkipNamespaceValidation bool
+
+	// PostConditions allows custom assertions to be made after reconciliation
+	PostConditions []func(*testing.T, *TableRow)
+
+	// Reconciler holds the controller.Reconciler that was used to evaluate this row.
+	// It is populated here to make it accessible to PostConditions.
+	Reconciler controller.Reconciler
+
+	// OtherTestData is arbitrary data needed for the test. It is not used directly by the table
+	// testing framework. Instead it is used in the test method. E.g. setting up the responses for a
+	// mock client can go in here.
+	OtherTestData map[string]interface{}
+
+	CmpOpts []cmp.Option
+}
+
+var (
+	ignoreLastTransitionTime = cmp.FilterPath(func(p cmp.Path) bool {
+		return strings.HasSuffix(p.String(), "LastTransitionTime.Inner.Time")
+	}, cmp.Ignore())
+
+	ignoreQuantity = cmpopts.IgnoreUnexported(resource.Quantity{})
+	defaultCmpOpts = []cmp.Option{ignoreLastTransitionTime, ignoreQuantity, cmpopts.EquateEmpty()}
+)
+
+func objKey(o runtime.Object) string {
+	on := o.(kmeta.Accessor)
+
+	var typeOf string
+	if gvk := on.GroupVersionKind(); gvk.Group != "" {
+		// This must be populated if we're dealing with unstructured.Unstructured.
+		typeOf = gvk.String()
+	} else if or, ok := on.(kmeta.OwnerRefable); ok {
+		// This is typically implemented by Knative resources.
+		typeOf = or.GetGroupVersionKind().String()
+	} else {
+		// Worst case, fallback on a non-GVK string.
+		typeOf = reflect.TypeOf(o).String()
+	}
+
+	// namespace + name is not unique, and the tests don't populate k8s kind
+	// information, so use GoLang's type name as part of the key.
+	return path.Join(typeOf, on.GetNamespace(), on.GetName())
+}
+
+// Factory returns a Reconciler.Interface to perform reconciliation in table test, and
+// ActionRecorderList/EventList to capture k8s actions/events produced during reconciliation.
+type Factory func(*testing.T, *TableRow) (controller.Reconciler, ActionRecorderList, EventList)
+
+// Test executes the single table test.
+func (r *TableRow) Test(t *testing.T, factory Factory) {
+	t.Helper()
+	c, recorderList, eventList := factory(t, r)
+
+	// Set the Reconciler for PostConditions to access it post-Reconcile()
+	r.Reconciler = c
+
+	// Set context to not be nil.
+	ctx := r.Ctx
+	if ctx == nil {
+		ctx = context.Background()
+	} else {
+		// If we have logger setup on the context, decorate it with the key, so that the logs
+		// look like in prod.
+		l := logging.FromContext(ctx)
+		l = l.With(zap.String(logkey.Key, r.Key))
+		ctx = logging.WithLogger(ctx, l)
+	}
+
+	// Run the Reconcile we're testing.
+	if err := c.Reconcile(ctx, r.Key); (err != nil) != r.WantErr {
+		t.Errorf("Reconcile() error = %v, WantErr %v", err, r.WantErr)
+	}
+
+	expectedNamespace, _, _ := cache.SplitMetaNamespaceKey(r.Key)
+
+	actions, err := recorderList.ActionsByVerb()
+	if err != nil {
+		t.Errorf("Error capturing actions by verb: %q", err)
+	}
+
+	effectiveOpts := append(r.CmpOpts, defaultCmpOpts...)
+	// Previous state is used to diff resource expected state for update requests that were missed.
+	objPrevState := make(map[string]runtime.Object, len(r.Objects))
+	for _, o := range r.Objects {
+		objPrevState[objKey(o)] = o
+	}
+
+	for i, want := range r.WantCreates {
+		if i >= len(actions.Creates) {
+			t.Errorf("Missing create: %#v", want)
+			continue
+		}
+		got := actions.Creates[i]
+		obj := got.GetObject()
+		objPrevState[objKey(obj)] = obj
+
+		if !r.SkipNamespaceValidation && got.GetNamespace() != expectedNamespace {
+			t.Errorf("Unexpected action[%d]: %#v", i, got)
+		}
+
+		if !cmp.Equal(want, obj, effectiveOpts...) {
+			t.Errorf("Unexpected create (-want, +got):\n%s",
+				cmp.Diff(want, obj, effectiveOpts...))
+		}
+	}
+	if got, want := len(actions.Creates), len(r.WantCreates); got > want {
+		for _, extra := range actions.Creates[want:] {
+			t.Errorf("Extra create: %#v", extra.GetObject())
+		}
+	}
+
+	updates := filterUpdatesWithSubresource("", actions.Updates)
+	for i, want := range r.WantUpdates {
+		if i >= len(updates) {
+			wo := want.GetObject()
+			key := objKey(wo)
+			oldObj, ok := objPrevState[key]
+			if !ok {
+				t.Errorf("Object %s was never created: want: %#v", key, wo)
+				continue
+			}
+			t.Errorf("Missing update for %s (-want, +prevState):\n%s", key,
+				cmp.Diff(wo, oldObj, effectiveOpts...))
+			continue
+		}
+
+		if want.GetSubresource() != "" {
+			t.Errorf("Expectation was invalid - it should not include a subresource: %#v", want)
+		}
+
+		got := updates[i].GetObject()
+
+		// Update the object state.
+		objPrevState[objKey(got)] = got
+
+		if !cmp.Equal(want.GetObject(), got, effectiveOpts...) {
+			t.Errorf("Unexpected update (-want, +got):\n%s",
+				cmp.Diff(want.GetObject(), got, effectiveOpts...))
+		}
+	}
+	if got, want := len(updates), len(r.WantUpdates); got > want {
+		for _, extra := range updates[want:] {
+			t.Errorf("Extra update: %#v", extra.GetObject())
+		}
+	}
+
+	// TODO(#2843): refactor.
+	statusUpdates := filterUpdatesWithSubresource("status", actions.Updates)
+	for i, want := range r.WantStatusUpdates {
+		if i >= len(statusUpdates) {
+			wo := want.GetObject()
+			key := objKey(wo)
+			oldObj, ok := objPrevState[key]
+			if !ok {
+				t.Errorf("Object %s was never created: want: %#v", key, wo)
+				continue
+			}
+			t.Errorf("Missing status update for %s (-want, +prevState):\n%s", key,
+				cmp.Diff(wo, oldObj, effectiveOpts...))
+			continue
+		}
+
+		got := statusUpdates[i].GetObject()
+
+		// Update the object state.
+		objPrevState[objKey(got)] = got
+
+		if !cmp.Equal(want.GetObject(), got, effectiveOpts...) {
+			t.Errorf("Unexpected status update (-want, +got):\n%s\nFull: %v",
+				cmp.Diff(want.GetObject(), got, effectiveOpts...), got)
+		}
+	}
+	if got, want := len(statusUpdates), len(r.WantStatusUpdates); got > want {
+		for _, extra := range statusUpdates[want:] {
+			wo := extra.GetObject()
+			key := objKey(wo)
+			oldObj, ok := objPrevState[key]
+			if !ok {
+				t.Errorf("Object %s was never created: want: %#v", key, wo)
+				continue
+			}
+			t.Errorf("Extra status update for %s (-extra, +prevState):\n%s", key,
+				cmp.Diff(wo, oldObj, effectiveOpts...))
+		}
+	}
+
+	if len(statusUpdates)+len(updates) != len(actions.Updates) {
+		var unexpected []runtime.Object
+
+		for _, update := range actions.Updates {
+			if update.GetSubresource() != "status" && update.GetSubresource() != "" {
+				unexpected = append(unexpected, update.GetObject())
+			}
+		}
+
+		t.Errorf("Unexpected subresource updates occurred %#v", unexpected)
+	}
+
+	// Build a set of unique strings that represent type-name{-namespace}.
+	// Adding type will help catch the bugs where several similarly named
+	// resources are deleted (and some should or should not).
+	gotDeletes := make(sets.String, len(actions.Deletes))
+	for _, w := range actions.Deletes {
+		n := w.GetResource().Resource + "~~" + w.GetName()
+		if !r.SkipNamespaceValidation {
+			n += "~~" + w.GetNamespace()
+		}
+		gotDeletes.Insert(n)
+	}
+	wantDeletes := make(sets.String, len(actions.Deletes))
+	for _, w := range r.WantDeletes {
+		n := w.GetResource().Resource + "~~" + w.GetName()
+		if !r.SkipNamespaceValidation {
+			n += "~~" + w.GetNamespace()
+		}
+		wantDeletes.Insert(n)
+	}
+	if !gotDeletes.Equal(wantDeletes) {
+		if extra := gotDeletes.Difference(wantDeletes); len(extra) > 0 {
+			t.Errorf("Extra or unexpected deletes: %v", extra.UnsortedList())
+		}
+		if missing := wantDeletes.Difference(gotDeletes); len(missing) > 0 {
+			t.Errorf("Missing deletes: %v", missing.UnsortedList())
+		}
+	}
+
+	for i, want := range r.WantPatches {
+		if i >= len(actions.Patches) {
+			t.Errorf("Missing patch: %#v; raw: %s", want, string(want.GetPatch()))
+			continue
+		}
+
+		got := actions.Patches[i]
+		if got.GetName() != want.GetName() {
+			t.Errorf("Unexpected patch[%d]: %#v", i, got)
+		}
+		if (!r.SkipNamespaceValidation && got.GetNamespace() != expectedNamespace) &&
+			(!r.SkipNamespaceValidation && got.GetResource().GroupResource().Resource != "namespaces" &&
+				got.GetName() != expectedNamespace) {
+			t.Errorf("Unexpected patch[%d]: %#v", i, got)
+		}
+		if got, want := string(got.GetPatch()), string(want.GetPatch()); got != want {
+			t.Errorf("Unexpected patch(-want, +got):\n%s", cmp.Diff(want, got))
+		}
+	}
+	if got, want := len(actions.Patches), len(r.WantPatches); got > want {
+		for _, extra := range actions.Patches[want:] {
+			t.Errorf("Extra patch: %#v; raw: %s", extra, string(extra.GetPatch()))
+		}
+	}
+
+	gotEvents := eventList.Events()
+	for i, want := range r.WantEvents {
+		if i >= len(gotEvents) {
+			t.Error("Missing event:", want)
+			continue
+		}
+
+		if !cmp.Equal(want, gotEvents[i]) {
+			t.Errorf("Unexpected event(-want, +got):\n%s", cmp.Diff(want, gotEvents[i]))
+		}
+	}
+	if got, want := len(gotEvents), len(r.WantEvents); got > want {
+		for _, extra := range gotEvents[want:] {
+			t.Error("Extra event:", extra)
+		}
+	}
+
+	for _, verify := range r.PostConditions {
+		verify(t, r)
+	}
+}
+
+func filterUpdatesWithSubresource(
+	subresource string,
+	actions []clientgotesting.UpdateAction) (result []clientgotesting.UpdateAction) {
+	for _, action := range actions {
+		if action.GetSubresource() == subresource {
+			result = append(result, action)
+		}
+	}
+	return
+}
+
+// TableTest represents a list of TableRow tests instances.
+type TableTest []TableRow
+
+// Test executes the whole suite of the table tests.
+func (tt TableTest) Test(t *testing.T, factory Factory) {
+	t.Helper()
+	for _, test := range tt {
+		// Record the original objects in table.
+		originObjects := make([]runtime.Object, len(test.Objects))
+		for i, obj := range test.Objects {
+			originObjects[i] = obj.DeepCopyObject()
+		}
+		t.Run(test.Name, func(t *testing.T) {
+			t.Helper()
+			test.Test(t, factory)
+			// Validate cached objects do not get soiled after controller loops.
+			if !cmp.Equal(originObjects, test.Objects, defaultCmpOpts...) {
+				t.Errorf("Unexpected objects (-want, +got):\n%s",
+					cmp.Diff(originObjects, test.Objects, defaultCmpOpts...))
+			}
+		})
+	}
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/tracker.go b/vendor/knative.dev/pkg/reconciler/testing/tracker.go
new file mode 100644
index 000000000..35a15f896
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/tracker.go
@@ -0,0 +1,109 @@
+/*
+Copyright 2019 The Knative 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 testing
+
+import (
+	"sync"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"knative.dev/pkg/kmeta"
+	"knative.dev/pkg/tracker"
+)
+
+// NullTracker implements Tracker
+//
+// Alias is preserved for backwards compatibility
+type NullTracker = FakeTracker
+
+// FakeTracker implements Tracker.
+type FakeTracker struct {
+	sync.Mutex
+	references map[tracker.Reference]map[types.NamespacedName]struct{}
+}
+
+var _ tracker.Interface = (*FakeTracker)(nil)
+
+// OnChanged implements OnChanged.
+func (*FakeTracker) OnChanged(interface{}) {}
+
+// OnDeletedObserver implements OnDeletedObserver.
+func (n *FakeTracker) OnDeletedObserver(obj interface{}) {
+	item, err := kmeta.DeletionHandlingAccessor(obj)
+	if err != nil {
+		return
+	}
+	key := types.NamespacedName{Namespace: item.GetNamespace(), Name: item.GetName()}
+
+	n.Lock()
+	defer n.Unlock()
+
+	for ref, objs := range n.references {
+		delete(objs, key)
+		if len(objs) == 0 {
+			delete(n.references, ref)
+		}
+	}
+}
+
+// Track implements tracker.Interface.
+func (n *FakeTracker) Track(ref corev1.ObjectReference, obj interface{}) error {
+	return n.TrackReference(tracker.Reference{
+		APIVersion: ref.APIVersion,
+		Kind:       ref.Kind,
+		Namespace:  ref.Namespace,
+		Name:       ref.Name,
+	}, obj)
+}
+
+// TrackReference implements tracker.Interface.
+func (n *FakeTracker) TrackReference(ref tracker.Reference, obj interface{}) error {
+	item, err := kmeta.DeletionHandlingAccessor(obj)
+	if err != nil {
+		return err
+	}
+	key := types.NamespacedName{Namespace: item.GetNamespace(), Name: item.GetName()}
+
+	n.Lock()
+	defer n.Unlock()
+
+	if n.references == nil {
+		n.references = make(map[tracker.Reference]map[types.NamespacedName]struct{}, 1)
+	}
+
+	objs := n.references[ref]
+	if objs == nil {
+		objs = make(map[types.NamespacedName]struct{}, 1)
+	}
+	objs[key] = struct{}{}
+	n.references[ref] = objs
+
+	return nil
+}
+
+// References returns the list of objects being tracked
+func (n *FakeTracker) References() []tracker.Reference {
+	n.Lock()
+	defer n.Unlock()
+
+	refs := make([]tracker.Reference, 0, len(n.references))
+	for ref := range n.references {
+		refs = append(refs, ref)
+	}
+
+	return refs
+}
diff --git a/vendor/knative.dev/pkg/reconciler/testing/util.go b/vendor/knative.dev/pkg/reconciler/testing/util.go
new file mode 100644
index 000000000..14689693f
--- /dev/null
+++ b/vendor/knative.dev/pkg/reconciler/testing/util.go
@@ -0,0 +1,85 @@
+/*
+Copyright 2019 The Knative 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 testing includes utilities for testing controllers.
+package testing
+
+import (
+	"regexp"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/tools/cache"
+)
+
+// KeyOrDie returns the string key of the Kubernetes object or panics if a key
+// cannot be generated.
+func KeyOrDie(obj interface{}) string {
+	key, err := cache.MetaNamespaceKeyFunc(obj)
+	if err != nil {
+		panic(err)
+	}
+	return key
+}
+
+// ExpectNormalEventDelivery returns a hook function that can be passed to a
+// Hooks.OnCreate() call to verify that an event of type Normal was created
+// matching the given regular expression. For this expectation to be effective
+// the test must also call Hooks.WaitForHooks().
+func ExpectNormalEventDelivery(t *testing.T, messageRegexp string) CreateHookFunc {
+	t.Helper()
+	wantRegexp, err := regexp.Compile(messageRegexp)
+	if err != nil {
+		t.Fatalf("Invalid regular expression: %v", err)
+	}
+	return func(obj runtime.Object) HookResult {
+		t.Helper()
+		event := obj.(*corev1.Event)
+		if !wantRegexp.MatchString(event.Message) {
+			return HookIncomplete
+		}
+		t.Logf("Got an event message matching %q: %q", wantRegexp, event.Message)
+		if got, want := event.Type, corev1.EventTypeNormal; got != want {
+			t.Errorf("unexpected event Type: %q expected: %q", got, want)
+		}
+		return HookComplete
+	}
+}
+
+// ExpectWarningEventDelivery returns a hook function that can be passed to a
+// Hooks.OnCreate() call to verify that an event of type Warning was created
+// matching the given regular expression. For this expectation to be effective
+// the test must also call Hooks.WaitForHooks().
+func ExpectWarningEventDelivery(t *testing.T, messageRegexp string) CreateHookFunc {
+	t.Helper()
+	wantRegexp, err := regexp.Compile(messageRegexp)
+	if err != nil {
+		t.Fatalf("Invalid regular expression: %v", err)
+	}
+	return func(obj runtime.Object) HookResult {
+		t.Helper()
+		event := obj.(*corev1.Event)
+		if !wantRegexp.MatchString(event.Message) {
+			return HookIncomplete
+		}
+		t.Logf("Got an event message matching %q: %q", wantRegexp, event.Message)
+		if got, want := event.Type, corev1.EventTypeWarning; got != want {
+			t.Errorf("unexpected event Type: %q expected: %q", got, want)
+		}
+		return HookComplete
+	}
+}
diff --git a/vendor/knative.dev/pkg/system/testing/names.go b/vendor/knative.dev/pkg/system/testing/names.go
new file mode 100644
index 000000000..2e7fae22c
--- /dev/null
+++ b/vendor/knative.dev/pkg/system/testing/names.go
@@ -0,0 +1,30 @@
+/*
+Copyright 2019 The Knative 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
+
+    https://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 testing
+
+import (
+	"os"
+
+	"knative.dev/pkg/system"
+)
+
+func init() {
+	if ns := os.Getenv(system.NamespaceEnvKey); ns != "" {
+		return
+	}
+	os.Setenv(system.NamespaceEnvKey, "knative-testing")
+}