Repository: tektoncd/results
Pull request #295 (2022-12-23 13:49:35)

Allow users to set a label selector to filter objects to be deleted
This pull request resolves https://github.com/tektoncd/results/issues/286.

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

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

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

- **[OWNERS](https://github.com/tektoncd/results/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":["adambkaplan"]} -->
dibyom: Nice!
/approve
tekton-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/tektoncd/results/pull/295#issuecomment-1371768712" title="Approved">dibyom</a>*

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

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/results/blob/main/OWNERS)~~ [dibyom]

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":[]} -->
dibyom: @adambkaplan - one thing I just realized, we don't really have a PR template for this repo unlike others. I think we should add some. For this PR for instance, it would be useful to have a Release note
adambkaplan: @dibyom :+1: on having a PR template and checks for release notes. I think for v0.5.0 we may have to hand-craft release notes.
tekton-robot: @alan-ghelardi: PR needs rebase.

<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.
</details>

Patch:
diff --git a/cmd/api/README.md b/cmd/api/README.md
index ab845c97..2c6d2ea0 100644
--- a/cmd/api/README.md
+++ b/cmd/api/README.md
@@ -2,21 +2,22 @@
 
 ## Variables
 
-| Environment Variable  | Description                                                                                                              | Example                                      |
-|-----------------------|--------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|
-| DB_USER               | Postgres Database user                                                                                                   | user                                         |
-| DB_PASSWORD           | Postgres Database Password                                                                                               | hunter2                                      |
-| DB_HOST               | Postgres Database host                                                                                                   | /cloudsql/my-project:us-east1:tekton-results |
-| DB_NAME               | Postgres Database name                                                                                                   | tekton_results                               |
-| DB_SSLMODE            | Database SSL mode                                                                                                        | verify-full                                  |
-| GRPC_PORT             | gRPC Server Port                                                                                                         | 50051 (default)                              |
-| REST_PORT             | REST proxy Port                                                                                                          | 8080  (default)                              |
-| PROMETHEUS_PORT       | Prometheus Port                                                                                                          | 9090  (default)                              |
-| TLS_HOSTNAME_OVERRIDE | Override the hostname used to serve TLS. This should not be set (or set to the empty string) in production environments. | results.tekton.dev                           |
-| TLS_PATH              | Path to TLS files                                                                                                        | /etc/tls                                     |
-| LOG_LEVEL             | Log level for api server                                                                                                 | INFO                                         |
-| LOGS_API              | Enable logs storage service                                                                                              | false (default)                              |
-| NO_AUTH               | Disable the Kubernetes RBAC authorization check (allow all incoming requests with no restrictions)                       | false (default)                              |
+| Environment Variable     | Description                                                                                                                       | Example                                      |
+|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|
+| DB_USER                  | Postgres Database user                                                                                                            | user                                         |
+| DB_PASSWORD              | Postgres Database Password                                                                                                        | hunter2                                      |
+| DB_HOST                  | Postgres Database host                                                                                                            | /cloudsql/my-project:us-east1:tekton-results |
+| DB_NAME                  | Postgres Database name                                                                                                            | tekton_results                               |
+| DB_SSLMODE               | Database SSL mode                                                                                                                 | verify-full                                  |
+| DB_ENABLE_AUTO_MIGRATION | Auto-migrate the database on startup (create/update schemas). For further details, refer to <https://gorm.io/docs/migration.html> | true (default)                               |
+| GRPC_PORT                | gRPC Server Port                                                                                                                  | 50051 (default)                              |
+| REST_PORT                | REST proxy Port                                                                                                                   | 8080  (default)                              |
+| PROMETHEUS_PORT          | Prometheus Port                                                                                                                   | 9090  (default)                              |
+| TLS_HOSTNAME_OVERRIDE    | Override the hostname used to serve TLS. This should not be set (or set to the empty string) in production environments.          | results.tekton.dev                           |
+| TLS_PATH                 | Path to TLS files                                                                                                                 | /etc/tls                                     |
+| LOG_LEVEL                | Log level for api server                                                                                                          | INFO                                         |
+| LOGS_API                 | Enable logs storage service                                                                                                       | false (default)                              |
+| NO_AUTH                  | Disable the Kubernetes RBAC authorization check (allow all incoming requests with no restrictions)                                | false (default)                              |
 
 These values can also be set in the config file located in the `config/env/config` directory.
 
diff --git a/cmd/api/main.go b/cmd/api/main.go
index 1796c2fa..d23abe75 100644
--- a/cmd/api/main.go
+++ b/cmd/api/main.go
@@ -49,20 +49,21 @@ import (
 )
 
 type ConfigFile struct {
-	DB_USER               string `mapstructure:"DB_USER"`
-	DB_PASSWORD           string `mapstructure:"DB_PASSWORD"`
-	DB_HOST               string `mapstructure:"DB_HOST"`
-	DB_PORT               string `mapstructure:"DB_PORT"`
-	DB_NAME               string `mapstructure:"DB_NAME"`
-	DB_SSLMODE            string `mapstructure:"DB_SSLMODE"`
-	GRPC_PORT             string `mapstructure:"GRPC_PORT"`
-	REST_PORT             string `mapstructure:"REST_PORT"`
-	PROMETHEUS_PORT       string `mapstructure:"PROMETHEUS_PORT"`
-	LOG_LEVEL             string `mapstructure:"LOG_LEVEL"`
-	TLS_HOSTNAME_OVERRIDE string `mapstructure:"TLS_HOSTNAME_OVERRIDE"`
-	TLS_PATH              string `mapstructure:"TLS_PATH"`
-	LOGS_API              bool   `mapstructure:"LOGS_API"`
-	NO_AUTH               bool   `mapstructure:"NO_AUTH"`
+	DB_USER                  string `mapstructure:"DB_USER"`
+	DB_PASSWORD              string `mapstructure:"DB_PASSWORD"`
+	DB_HOST                  string `mapstructure:"DB_HOST"`
+	DB_PORT                  string `mapstructure:"DB_PORT"`
+	DB_NAME                  string `mapstructure:"DB_NAME"`
+	DB_SSLMODE               string `mapstructure:"DB_SSLMODE"`
+	DB_ENABLE_AUTO_MIGRATION bool   `mapstructure:"DB_ENABLE_AUTO_MIGRATION"`
+	GRPC_PORT                string `mapstructure:"GRPC_PORT"`
+	REST_PORT                string `mapstructure:"REST_PORT"`
+	PROMETHEUS_PORT          string `mapstructure:"PROMETHEUS_PORT"`
+	LOG_LEVEL                string `mapstructure:"LOG_LEVEL"`
+	TLS_HOSTNAME_OVERRIDE    string `mapstructure:"TLS_HOSTNAME_OVERRIDE"`
+	TLS_PATH                 string `mapstructure:"TLS_PATH"`
+	LOGS_API                 bool   `mapstructure:"LOGS_API"`
+	NO_AUTH                  bool   `mapstructure:"NO_AUTH"`
 }
 
 func main() {
@@ -136,7 +137,7 @@ func main() {
 	}
 
 	// Register API server(s)
-	v1a2, err := v1alpha2.New(db, v1alpha2.WithAuth(checker))
+	v1a2, err := v1alpha2.New(db, v1alpha2.WithAuth(checker), v1alpha2.WithDatabaseAutoMigration(configFile.DB_ENABLE_AUTO_MIGRATION))
 	if err != nil {
 		log.Fatalf("Failed to create server: %v", err)
 	}
diff --git a/cmd/watcher/main.go b/cmd/watcher/main.go
index 36f74bb5..2e4fd3e3 100644
--- a/cmd/watcher/main.go
+++ b/cmd/watcher/main.go
@@ -21,12 +21,13 @@ import (
 	"crypto/x509"
 	"flag"
 	"fmt"
-	"github.com/tektoncd/results/pkg/watcher/logs"
 	"io"
 	"log"
 	"os"
 	"time"
 
+	"github.com/tektoncd/results/pkg/watcher/logs"
+
 	creds "github.com/tektoncd/results/pkg/watcher/grpc"
 	"github.com/tektoncd/results/pkg/watcher/reconciler"
 	"github.com/tektoncd/results/pkg/watcher/reconciler/pipelinerun"
@@ -60,6 +61,8 @@ var (
 	completedRunGracePeriod = flag.Duration("completed_run_grace_period", 0, "Grace period duration before Runs should be deleted. If 0, Runs will not be deleted. If < 0, Runs will be deleted immediately.")
 	threadiness             = flag.Int("threadiness", controller.DefaultThreadsPerController, "Number of threads (Go routines) allocated to each controller")
 	logsAPI                 = flag.Bool("logs_api", true, "Disable sending logs. If not set, the logs will be sent only if server support API for it")
+	labelSelector           = flag.String("label_selector", "", "Selector (label query) to filter objects to be deleted. Matching objects must satisfy all labels requirements to be eligible for deletion")
+	requeueInterval         = flag.Duration("requeue_interval", 10*time.Minute, "How long the Watcher waits to reprocess keys on certain events (e.g. an object doesn't match the provided selectors)")
 )
 
 func main() {
@@ -90,6 +93,13 @@ func main() {
 	cfg := &reconciler.Config{
 		DisableAnnotationUpdate:      *disableCRDUpdate,
 		CompletedResourceGracePeriod: *completedRunGracePeriod,
+		RequeueInterval:              *requeueInterval,
+	}
+
+	if selector := *labelSelector; selector != "" {
+		if err := cfg.SetLabelSelector(selector); err != nil {
+			log.Fatalf("Malformed -label_selector value: %v", err)
+		}
 	}
 
 	sharedmain.MainWithContext(injection.WithNamespaceScope(ctx, corev1.NamespaceAll), "watcher",
diff --git a/config/100-sql-init.yaml b/config/100-sql-init.yaml
deleted file mode 100644
index 6ba5a1f6..00000000
--- a/config/100-sql-init.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-# sql-initdb-config contains the initialization config for the database.
-# NOTE: Do not modify the contents in results.sql manually - this is generated
-# from ../schema/results.sql via `go generate` to ensure that we are using the
-# same schema for deployments and tests.
-# This config should be mounted to /docker-entrypoint-initdb.d to initialize a
-# database on first startup.
-# See "Initialization Scripts" under https://hub.docker.com/_/postgres for
-# more info.
-apiVersion: v1
-data:
-  results.sql: "-- Copyright 2020 The Tekton Authors\n--\n-- Licensed under the Apache
-    License, Version 2.0 (the \"License\");\n-- you may not use this file except in
-    compliance with the License.\n-- You may obtain a copy of the License at\n--\n--
-    \     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable
-    law or agreed to in writing, software\n-- distributed under the License is distributed
-    on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
-    express or implied.\n-- See the License for the specific language governing permissions
-    and\n-- limitations under the License.\n\nCREATE TABLE results (\n\tparent varchar(64),\n\tid
-    varchar(64),\n\n\tname varchar(64),\n\tannotations jsonb,\n\n\tcreated_time timestamp
-    default current_timestamp not null,\n\tupdated_time timestamp default current_timestamp
-    not null,\n\t\n\tetag varchar(128),\n\n\tPRIMARY KEY(parent, id)\n);\nCREATE UNIQUE
-    INDEX results_by_name ON results(parent, name);\n\nCREATE TABLE records (\n\tparent
-    varchar(64),\n\tresult_id varchar(64),\n\tid varchar(64),\n\n\tresult_name varchar(64),\n\tname
-    varchar(64),\n\n\ttype varchar(128),\n\tdata jsonb,\n\n\tcreated_time timestamp
-    default current_timestamp not null,\n\tupdated_time timestamp default current_timestamp
-    not null,\n\n\tetag varchar(128),\n\n\tPRIMARY KEY(parent, result_id, id),\n\tFOREIGN
-    KEY(parent, result_id) REFERENCES results(parent, id) ON DELETE CASCADE\n);\nCREATE
-    UNIQUE INDEX records_by_name ON records(parent, result_name, name);\n"
-kind: ConfigMap
-metadata:
-  creationTimestamp: null
-  name: sql-initdb-config
-  namespace: tekton-pipelines
diff --git a/config/201-sql-deployment.yaml b/config/201-sql-deployment.yaml
index 2b52653b..c197f406 100644
--- a/config/201-sql-deployment.yaml
+++ b/config/201-sql-deployment.yaml
@@ -54,8 +54,6 @@ spec:
         volumeMounts:
         - name: postgredb
           mountPath: /bitnami/postgresql
-        - name: sql-initdb
-          mountPath: /docker-entrypoint-initdb.d
         securityContext:
           seccompProfile:
             type: RuntimeDefault
@@ -66,10 +64,6 @@ spec:
               - ALL
             add:
               - NET_BIND_SERVICE
-      volumes:
-        - name: sql-initdb
-          configMap:
-            name: sql-initdb-config
   volumeClaimTemplates:
   - metadata:
       name: postgredb
diff --git a/config/env/config b/config/env/config
index 3b039d2c..7641b83c 100644
--- a/config/env/config
+++ b/config/env/config
@@ -4,6 +4,7 @@ DB_HOST=
 DB_PORT=5432
 DB_NAME=
 DB_SSLMODE=disable
+DB_ENABLE_AUTO_MIGRATION=true
 GRPC_PORT=50051
 REST_PORT=8080
 PROMETHEUS_PORT=9090
diff --git a/config/generate.go b/config/generate.go
deleted file mode 100644
index 493a4957..00000000
--- a/config/generate.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2021 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.
-
-//go:generate bash -c "kubectl create configmap sql-initdb-config --from-file=\"../schema/results.sql\" --namespace=\"tekton-pipelines\" --dry-run -o yaml > 100-sql-init.yaml"
-
-package config
diff --git a/config/kustomization.yaml b/config/kustomization.yaml
index 57e3fa3f..319394da 100644
--- a/config/kustomization.yaml
+++ b/config/kustomization.yaml
@@ -14,7 +14,6 @@
 
 resources:
 - 100-api-serviceaccount.yaml
-- 100-sql-init.yaml
 - 100-watcher-serviceaccount.yaml
 - 201-sql-deployment.yaml
 - api.yaml
diff --git a/pkg/api/server/db/model.go b/pkg/api/server/db/model.go
index 96bb59f7..2a5240c6 100644
--- a/pkg/api/server/db/model.go
+++ b/pkg/api/server/db/model.go
@@ -25,27 +25,29 @@ import (
 
 // Result is the database model of a Result.
 type Result struct {
-	Parent      string `gorm:"primaryKey;index:results_by_name,priority:1"`
-	ID          string `gorm:"primaryKey"`
-	Name        string `gorm:"index:results_by_name,priority:2"`
-	Annotations Annotations
+	Parent      string      `gorm:"primaryKey;uniqueIndex:results_by_name,priority:1;size:64;"`
+	ID          string      `gorm:"primaryKey;size:64;"`
+	Name        string      `gorm:"uniqueIndex:results_by_name,priority:2;size:64;"`
+	Annotations Annotations `gorm:"type:jsonb;"`
 
-	CreatedTime time.Time
-	UpdatedTime time.Time
+	CreatedTime time.Time `gorm:"default:current_timestamp;"`
+	UpdatedTime time.Time `gorm:"default:current_timestamp;"`
 
-	Summary RecordSummary `gorm:"embedded;embeddedPrefix:recordsummary_"`
+	Summary RecordSummary `gorm:"embedded;embeddedPrefix:recordsummary_;"`
 
-	Etag string
+	Etag string `gorm:"size:128;"`
 }
 
 // RecordSummary is the database model of a Result.RecordSummary.
 type RecordSummary struct {
-	Record      string
-	Type        string
+	Record string `gorm:"size:256;"`
+	// Napkin Math (with a bit of buffer): 256 (DNS Subdomain) * 3 (Group +
+	// Version + Kind).
+	Type        string `gorm:"size:768;"`
 	StartTime   *time.Time
 	EndTime     *time.Time
 	Status      int32
-	Annotations Annotations
+	Annotations Annotations `gorm:"type:jsonb;"`
 }
 
 func (r Result) String() string {
@@ -58,20 +60,22 @@ type Record struct {
 	// table. Data will not be returned here during reads. Use the foreign key
 	// fields instead.
 	Result     Result `gorm:"foreignKey:Parent,ResultID;references:Parent,ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
-	Parent     string `gorm:"primaryKey;index:records_by_name,priority:1"`
-	ResultID   string `gorm:"primaryKey"`
-	ResultName string `gorm:"index:records_by_name,priority:2"`
+	Parent     string `gorm:"primaryKey;uniqueIndex:records_by_name,priority:1;size:64;"`
+	ResultID   string `gorm:"primaryKey;size:64;"`
+	ResultName string `gorm:"uniqueIndex:records_by_name,priority:2;size:64;"`
 
-	ID   string `gorm:"primaryKey"`
-	Name string `gorm:"index:records_by_name,priority:3"`
+	ID   string `gorm:"primaryKey;size:64;"`
+	Name string `gorm:"index:records_by_name,priority:3;size:64;"`
 
-	Type string
-	Data []byte
+	// Napkin Math (with a bit of buffer): 256 (DNS Subdomain) * 3 (Group +
+	// Version + Kind).
+	Type string `gorm:"size:768;"`
+	Data []byte `gorm:"type:jsonb;"`
 
-	CreatedTime time.Time
-	UpdatedTime time.Time
+	CreatedTime time.Time `gorm:"default:current_timestamp;"`
+	UpdatedTime time.Time `gorm:"default:current_timestamp;"`
 
-	Etag string
+	Etag string `gorm:"size:128;"`
 }
 
 // Annotations is a custom-defined type of a gorm model field.
diff --git a/pkg/api/server/v1alpha2/records.go b/pkg/api/server/v1alpha2/records.go
index f136c0aa..19b8bba0 100644
--- a/pkg/api/server/v1alpha2/records.go
+++ b/pkg/api/server/v1alpha2/records.go
@@ -123,9 +123,14 @@ func (s *Server) GetRecord(ctx context.Context, req *pb.GetRecordRequest) (*pb.R
 }
 
 func getRecord(txn *gorm.DB, parent, result, name string) (*db.Record, error) {
+	// Note: set the Parent, ResultName and Name fields in the model used to
+	// query the database to take advantage of the records_by_name composite
+	// index. Although the Name is an unique value as well, leveraging the
+	// index speeds up the query significantly. See
+	// https://github.com/tektoncd/results/issues/336.
 	store := &db.Record{}
 	q := txn.
-		Where(&db.Record{Result: db.Result{Parent: parent, Name: result}, Name: name}).
+		Where(&db.Record{Parent: parent, ResultName: result, Name: name}).
 		First(store)
 	if err := errors.Wrap(q.Error); err != nil {
 		return nil, err
diff --git a/pkg/api/server/v1alpha2/server.go b/pkg/api/server/v1alpha2/server.go
index 02ea85f8..a29995e3 100644
--- a/pkg/api/server/v1alpha2/server.go
+++ b/pkg/api/server/v1alpha2/server.go
@@ -46,6 +46,10 @@ type Server struct {
 	db   *gorm.DB
 	auth auth.Checker
 
+	// enableDatabaseAutoMigration controls whether the API server will
+	// auto-migrate the database upon startup.
+	enableDatabaseAutoMigration bool
+
 	// Converts result names -> IDs configurable to allow overrides for
 	// testing.
 	getResultID getResultID
@@ -53,20 +57,24 @@ type Server struct {
 
 // New set up environment for the api server
 func New(db *gorm.DB, opts ...Option) (*Server, error) {
-	if err := db.AutoMigrate(&model.Result{}, &model.Record{}); err != nil {
-		return nil, fmt.Errorf("error automigrating DB: %w", err)
-	}
 	env, err := resultscel.NewEnv()
 	if err != nil {
 		return nil, fmt.Errorf("failed to create CEL environment: %w", err)
 	}
+
 	srv := &Server{
 		db:  db,
 		env: env,
-
 		// Default open auth for easier testing.
 		auth: auth.AllowAll{},
+		// By default always create/update tables on startup. Note: Gorm
+		// doesn't attempt to delete unused tables or fields. For
+		// further details, refer to
+		// https://gorm.io/docs/migration.html. Users may disable this
+		// option via configuration.
+		enableDatabaseAutoMigration: true,
 	}
+
 	// Set default impls of overridable behavior
 	srv.getResultID = srv.getResultIDImpl
 
@@ -74,6 +82,12 @@ func New(db *gorm.DB, opts ...Option) (*Server, error) {
 		o(srv)
 	}
 
+	if srv.enableDatabaseAutoMigration {
+		if err := db.AutoMigrate(&model.Result{}, &model.Record{}); err != nil {
+			return nil, fmt.Errorf("error automigrating DB: %w", err)
+		}
+	}
+
 	return srv, nil
 }
 
@@ -85,6 +99,14 @@ func WithAuth(c auth.Checker) Option {
 	}
 }
 
+// WithDatabaseAutoMigration allows callers to enable or disable the database
+// auto-migration upon startup.
+func WithDatabaseAutoMigration(value bool) Option {
+	return func(server *Server) {
+		server.enableDatabaseAutoMigration = value
+	}
+}
+
 func withGetResultID(f getResultID) Option {
 	return func(s *Server) {
 		s.getResultID = f
diff --git a/pkg/watcher/reconciler/config.go b/pkg/watcher/reconciler/config.go
index 9d802a72..af5ab821 100644
--- a/pkg/watcher/reconciler/config.go
+++ b/pkg/watcher/reconciler/config.go
@@ -14,7 +14,11 @@
 
 package reconciler
 
-import "time"
+import (
+	"time"
+
+	"k8s.io/apimachinery/pkg/labels"
+)
 
 // Config defines shared reconciler configuration options.
 type Config struct {
@@ -25,6 +29,15 @@ type Config struct {
 	// CompletedResourceGracePeriod is the time to wait before deleting completed resources.
 	// 0 implies the duration
 	CompletedResourceGracePeriod time.Duration
+
+	// Label selector to match resources against in order to determine
+	// whether completed resources are eligible for deletion. The default
+	// value is labels.Everything() which matches any resource.
+	labelSelector labels.Selector
+
+	// How long the controller waits to reprocess keys on certain events
+	// (e.g. an object doesn't match the provided label selectors).
+	RequeueInterval time.Duration
 }
 
 // GetDisableAnnotationupdate returns whether annotation updates should be
@@ -48,3 +61,26 @@ func (c *Config) GetCompletedResourceGracePeriod() time.Duration {
 	}
 	return c.CompletedResourceGracePeriod
 }
+
+// GetLabelSelector returns the label selector to match resources against in
+// order to determine whether they're eligible for deletion. If no selector was
+// configured via the SetLabelSelector method, returns a selector that always
+// matches any resource.
+func (c *Config) GetLabelSelector() labels.Selector {
+	if c.labelSelector == nil {
+		return labels.Everything()
+	}
+	return c.labelSelector
+}
+
+// SetLabelSelector sets a label selector to match resources against in order to
+// determine whether they're eligible for deletion. The syntax obeys the same
+// format accepted by list operations peformed on the Kubernetes API server.
+func (c *Config) SetLabelSelector(selector string) error {
+	parsedSelector, err := labels.Parse(selector)
+	if err != nil {
+		return err
+	}
+	c.labelSelector = parsedSelector
+	return nil
+}
diff --git a/pkg/watcher/reconciler/dynamic/dynamic.go b/pkg/watcher/reconciler/dynamic/dynamic.go
index 978d2d24..f0dfb8b3 100644
--- a/pkg/watcher/reconciler/dynamic/dynamic.go
+++ b/pkg/watcher/reconciler/dynamic/dynamic.go
@@ -29,6 +29,7 @@ import (
 	"go.uber.org/zap"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/types"
 	"knative.dev/pkg/apis"
 	"knative.dev/pkg/controller"
@@ -122,6 +123,7 @@ func (r *Reconciler) addResultsAnnotations(ctx context.Context, o results.Object
 // than 0).
 // * The object is done and it isn't owned by other object.
 // * The configured grace period has elapsed since the object's completion.
+// * The object satisfies all label requirements defined in the supplied config.
 func (r *Reconciler) deleteUponCompletion(ctx context.Context, o results.Object) error {
 	logger := logging.FromContext(ctx)
 
@@ -160,6 +162,12 @@ func (r *Reconciler) deleteUponCompletion(ctx context.Context, o results.Object)
 		return controller.NewRequeueAfter(requeueAfter)
 	}
 
+	// Verify whether this object matches the provided label selectors
+	if selectors := r.cfg.GetLabelSelector(); !selectors.Matches(labels.Set(o.GetLabels())) {
+		logger.Debugw("Object doesn't match the required label selectors - requeuing to process later", zap.String("results.tekton.dev/label-selectors", selectors.String()))
+		return controller.NewRequeueAfter(r.cfg.RequeueInterval)
+	}
+
 	logger.Infow("Deleting object", zap.String("results.tekton.dev/uid", string(o.GetUID())),
 		zap.Int64("results.tekton.dev/time-taken-seconds", int64(time.Since(*completionTime).Seconds())))
 	if err := r.objectClient.Delete(ctx, o.GetName(), metav1.DeleteOptions{
diff --git a/schema/results.sql b/schema/results.sql
deleted file mode 100644
index 849e6051..00000000
--- a/schema/results.sql
+++ /dev/null
@@ -1,58 +0,0 @@
--- 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.
-
-CREATE TABLE results (
-	parent varchar(64),
-	id varchar(64),
-
-	name varchar(64),
-	annotations jsonb,
-
-	created_time timestamp default current_timestamp not null,
-	updated_time timestamp default current_timestamp not null,
-	
-	etag varchar(128),
-
-	recordsummary_record varchar(256),
-	recordsummary_type varchar(768),
-	recordsummary_start_time timestamp,
-	recordsummary_end_time timestamp,
-	recordsummary_status int,
-	recordsummary_annotations jsonb,
-
-	PRIMARY KEY(parent, id)
-);
-CREATE UNIQUE INDEX results_by_name ON results(parent, name);
-
-CREATE TABLE records (
-	parent varchar(64),
-	result_id varchar(64),
-	id varchar(64),
-
-	result_name varchar(64),
-	name varchar(64),
-
-	-- Napkin Math (with a bit of buffer): 256 (DNS Subdomain) * 3 (Group + Version + Kind)
-	type varchar(768),
-	data jsonb,
-
-	created_time timestamp default current_timestamp not null,
-	updated_time timestamp default current_timestamp not null,
-
-	etag varchar(128),
-
-	PRIMARY KEY(parent, result_id, id),
-	FOREIGN KEY(parent, result_id) REFERENCES results(parent, id) ON DELETE CASCADE
-);
-CREATE UNIQUE INDEX records_by_name ON records(parent, result_name, name);


Test patch:
diff --git a/pkg/api/server/test/db.go b/pkg/api/server/test/db.go
index 94109e91..72bd9dbe 100644
--- a/pkg/api/server/test/db.go
+++ b/pkg/api/server/test/db.go
@@ -15,12 +15,8 @@
 package test
 
 import (
-	"database/sql"
 	"io/ioutil"
 	"os"
-	"path"
-	"path/filepath"
-	"runtime"
 	"testing"
 
 	"gorm.io/driver/sqlite"
@@ -46,25 +42,7 @@ func NewDB(t *testing.T) *gorm.DB {
 		os.Remove(tmpfile.Name())
 	})
 
-	// Connect to sqlite DB manually to load in schema.
-	db, err := sql.Open("sqlite3", tmpfile.Name())
-	if err != nil {
-		t.Fatalf("failed to open the results.db: %v", err)
-	}
-	defer db.Close()
-
-	_, b, _, _ := runtime.Caller(0)
-	basepath := filepath.Dir(b)
-	schema, err := ioutil.ReadFile(path.Join(basepath, "../../../../schema/results.sql"))
-	if err != nil {
-		t.Fatalf("failed to read schema file: %v", err)
-	}
-	// Create result table using the checked in scheme to ensure compatibility.
-	if _, err := db.Exec(string(schema)); err != nil {
-		t.Fatalf("failed to execute the result table creation statement statement: %v", err)
-	}
-
-	// Reopen DB using gorm to use all the nice gorm tools.
+	// Open DB using gorm to use all the nice gorm tools.
 	gdb, err := gorm.Open(sqlite.Open(tmpfile.Name()), &gorm.Config{
 		// Configure verbose db logging to use testing logger.
 		// This will show all SQL statements made if the test fails.
@@ -72,8 +50,6 @@ func NewDB(t *testing.T) *gorm.DB {
 			LogLevel: logger.Info,
 			Colorful: true,
 		}),
-		// Constraints not implemented on sqlite
-		DisableForeignKeyConstraintWhenMigrating: true,
 	})
 	if err != nil {
 		t.Fatalf("failed to open the results.db: %v", err)
diff --git a/pkg/watcher/reconciler/dynamic/dynamic_test.go b/pkg/watcher/reconciler/dynamic/dynamic_test.go
index b5705290..69f41df8 100644
--- a/pkg/watcher/reconciler/dynamic/dynamic_test.go
+++ b/pkg/watcher/reconciler/dynamic/dynamic_test.go
@@ -124,6 +124,7 @@ func TestReconcile_TaskRun(t *testing.T) {
 
 	cfg := &reconciler.Config{
 		DisableAnnotationUpdate: true,
+		RequeueInterval:         1 * time.Second,
 	}
 
 	r := NewDynamicReconciler(results, trclient, cfg)
@@ -131,7 +132,10 @@ func TestReconcile_TaskRun(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	taskrun.Status.InitializeConditions()
+	isRequeueKey := func(err error) bool {
+		ok, _ := controller.IsRequeueKey(err)
+		return ok
+	}
 
 	t.Run("DisabledAnnotations", func(t *testing.T) {
 		resultName := result.FormatName(taskrun.GetNamespace(), string(taskrun.GetUID()))
@@ -171,11 +175,6 @@ func TestReconcile_TaskRun(t *testing.T) {
 		// Enable object deletion, re-reconcile
 		cfg.CompletedResourceGracePeriod = 1 * time.Second
 
-		isRequeueKey := func(err error) bool {
-			ok, _ := controller.IsRequeueKey(err)
-			return ok
-		}
-
 		// Simulate a successful TaskRun. The next test case will make
 		// sure that failed objects can be deleted as well.
 		taskrun.Status.SetCondition(&apis.Condition{
@@ -233,6 +232,36 @@ func TestReconcile_TaskRun(t *testing.T) {
 			t.Fatalf("Want NotFound, but got %v", err)
 		}
 	})
+
+	t.Run("wait until the object has the desired labels to delete it", func(t *testing.T) {
+		// Recreate the object to retest the deletion
+		if _, err := trclient.Create(ctx, taskrun, metav1.CreateOptions{}); err != nil {
+			t.Fatal(err)
+		}
+
+		if err := cfg.SetLabelSelector("x=foo"); err != nil {
+			t.Fatal(err)
+		}
+
+		// The controller must return a RequeueKeyError because the
+		// TaskRun doesn't have the expected labels
+		if err := r.Reconcile(ctx, taskrun); !isRequeueKey(err) {
+			t.Fatalf("Want a controller.RequeueKey error, but got %v", err)
+		}
+
+		// Set the expected label and reconcile.
+		taskrun.Labels = map[string]string{
+			"x": "foo",
+		}
+		if err := r.Reconcile(ctx, taskrun); err != nil {
+			t.Fatal(err)
+		}
+
+		// Make sure that the resource no longer exists
+		if _, err := trclient.Get(ctx, taskrun.GetName(), metav1.GetOptions{}); !apierrors.IsNotFound(err) {
+			t.Fatalf("Want NotFound, but got %v", err)
+		}
+	})
 }
 
 // This is a simpler test than TaskRun, since most of this behavior is
diff --git a/test/e2e/01-install.sh b/test/e2e/01-install.sh
index 14af1e2a..edc80254 100755
--- a/test/e2e/01-install.sh
+++ b/test/e2e/01-install.sh
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# shellcheck disable=SC2181 # To ignore long command exit code check
+
 set -e
 
 export KO_DOCKER_REPO=${KO_DOCKER_REPO:-"kind.local"}
@@ -24,11 +26,11 @@ ROOT="$(git rev-parse --show-toplevel)"
 
 echo "Installing Tekton Pipelines..."
 TEKTON_PIPELINE_CONFIG=${TEKTON_PIPELINE_CONFIG:-"https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml"}
-kubectl apply --filename ${TEKTON_PIPELINE_CONFIG}
+kubectl apply --filename "${TEKTON_PIPELINE_CONFIG}"
 
 echo "Generating DB secret..."
 # Don't fail if the secret isn't created - this can happen if the secret already exists.
-kubectl create secret generic tekton-results-postgres --namespace="tekton-pipelines" --from-literal=POSTGRES_USER=postgres --from-literal=POSTGRES_PASSWORD=$(openssl rand -base64 20) || true
+kubectl create secret generic tekton-results-postgres --namespace="tekton-pipelines" --from-literal=POSTGRES_USER=postgres --from-literal=POSTGRES_PASSWORD="$(openssl rand -base64 20)" || true
 
 echo "Generating TLS key pair..."
 set +e
@@ -66,13 +68,14 @@ set -e
 kubectl create secret tls -n tekton-pipelines tekton-results-tls --cert="${SSL_CERT_PATH}/tekton-results-cert.pem" --key="${SSL_CERT_PATH}/tekton-results-key.pem" || true
 
 echo "Installing Tekton Results..."
-kubectl kustomize "${ROOT}/test/e2e/kustomize" | ko apply -f -
+extra_ko_params="linux/$(go env GOARCH)"
+kubectl kustomize "${ROOT}/test/e2e/kustomize" | ko apply --platform="$extra_ko_params" -f -
 
 echo "Fetching access tokens..."
-mkdir -p ${SA_TOKEN_PATH}
+mkdir -p "${SA_TOKEN_PATH}"
 service_accounts=(all-namespaces-read-access single-namespace-read-access)
 for service_account in "${service_accounts[@]}"; do
-    kubectl create token $service_account > ${SA_TOKEN_PATH}/$service_account
+    kubectl create token "$service_account" > "${SA_TOKEN_PATH}"/"$service_account"
     echo "Created ${SA_TOKEN_PATH}/$service_account"
 done