Repository: knative/pkg
Pull request #1259 (2020-04-29 13:27:40)

Functions common to HA tests
Moving common code related to HA (high availability) tests from knative/serving that will be re-used in knative/net-kourier.

PR for knative/serving: https://github.com/knative/serving/pull/7777
PR for knative/net-kourier: https://github.com/knative/net-kourier/pull/55

knative-prow-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/1259#" title="Author self-approved">mgencur</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **chaodaig**
You can assign the PR to them by writing `/assign @chaodaig` 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=knative%2Fpkg).

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

- **[test/OWNERS](https://github.com/knative/pkg/blob/master/test/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":["chaodaig"]} -->
mgencur: /hold
knative-prow-robot: @mgencur: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-pkg-unit-tests | 2b6fcc17926da7e4d23a3f0e38444797f2e48c17 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_pkg/1259/pull-knative-pkg-unit-tests/1257926099535400961) | `/test pull-knative-pkg-unit-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/1259). [Your PR dashboard](https://gubernator.knative.dev/pr/mgencur).

<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 -->
knative-prow-robot: @mgencur: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-pkg-unit-tests | 2b6fcc17926da7e4d23a3f0e38444797f2e48c17 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_pkg/1259/pull-knative-pkg-unit-tests/1257926099535400961) | `/test pull-knative-pkg-unit-tests`
pull-knative-pkg-build-tests | 2b6fcc17926da7e4d23a3f0e38444797f2e48c17 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_pkg/1259/pull-knative-pkg-build-tests/1257926099539595265) | `/test pull-knative-pkg-build-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/1259). [Your PR dashboard](https://gubernator.knative.dev/pr/mgencur).

<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 -->
mgencur: /hold cancel
mgencur: @markusthoemmes would you mind giving it another review? The tests in related knative/serving and net-kourier PRs passed! Thanks

Patch:
diff --git a/apis/condition_set.go b/apis/condition_set.go
index 49f9d2e9b..8d02fec48 100644
--- a/apis/condition_set.go
+++ b/apis/condition_set.go
@@ -142,6 +142,11 @@ type conditionsImpl struct {
 	accessor ConditionsAccessor
 }
 
+// GetTopLevelConditionType is an accessor for the top-level happy condition.
+func (r ConditionSet) GetTopLevelConditionType() ConditionType {
+	return r.happy
+}
+
 // Manage creates a ConditionManager from an accessor object using the original
 // ConditionSet as a reference. Status must be a pointer to a struct.
 func (r ConditionSet) Manage(status ConditionsAccessor) ConditionManager {
diff --git a/apis/duck/v1/kresource_type.go b/apis/duck/v1/kresource_type.go
index b64977fcb..78d8ece66 100644
--- a/apis/duck/v1/kresource_type.go
+++ b/apis/duck/v1/kresource_type.go
@@ -22,19 +22,19 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 
 	"knative.dev/pkg/apis"
 )
 
-// KRShaped is an interface for retrieving the duck elements of an arbitraty resource.
+// KRShaped is an interface for retrieving the duck elements of an arbitrary resource.
 type KRShaped interface {
-	metav1.ObjectMetaAccessor
-
-	GetTypeMeta() *metav1.TypeMeta
+	metav1.Object
+	schema.ObjectKind
 
 	GetStatus() *Status
 
-	GetTopLevelConditionType() apis.ConditionType
+	GetConditionSet() apis.ConditionSet
 }
 
 // Asserts KResource conformance with KRShaped
@@ -81,22 +81,17 @@ type KResourceList struct {
 	Items []KResource `json:"items"`
 }
 
-// GetTypeMeta retrieves the ObjectMeta of the KResource. Implements the KRShaped interface.
-func (t *KResource) GetTypeMeta() *metav1.TypeMeta {
-	return &t.TypeMeta
-}
-
 // GetStatus retrieves the status of the KResource. Implements the KRShaped interface.
 func (t *KResource) GetStatus() *Status {
 	return &t.Status
 }
 
-// GetTopLevelConditionType retrieves the happy condition of this resource. Implements the KRShaped interface.
-func (t *KResource) GetTopLevelConditionType() apis.ConditionType {
+// GetConditionSet retrieves the condition set for this resource. Implements the KRShaped interface.
+func (t *KResource) GetConditionSet() apis.ConditionSet {
 	// Note: KResources are unmarshalled from existing resources. This will only work properly for resources that
 	// have already been initialized to their type.
 	if cond := t.Status.GetCondition(apis.ConditionSucceeded); cond != nil {
-		return apis.ConditionSucceeded
+		return apis.NewBatchConditionSet()
 	}
-	return apis.ConditionReady
+	return apis.NewLivingConditionSet()
 }
diff --git a/apis/volatile_time.go b/apis/volatile_time.go
index 3d2daa277..d735ada99 100644
--- a/apis/volatile_time.go
+++ b/apis/volatile_time.go
@@ -22,6 +22,12 @@ import (
 )
 
 // VolatileTime wraps metav1.Time
+//
+// Unlike metav1.Time, VolatileTimes are considered semantically equal when
+// using kubernetes semantic equality checks.
+// Thus differing VolatileTime values are not considered different.
+// Note, go-cmp will still return inequality, see unit test if you
+// need this behavior for go-cmp.
 type VolatileTime struct {
 	Inner metav1.Time
 }
@@ -39,7 +45,7 @@ func (t *VolatileTime) UnmarshalJSON(b []byte) error {
 func init() {
 	equality.Semantic.AddFunc(
 		// Always treat VolatileTime fields as equivalent.
-		func(a, b VolatileTime) bool {
+		func(VolatileTime, VolatileTime) bool {
 			return true
 		},
 	)
diff --git a/client/injection/apiextensions/reconciler/apiextensions/v1beta1/customresourcedefinition/reconciler.go b/client/injection/apiextensions/reconciler/apiextensions/v1beta1/customresourcedefinition/reconciler.go
index 5c870fc4d..d2c832ed1 100644
--- a/client/injection/apiextensions/reconciler/apiextensions/v1beta1/customresourcedefinition/reconciler.go
+++ b/client/injection/apiextensions/reconciler/apiextensions/v1beta1/customresourcedefinition/reconciler.go
@@ -20,18 +20,18 @@ package customresourcedefinition
 
 import (
 	context "context"
-	"encoding/json"
-	"reflect"
+	json "encoding/json"
+	reflect "reflect"
 
 	zap "go.uber.org/zap"
 	v1 "k8s.io/api/core/v1"
 	v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
 	clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
 	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1beta1"
-	"k8s.io/apimachinery/pkg/api/equality"
+	equality "k8s.io/apimachinery/pkg/api/equality"
 	errors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/types"
+	types "k8s.io/apimachinery/pkg/types"
 	sets "k8s.io/apimachinery/pkg/util/sets"
 	cache "k8s.io/client-go/tools/cache"
 	record "k8s.io/client-go/tools/record"
@@ -167,6 +167,7 @@ func (r *reconcilerImpl) Reconcile(ctx context.Context, key string) error {
 		// Reconcile this copy of the resource and then write back any status
 		// updates regardless of whether the reconciliation errored out.
 		reconcileEvent = r.reconciler.ReconcileKind(ctx, resource)
+
 	} else if fin, ok := r.reconciler.(Finalizer); ok {
 		// Append the target method to the logger.
 		logger = logger.With(zap.String("targetMethod", "FinalizeKind"))
@@ -198,13 +199,19 @@ func (r *reconcilerImpl) Reconcile(ctx context.Context, key string) error {
 		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
-		} else {
-			logger.Errorw("Returned an error", zap.Error(reconcileEvent))
-			r.Recorder.Event(resource, v1.EventTypeWarning, "InternalError", reconcileEvent.Error())
-			return reconcileEvent
 		}
+
+		logger.Errorw("Returned an error", zap.Error(reconcileEvent))
+		r.Recorder.Event(resource, v1.EventTypeWarning, "InternalError", reconcileEvent.Error())
+		return reconcileEvent
 	}
+
 	return nil
 }
 
diff --git a/client/injection/kube/reconciler/core/v1/namespace/reconciler.go b/client/injection/kube/reconciler/core/v1/namespace/reconciler.go
index 2893639c3..f040f5681 100644
--- a/client/injection/kube/reconciler/core/v1/namespace/reconciler.go
+++ b/client/injection/kube/reconciler/core/v1/namespace/reconciler.go
@@ -20,15 +20,15 @@ package namespace
 
 import (
 	context "context"
-	"encoding/json"
-	"reflect"
+	json "encoding/json"
+	reflect "reflect"
 
 	zap "go.uber.org/zap"
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/equality"
+	equality "k8s.io/apimachinery/pkg/api/equality"
 	errors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/types"
+	types "k8s.io/apimachinery/pkg/types"
 	sets "k8s.io/apimachinery/pkg/util/sets"
 	kubernetes "k8s.io/client-go/kubernetes"
 	corev1 "k8s.io/client-go/listers/core/v1"
@@ -166,6 +166,7 @@ func (r *reconcilerImpl) Reconcile(ctx context.Context, key string) error {
 		// Reconcile this copy of the resource and then write back any status
 		// updates regardless of whether the reconciliation errored out.
 		reconcileEvent = r.reconciler.ReconcileKind(ctx, resource)
+
 	} else if fin, ok := r.reconciler.(Finalizer); ok {
 		// Append the target method to the logger.
 		logger = logger.With(zap.String("targetMethod", "FinalizeKind"))
@@ -197,13 +198,19 @@ func (r *reconcilerImpl) Reconcile(ctx context.Context, key string) error {
 		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
-		} else {
-			logger.Errorw("Returned an error", zap.Error(reconcileEvent))
-			r.Recorder.Event(resource, v1.EventTypeWarning, "InternalError", reconcileEvent.Error())
-			return reconcileEvent
 		}
+
+		logger.Errorw("Returned an error", zap.Error(reconcileEvent))
+		r.Recorder.Event(resource, v1.EventTypeWarning, "InternalError", reconcileEvent.Error())
+		return reconcileEvent
 	}
+
 	return nil
 }
 
diff --git a/codegen/cmd/injection-gen/generators/packages.go b/codegen/cmd/injection-gen/generators/packages.go
index a53dc8b02..c54d6f915 100644
--- a/codegen/cmd/injection-gen/generators/packages.go
+++ b/codegen/cmd/injection-gen/generators/packages.go
@@ -173,17 +173,11 @@ func MustParseClientGenTags(lines []string) Tags {
 		Tags: util.MustParseClientGenTags(lines),
 	}
 
-	values := types.ExtractCommentTags("+", lines)
+	values := ExtractCommentTags("+", lines)
 
 	_, ret.GenerateDuck = values["genduck"]
 
-	_, genRec := values["genreconciler"]
-	_, genRecClass := values["genreconciler:class"]
-
-	// Generate Reconciler code if genreconciler OR genreconciler:class exist.
-	if genRec || genRecClass {
-		ret.GenerateReconciler = true
-	}
+	_, ret.GenerateReconciler = values["genreconciler"]
 
 	return ret
 }
@@ -202,6 +196,14 @@ func extractReconcilerClassTag(tags map[string]map[string]string) (string, bool)
 	return classname, has
 }
 
+func isKRShaped(tags map[string]map[string]string) bool {
+	vals, has := tags["genreconciler"]
+	if !has {
+		return false
+	}
+	return vals["krshapedlogic"] == "true"
+}
+
 func isNonNamespaced(tags map[string]map[string]string) bool {
 	vals, has := tags["genclient"]
 	if !has {
@@ -225,118 +227,108 @@ func typedInformerPackage(groupPkgName string, gv clientgentypes.GroupVersion, e
 func versionClientsPackages(basePackage string, boilerplate []byte, customArgs *informergenargs.CustomArgs) []generator.Package {
 	packagePath := filepath.Join(basePackage, "client")
 
-	vers := make([]generator.Package, 0, 2)
-
-	// Impl
-	vers = append(vers, &generator.DefaultPackage{
-		PackageName: "client",
-		PackagePath: packagePath,
-		HeaderText:  boilerplate,
-		GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
-			// Impl
-			generators = append(generators, &clientGenerator{
-				DefaultGen: generator.DefaultGen{
-					OptionalName: "client",
-				},
-				outputPackage:    packagePath,
-				imports:          generator.NewImportTracker(),
-				clientSetPackage: customArgs.VersionedClientSetPackage,
-			})
-
-			return generators
-		},
-		FilterFunc: func(c *generator.Context, t *types.Type) bool {
-			tags := MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
-			return tags.NeedsInformerInjection()
-		},
-	})
-
-	// Fake
-	vers = append(vers, &generator.DefaultPackage{
-		PackageName: "fake",
-		PackagePath: filepath.Join(packagePath, "fake"),
-		HeaderText:  boilerplate,
-		GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
-
-			// Impl
-			generators = append(generators, &fakeClientGenerator{
-				DefaultGen: generator.DefaultGen{
-					OptionalName: "fake",
-				},
-				outputPackage:      filepath.Join(packagePath, "fake"),
-				imports:            generator.NewImportTracker(),
-				fakeClientPkg:      filepath.Join(customArgs.VersionedClientSetPackage, "fake"),
-				clientInjectionPkg: packagePath,
-			})
-
-			return generators
+	vers := []generator.Package{
+		// Impl
+		&generator.DefaultPackage{
+			PackageName: "client",
+			PackagePath: packagePath,
+			HeaderText:  boilerplate,
+			GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
+				// Impl
+				generators = append(generators, &clientGenerator{
+					DefaultGen: generator.DefaultGen{
+						OptionalName: "client",
+					},
+					outputPackage:    packagePath,
+					imports:          generator.NewImportTracker(),
+					clientSetPackage: customArgs.VersionedClientSetPackage,
+				})
+				return generators
+			},
+			FilterFunc: func(c *generator.Context, t *types.Type) bool {
+				tags := MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
+				return tags.NeedsInformerInjection()
+			},
 		},
-		FilterFunc: func(c *generator.Context, t *types.Type) bool {
-			tags := MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
-			return tags.NeedsInformerInjection()
+		// Fake
+		&generator.DefaultPackage{
+			PackageName: "fake",
+			PackagePath: filepath.Join(packagePath, "fake"),
+			HeaderText:  boilerplate,
+			GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
+				// Impl
+				generators = append(generators, &fakeClientGenerator{
+					DefaultGen: generator.DefaultGen{
+						OptionalName: "fake",
+					},
+					outputPackage:      filepath.Join(packagePath, "fake"),
+					imports:            generator.NewImportTracker(),
+					fakeClientPkg:      filepath.Join(customArgs.VersionedClientSetPackage, "fake"),
+					clientInjectionPkg: packagePath,
+				})
+				return generators
+			},
+			FilterFunc: func(c *generator.Context, t *types.Type) bool {
+				tags := MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
+				return tags.NeedsInformerInjection()
+			},
 		},
-	})
-
+	}
 	return vers
 }
 
 func versionFactoryPackages(basePackage string, boilerplate []byte, customArgs *informergenargs.CustomArgs) []generator.Package {
 	packagePath := filepath.Join(basePackage, "informers", "factory")
 
-	vers := make([]generator.Package, 0, 2)
-
-	// Impl
-	vers = append(vers, &generator.DefaultPackage{
-		PackageName: "factory",
-		PackagePath: packagePath,
-		HeaderText:  boilerplate,
-		GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
-			// Impl
-			generators = append(generators, &factoryGenerator{
-				DefaultGen: generator.DefaultGen{
-					OptionalName: "factory",
-				},
-				outputPackage:                packagePath,
-				cachingClientSetPackage:      filepath.Join(basePackage, "client"),
-				sharedInformerFactoryPackage: customArgs.ExternalVersionsInformersPackage,
-				imports:                      generator.NewImportTracker(),
-			})
-
-			return generators
-		},
-		FilterFunc: func(c *generator.Context, t *types.Type) bool {
-			tags := MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
-			return tags.NeedsInformerInjection()
-		},
-	})
-
-	// Fake
-	vers = append(vers, &generator.DefaultPackage{
-		PackageName: "fake",
-		PackagePath: filepath.Join(packagePath, "fake"),
-		HeaderText:  boilerplate,
-		GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
-
-			// Impl
-			generators = append(generators, &fakeFactoryGenerator{
-				DefaultGen: generator.DefaultGen{
-					OptionalName: "fake",
-				},
-				outputPackage:                filepath.Join(packagePath, "fake"),
-				factoryInjectionPkg:          packagePath,
-				fakeClientInjectionPkg:       filepath.Join(basePackage, "client", "fake"),
-				sharedInformerFactoryPackage: customArgs.ExternalVersionsInformersPackage,
-				imports:                      generator.NewImportTracker(),
-			})
-
-			return generators
+	vers := []generator.Package{
+		// Impl
+		&generator.DefaultPackage{
+			PackageName: "factory",
+			PackagePath: packagePath,
+			HeaderText:  boilerplate,
+			GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
+				// Impl
+				generators = append(generators, &factoryGenerator{
+					DefaultGen: generator.DefaultGen{
+						OptionalName: "factory",
+					},
+					outputPackage:                packagePath,
+					cachingClientSetPackage:      filepath.Join(basePackage, "client"),
+					sharedInformerFactoryPackage: customArgs.ExternalVersionsInformersPackage,
+					imports:                      generator.NewImportTracker(),
+				})
+				return generators
+			},
+			FilterFunc: func(c *generator.Context, t *types.Type) bool {
+				tags := MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
+				return tags.NeedsInformerInjection()
+			},
 		},
-		FilterFunc: func(c *generator.Context, t *types.Type) bool {
-			tags := MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
-			return tags.NeedsInformerInjection()
+		// Fake
+		&generator.DefaultPackage{
+			PackageName: "fake",
+			PackagePath: filepath.Join(packagePath, "fake"),
+			HeaderText:  boilerplate,
+			GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
+				// Impl
+				generators = append(generators, &fakeFactoryGenerator{
+					DefaultGen: generator.DefaultGen{
+						OptionalName: "fake",
+					},
+					outputPackage:                filepath.Join(packagePath, "fake"),
+					factoryInjectionPkg:          packagePath,
+					fakeClientInjectionPkg:       filepath.Join(basePackage, "client", "fake"),
+					sharedInformerFactoryPackage: customArgs.ExternalVersionsInformersPackage,
+					imports:                      generator.NewImportTracker(),
+				})
+				return generators
+			},
+			FilterFunc: func(c *generator.Context, t *types.Type) bool {
+				tags := MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
+				return tags.NeedsInformerInjection()
+			},
 		},
-	})
-
+	}
 	return vers
 }
 
@@ -344,12 +336,11 @@ func versionInformerPackages(basePackage string, groupPkgName string, gv clientg
 	factoryPackagePath := filepath.Join(basePackage, "informers", "factory")
 	packagePath := filepath.Join(basePackage, "informers", groupPkgName, strings.ToLower(gv.Version.NonEmpty()))
 
-	vers := make([]generator.Package, 0, len(typesToGenerate))
+	vers := make([]generator.Package, 0, 2*len(typesToGenerate))
 
 	for _, t := range typesToGenerate {
 		// Fix for golang iterator bug.
 		t := t
-
 		packagePath := packagePath + "/" + strings.ToLower(t.Name.Name)
 		typedInformerPackage := typedInformerPackage(groupPkgName, gv, customArgs.ExternalVersionsInformersPackage)
 
@@ -372,7 +363,6 @@ func versionInformerPackages(basePackage string, groupPkgName string, gv clientg
 					typedInformerPackage:        typedInformerPackage,
 					groupInformerFactoryPackage: factoryPackagePath,
 				})
-
 				return generators
 			},
 			FilterFunc: func(c *generator.Context, t *types.Type) bool {
@@ -400,7 +390,6 @@ func versionInformerPackages(basePackage string, groupPkgName string, gv clientg
 					informerInjectionPkg:    packagePath,
 					fakeFactoryInjectionPkg: filepath.Join(factoryPackagePath, "fake"),
 				})
-
 				return generators
 			},
 			FilterFunc: func(c *generator.Context, t *types.Type) bool {
@@ -414,22 +403,21 @@ func versionInformerPackages(basePackage string, groupPkgName string, gv clientg
 
 func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentypes.GroupVersion, groupGoName string, boilerplate []byte, typesToGenerate []*types.Type, customArgs *informergenargs.CustomArgs) []generator.Package {
 	packagePath := filepath.Join(basePackage, "reconciler", groupPkgName, strings.ToLower(gv.Version.NonEmpty()))
+	clientPackagePath := filepath.Join(basePackage, "client")
 
-	vers := make([]generator.Package, 0, len(typesToGenerate))
+	vers := make([]generator.Package, 0, 4*len(typesToGenerate))
 
 	for _, t := range typesToGenerate {
 		// Fix for golang iterator bug.
 		t := t
-
 		extracted := extractCommentTags(t)
 		reconcilerClass, hasReconcilerClass := extractReconcilerClassTag(extracted)
 		nonNamespaced := isNonNamespaced(extracted)
+		isKRShaped := isKRShaped(extracted)
 
 		packagePath := filepath.Join(packagePath, strings.ToLower(t.Name.Name))
 
-		clientPackagePath := filepath.Join(basePackage, "client")
 		informerPackagePath := filepath.Join(basePackage, "informers", groupPkgName, strings.ToLower(gv.Version.NonEmpty()), strings.ToLower(t.Name.Name))
-
 		listerPackagePath := filepath.Join(customArgs.ListersPackage, groupPkgName, strings.ToLower(gv.Version.NonEmpty()))
 
 		// Controller
@@ -453,7 +441,6 @@ func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentyp
 					reconcilerClass:     reconcilerClass,
 					hasReconcilerClass:  hasReconcilerClass,
 				})
-
 				return generators
 			},
 			FilterFunc: func(c *generator.Context, t *types.Type) bool {
@@ -481,7 +468,6 @@ func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentyp
 					reconcilerClass:     reconcilerClass,
 					hasReconcilerClass:  hasReconcilerClass,
 				})
-
 				return generators
 			},
 			FilterFunc: func(c *generator.Context, t *types.Type) bool {
@@ -512,8 +498,8 @@ func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentyp
 					reconcilerClass:    reconcilerClass,
 					hasReconcilerClass: hasReconcilerClass,
 					nonNamespaced:      nonNamespaced,
+					isKRShaped:         isKRShaped,
 				})
-
 				return generators
 			},
 			FilterFunc: func(c *generator.Context, t *types.Type) bool {
@@ -538,7 +524,6 @@ func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentyp
 					outputPackage:  filepath.Join(packagePath, "stub"),
 					imports:        generator.NewImportTracker(),
 				})
-
 				return generators
 			},
 			FilterFunc: func(c *generator.Context, t *types.Type) bool {
@@ -553,12 +538,11 @@ func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentyp
 func versionDuckPackages(basePackage string, groupPkgName string, gv clientgentypes.GroupVersion, groupGoName string, boilerplate []byte, typesToGenerate []*types.Type, customArgs *informergenargs.CustomArgs) []generator.Package {
 	packagePath := filepath.Join(basePackage, "ducks", groupPkgName, strings.ToLower(gv.Version.NonEmpty()))
 
-	vers := make([]generator.Package, 0, len(typesToGenerate))
+	vers := make([]generator.Package, 0, 2*len(typesToGenerate))
 
 	for _, t := range typesToGenerate {
 		// Fix for golang iterator bug.
 		t := t
-
 		packagePath := filepath.Join(packagePath, strings.ToLower(t.Name.Name))
 
 		// Impl
@@ -578,7 +562,6 @@ func versionDuckPackages(basePackage string, groupPkgName string, gv clientgenty
 					typeToGenerate: t,
 					imports:        generator.NewImportTracker(),
 				})
-
 				return generators
 			},
 			FilterFunc: func(c *generator.Context, t *types.Type) bool {
@@ -605,7 +588,6 @@ func versionDuckPackages(basePackage string, groupPkgName string, gv clientgenty
 					groupGoName:      groupGoName,
 					duckInjectionPkg: packagePath,
 				})
-
 				return generators
 			},
 			FilterFunc: func(c *generator.Context, t *types.Type) bool {
diff --git a/codegen/cmd/injection-gen/generators/reconciler_reconciler.go b/codegen/cmd/injection-gen/generators/reconciler_reconciler.go
index d1ceb54ed..70a6578f3 100644
--- a/codegen/cmd/injection-gen/generators/reconciler_reconciler.go
+++ b/codegen/cmd/injection-gen/generators/reconciler_reconciler.go
@@ -40,6 +40,7 @@ type reconcilerReconcilerGenerator struct {
 	reconcilerClass    string
 	hasReconcilerClass bool
 	nonNamespaced      bool
+	isKRShaped         bool
 
 	groupGoName  string
 	groupVersion clientgentypes.GroupVersion
@@ -74,6 +75,7 @@ func (g *reconcilerReconcilerGenerator) GenerateType(c *generator.Context, t *ty
 		"version":       namer.IC(g.groupVersion.Version.String()),
 		"class":         g.reconcilerClass,
 		"hasClass":      g.hasReconcilerClass,
+		"isKRShaped":    g.isKRShaped,
 		"nonNamespaced": g.nonNamespaced,
 		"controllerImpl": c.Universe.Type(types.Name{
 			Package: "knative.dev/pkg/controller",
@@ -145,6 +147,10 @@ func (g *reconcilerReconcilerGenerator) GenerateType(c *generator.Context, t *ty
 			Package: "context",
 			Name:    "Context",
 		}),
+		"reflectDeepEqual":    c.Universe.Package("reflect").Function("DeepEqual"),
+		"equalitySemantic":    c.Universe.Package("k8s.io/apimachinery/pkg/api/equality").Variable("Semantic"),
+		"jsonMarshal":         c.Universe.Package("encoding/json").Function("Marshal"),
+		"typesMergePatchType": c.Universe.Package("k8s.io/apimachinery/pkg/types").Constant("MergePatchType"),
 	}
 
 	sw.Do(reconcilerInterfaceFactory, m)
@@ -305,9 +311,17 @@ func (r *reconcilerImpl) Reconcile(ctx {{.contextContext|raw}}, key string) erro
 			logger.Warnw("Failed to set finalizers", zap.Error(err))
 		}
 
+		{{if .isKRShaped}}
+		reconciler.PreProcessReconcile(ctx, resource)
+		{{end}}
+
 		// Reconcile this copy of the resource and then write back any status
 		// updates regardless of whether the reconciliation errored out.
 		reconcileEvent = r.reconciler.ReconcileKind(ctx, resource)
+
+		{{if .isKRShaped}}
+		reconciler.PostProcessReconcile(ctx, resource)
+		{{end}}
 	} else if fin, ok := r.reconciler.(Finalizer); ok {
 		// Append the target method to the logger.
 		logger = logger.With(zap.String("targetMethod", "FinalizeKind"))
@@ -321,7 +335,7 @@ func (r *reconcilerImpl) Reconcile(ctx {{.contextContext|raw}}, key string) erro
 	}
 
 	// Synchronize the status.
-	if equality.Semantic.DeepEqual(original.Status, resource.Status) {
+	if {{.equalitySemantic|raw}}.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
@@ -339,13 +353,19 @@ func (r *reconcilerImpl) Reconcile(ctx {{.contextContext|raw}}, key string) erro
 		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
-		} else {
-			logger.Errorw("Returned an error", zap.Error(reconcileEvent))
-			r.Recorder.Event(resource, {{.corev1EventTypeWarning|raw}}, "InternalError", reconcileEvent.Error())
-			return reconcileEvent
 		}
+
+		logger.Errorw("Returned an error", zap.Error(reconcileEvent))
+		r.Recorder.Event(resource, {{.corev1EventTypeWarning|raw}}, "InternalError", reconcileEvent.Error())
+		return reconcileEvent
 	}
+
 	return nil
 }
 `
@@ -368,7 +388,7 @@ func (r *reconcilerImpl) updateStatus(existing *{{.type|raw}}, desired *{{.type|
 		}
 
 		// If there's nothing to update, just return.
-		if reflect.DeepEqual(existing.Status, desired.Status) {
+		if {{.reflectDeepEqual|raw}}(existing.Status, desired.Status) {
 			return nil
 		}
 
@@ -433,7 +453,7 @@ func (r *reconcilerImpl) updateFinalizersFiltered(ctx {{.contextContext|raw}}, r
 		},
 	}
 
-	patch, err := json.Marshal(mergePatch)
+	patch, err := {{.jsonMarshal|raw}}(mergePatch)
 	if err != nil {
 		return resource, err
 	}
@@ -443,7 +463,7 @@ func (r *reconcilerImpl) updateFinalizersFiltered(ctx {{.contextContext|raw}}, r
 	{{else}}
 	patcher := r.Client.{{.group}}{{.version}}().{{.type|apiGroup}}(resource.Namespace)
 	{{end}}
-	resource, err = patcher.Patch(resource.Name, types.MergePatchType, patch)
+	resource, err = patcher.Patch(resource.Name, {{.typesMergePatchType|raw}}, patch)
 	if err != nil {
 		r.Recorder.Eventf(resource, {{.corev1EventTypeWarning|raw}}, "FinalizerUpdateFailed",
 			"Failed to update finalizers for %q: %v", resource.Name, err)
diff --git a/configmap/parse.go b/configmap/parse.go
new file mode 100644
index 000000000..097b38f90
--- /dev/null
+++ b/configmap/parse.go
@@ -0,0 +1,112 @@
+/*
+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.
+*/
+
+package configmap
+
+import (
+	"strconv"
+	"strings"
+	"time"
+)
+
+// ParseFunc is a function taking ConfigMap data and applying a parse operation to it.
+type ParseFunc func(map[string]string) error
+
+// AsString passes the value at key through into the target, if it exists.
+func AsString(key string, target *string) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			*target = raw
+		}
+		return nil
+	}
+}
+
+// AsBool parses the value at key as a boolean into the target, if it exists.
+func AsBool(key string, target *bool) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			*target = strings.EqualFold(raw, "true")
+		}
+		return nil
+	}
+}
+
+// AsInt32 parses the value at key as an int32 into the target, if it exists.
+func AsInt32(key string, target *int32) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			val, err := strconv.ParseInt(raw, 10, 32)
+			if err != nil {
+				return err
+			}
+			*target = int32(val)
+		}
+		return nil
+	}
+}
+
+// AsInt64 parses the value at key as an int64 into the target, if it exists.
+func AsInt64(key string, target *int64) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			val, err := strconv.ParseInt(raw, 10, 64)
+			if err != nil {
+				return err
+			}
+			*target = val
+		}
+		return nil
+	}
+}
+
+// AsFloat64 parses the value at key as a float64 into the target, if it exists.
+func AsFloat64(key string, target *float64) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			val, err := strconv.ParseFloat(raw, 64)
+			if err != nil {
+				return err
+			}
+			*target = val
+		}
+		return nil
+	}
+}
+
+// AsDuration parses the value at key as a time.Duration into the target, if it exists.
+func AsDuration(key string, target *time.Duration) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			val, err := time.ParseDuration(raw)
+			if err != nil {
+				return err
+			}
+			*target = val
+		}
+		return nil
+	}
+}
+
+// Parse parses the given map using the parser functions passed in.
+func Parse(data map[string]string, parsers ...ParseFunc) error {
+	for _, parse := range parsers {
+		if err := parse(data); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/controller/controller.go b/controller/controller.go
index 4713335d1..15496b8a9 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -427,20 +427,20 @@ func (c *Impl) processNextWorkItem() bool {
 	// Embed the key into the logger and attach that to the context we pass
 	// to the Reconciler.
 	logger := c.logger.With(zap.String(logkey.TraceId, uuid.New().String()), zap.String(logkey.Key, keyStr))
-	ctx := logging.WithLogger(context.TODO(), logger)
+	ctx := logging.WithLogger(context.Background(), logger)
 
 	// Run Reconcile, passing it the namespace/name string of the
 	// resource to be synced.
 	if err = c.Reconciler.Reconcile(ctx, keyStr); err != nil {
 		c.handleErr(err, key)
-		logger.Infof("Reconcile failed. Time taken: %v.", time.Since(startTime))
+		logger.Info("Reconcile failed. Time taken: ", time.Since(startTime))
 		return true
 	}
 
 	// Finally, if no error occurs we Forget this item so it does not
 	// have any delay when another change happens.
 	c.WorkQueue.Forget(key)
-	logger.Infof("Reconcile succeeded. Time taken: %v.", time.Since(startTime))
+	logger.Info("Reconcile succeeded. Time taken: ", time.Since(startTime))
 
 	return true
 }
diff --git a/go.mod b/go.mod
index 4cbe57269..976998bf5 100644
--- a/go.mod
+++ b/go.mod
@@ -48,7 +48,7 @@ require (
 	k8s.io/klog v1.0.0
 	k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a
 	k8s.io/test-infra v0.0.0-20200407001919-bc7f71ef65b8
-	knative.dev/test-infra v0.0.0-20200505192244-75864c82db21
+	knative.dev/test-infra v0.0.0-20200513011557-d03429a76034
 )
 
 replace (
diff --git a/go.sum b/go.sum
index c65606075..1673431ef 100644
--- a/go.sum
+++ b/go.sum
@@ -1346,10 +1346,12 @@ knative.dev/caching v0.0.0-20190719140829-2032732871ff/go.mod h1:dHXFU6CGlLlbzaW
 knative.dev/eventing-contrib v0.6.1-0.20190723221543-5ce18048c08b/go.mod h1:SnXZgSGgMSMLNFTwTnpaOH7hXDzTFtw0J8OmHflNx3g=
 knative.dev/pkg v0.0.0-20191101194912-56c2594e4f11/go.mod h1:pgODObA1dTyhNoFxPZTTjNWfx6F0aKsKzn+vaT9XO/Q=
 knative.dev/pkg v0.0.0-20191111150521-6d806b998379/go.mod h1:pgODObA1dTyhNoFxPZTTjNWfx6F0aKsKzn+vaT9XO/Q=
-knative.dev/pkg v0.0.0-20200504180943-4a2ba059b008/go.mod h1:1RvwKBbKqKYt5rgI4lfYdWCdtXgMxJY73QxPb3jZPC4=
-knative.dev/test-infra v0.0.0-20200430225942-f7c1fafc1cde/go.mod h1:xcdUkMJrLlBswIZqL5zCuBFOC22WIPMQoVX1L35i0vQ=
-knative.dev/test-infra v0.0.0-20200505192244-75864c82db21 h1:SsvqMKpvrn7cl7UqRUIT90SXDowHzpzHwHaTu+wN70s=
-knative.dev/test-infra v0.0.0-20200505192244-75864c82db21/go.mod h1:AqweEMgaMbb2xmYq9ZOPsH/lQ61qNx2XGr5tGltj5QU=
+knative.dev/pkg v0.0.0-20200428194351-90fc61bae7f7/go.mod h1:o+e8OVEJKIuvXPsGVPIautjXgs05xbos7G+QMRjuUps=
+knative.dev/pkg v0.0.0-20200505191044-3da93ebb24c2/go.mod h1:Q6sL35DdGs8hIQZKdaCXJGgY8f90BmNBKSb8z6d/BTM=
+knative.dev/test-infra v0.0.0-20200407185800-1b88cb3b45a5/go.mod h1:xcdUkMJrLlBswIZqL5zCuBFOC22WIPMQoVX1L35i0vQ=
+knative.dev/test-infra v0.0.0-20200505052144-5ea2f705bb55/go.mod h1:WqF1Azka+FxPZ20keR2zCNtiQA1MP9ZB4BH4HuI+SIU=
+knative.dev/test-infra v0.0.0-20200513011557-d03429a76034 h1:JxqONCZVS7or+Fv3ebVQoipuIBH7Ig3Qbx170hgIF+A=
+knative.dev/test-infra v0.0.0-20200513011557-d03429a76034/go.mod h1:aMif0KXL4g19YCYwsy4Ocjjz5xgPlseYV+B95Oo4JGE=
 modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
 modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
 modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
diff --git a/injection/README.md b/injection/README.md
index ab452b6e5..d30b090bc 100644
--- a/injection/README.md
+++ b/injection/README.md
@@ -452,6 +452,28 @@ value of the `<key>` annotation on a resource must match the value provided to
 `NewImpl` (or `NewReconcile`) for `ReconcileKind` or `FinalizeKind` to be called
 for that resource.
 
+#### Annotation based common logic
+
+**krshaped=true may become the default if omitted in the future**
+
+Reconcilers can handle common logic for resources that conform to the KRShaped
+interface. This allows the generated code to automatically increment
+ObservedGeneration.
+
+```go
+// +genreconciler:krshapedlogic=true
+```
+
+Setting this annotation will emit the following in the generated reconciler.
+
+```go
+reconciler.PreProcessReconcile(ctx, resource)
+
+reconcileEvent = r.reconciler.ReconcileKind(ctx, resource)
+
+reconciler.PostProcessReconcile(ctx, resource)
+```
+
 #### Stubs
 
 To get started, or to use as reference. It is intended to be copied out of the
diff --git a/injection/sharedmain/main.go b/injection/sharedmain/main.go
index c169fa20c..1410138e1 100644
--- a/injection/sharedmain/main.go
+++ b/injection/sharedmain/main.go
@@ -189,7 +189,7 @@ func MainWithConfig(ctx context.Context, component string, cfg *rest.Config, cto
 	// Set up leader election config
 	leaderElectionConfig, err := GetLeaderElectionConfig(ctx)
 	if err != nil {
-		logger.Fatalf("Error loading leader election configuration: %v", err)
+		logger.Fatalw("Error loading leader election configuration", zap.Error(err))
 	}
 	leConfig := leaderElectionConfig.GetComponentConfig(component)
 
@@ -343,9 +343,9 @@ func SetupConfigMapWatchOrDie(ctx context.Context, logger *zap.SugaredLogger) *c
 	if cmLabel := system.ResourceLabel(); cmLabel != "" {
 		req, err := configmap.FilterConfigByLabelExists(cmLabel)
 		if err != nil {
-			logger.With(zap.Error(err)).Fatalf("Failed to generate requirement for label %q")
+			logger.Fatalw("Failed to generate requirement for label "+cmLabel, zap.Error(err))
 		}
-		logger.Infof("Setting up ConfigMap watcher with label selector %q", req)
+		logger.Info("Setting up ConfigMap watcher with label selector: ", req)
 		cmLabelReqs = append(cmLabelReqs, *req)
 	}
 	// TODO(mattmoor): This should itself take a context and be injection-based.
@@ -360,7 +360,7 @@ func WatchLoggingConfigOrDie(ctx context.Context, cmw *configmap.InformedWatcher
 		metav1.GetOptions{}); err == nil {
 		cmw.Watch(logging.ConfigMapName(), logging.UpdateLevelFromConfigMap(logger, atomicLevel, component))
 	} else if !apierrors.IsNotFound(err) {
-		logger.With(zap.Error(err)).Fatalf("Error reading ConfigMap %q", logging.ConfigMapName())
+		logger.Fatalw("Error reading ConfigMap "+logging.ConfigMapName(), zap.Error(err))
 	}
 }
 
@@ -374,7 +374,7 @@ func WatchObservabilityConfigOrDie(ctx context.Context, cmw *configmap.InformedW
 			metrics.ConfigMapWatcher(component, SecretFetcher(ctx), logger),
 			profilingHandler.UpdateFromConfigMap)
 	} else if !apierrors.IsNotFound(err) {
-		logger.With(zap.Error(err)).Fatalf("Error reading ConfigMap %q", metrics.ConfigMapName())
+		logger.Fatalw("Error reading ConfigMap "+metrics.ConfigMapName(), zap.Error(err))
 	}
 }
 
@@ -457,7 +457,7 @@ func RunLeaderElected(ctx context.Context, logger *zap.SugaredLogger, run func(c
 			EventRecorder: recorder,
 		})
 	if err != nil {
-		logger.Fatalw("Error creating lock: %v", err)
+		logger.Fatalw("Error creating lock", zap.Error(err))
 	}
 
 	// Execute the `run` function when we have the lock.
@@ -469,7 +469,7 @@ func RunLeaderElected(ctx context.Context, logger *zap.SugaredLogger, run func(c
 		Callbacks: leaderelection.LeaderCallbacks{
 			OnStartedLeading: run,
 			OnStoppedLeading: func() {
-				logger.Fatal("leaderelection lost")
+				logger.Fatal("Leader election lost")
 			},
 		},
 		ReleaseOnCancel: true,
diff --git a/logging/config.go b/logging/config.go
index a0fb96e27..9e6f488fd 100644
--- a/logging/config.go
+++ b/logging/config.go
@@ -52,7 +52,7 @@ var (
 func NewLogger(configJSON string, levelOverride string, opts ...zap.Option) (*zap.SugaredLogger, zap.AtomicLevel) {
 	logger, atomicLevel, err := newLoggerFromConfig(configJSON, levelOverride, opts)
 	if err == nil {
-		return enrichLoggerWithCommitID(logger.Sugar()), atomicLevel
+		return enrichLoggerWithCommitID(logger), atomicLevel
 	}
 
 	loggingCfg := zap.NewProductionConfig()
@@ -66,18 +66,18 @@ func NewLogger(configJSON string, levelOverride string, opts ...zap.Option) (*za
 	if err2 != nil {
 		panic(err2)
 	}
-	return enrichLoggerWithCommitID(logger.Named(fallbackLoggerName).Sugar()), loggingCfg.Level
+	return enrichLoggerWithCommitID(logger.Named(fallbackLoggerName)), loggingCfg.Level
 }
 
-func enrichLoggerWithCommitID(logger *zap.SugaredLogger) *zap.SugaredLogger {
+func enrichLoggerWithCommitID(logger *zap.Logger) *zap.SugaredLogger {
 	commmitID, err := changeset.Get()
 	if err == nil {
 		// Enrich logs with GitHub commit ID.
-		return logger.With(zap.String(logkey.GitHubCommitID, commmitID))
+		return logger.With(zap.String(logkey.GitHubCommitID, commmitID)).Sugar()
 	}
 
-	logger.Infof("Fetch GitHub commit ID from kodata failed: %v", err)
-	return logger
+	logger.Info("Fetch GitHub commit ID from kodata failed", zap.Error(err))
+	return logger.Sugar()
 }
 
 // NewLoggerFromConfig creates a logger using the provided Config
diff --git a/logging/object_encoders.go b/logging/object_encoders.go
new file mode 100644
index 000000000..b6f39fe21
--- /dev/null
+++ b/logging/object_encoders.go
@@ -0,0 +1,59 @@
+/*
+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.
+*/
+
+package logging
+
+import (
+	"strings"
+
+	"go.uber.org/zap/zapcore"
+
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+// This file contains the specific object encoders for use in Knative
+// to optimize logging experience and performance.
+
+// StringSet returns a marshaler for the set of strings.
+// To use this in sugared logger do:
+//	logger.Infow("Revision State", zap.Object("healthy", logging.StringSet(healthySet)),
+//		zap.Object("unhealthy", logging.StringSet(unhealthySet)))
+// To use with non-sugared logger do:
+//	logger.Info("Revision State", zap.Object("healthy", logging.StringSet(healthySet)),
+//		zap.Object("unhealthy", logging.StringSet(unhealthySet)))
+func StringSet(s sets.String) zapcore.ObjectMarshalerFunc {
+	return func(enc zapcore.ObjectEncoder) error {
+		enc.AddString("keys", strings.Join(s.UnsortedList(), ","))
+		return nil
+	}
+}
+
+// NamespacedName returns a marshaler for NamespacedName.
+// To use this in sugared logger do:
+//	logger.Infow("Enqueuing", zap.Object("key", logging.NamespacedName(n)))
+// To use with non-sugared logger do:
+//	logger.Info("Enqueuing", zap.Object("key", logging.NamespacedName(n)))
+func NamespacedName(n types.NamespacedName) zapcore.ObjectMarshalerFunc {
+	return func(enc zapcore.ObjectEncoder) error {
+		if n.Namespace != "" {
+			enc.AddString("key", n.Name)
+		} else {
+			enc.AddString("key", n.Namespace+"/"+n.Name)
+		}
+		return nil
+	}
+}
diff --git a/metrics/config.go b/metrics/config.go
index 71161baa1..a752e8d37 100644
--- a/metrics/config.go
+++ b/metrics/config.go
@@ -21,6 +21,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"log"
 	"os"
 	"path"
 	"strconv"
@@ -153,7 +154,20 @@ func NewStackdriverClientConfigFromMap(config map[string]string) *StackdriverCli
 // record applies the `ros` Options to each measurement in `mss` and then records the resulting
 // measurements in the metricsConfig's designated backend.
 func (mc *metricsConfig) record(ctx context.Context, mss []stats.Measurement, ros ...stats.Options) error {
-	if mc == nil || mc.recorder == nil {
+	if mc == nil {
+		log.Println(`The metricsConfig has not been initialized yet.
+
+If this is a Go unit test consuming metric.Record(...) or metric.RecordBatch(...) then
+it should add the following import:
+
+import (
+	_ "knative.dev/pkg/metrics/testing"
+)`)
+		// Don't record data points if the metric config is not initialized yet.
+		// At this point, it's unclear whether should record or not.
+		return nil
+	}
+	if mc.recorder == nil {
 		return stats.RecordWithOptions(ctx, append(ros, stats.WithMeasurements(mss...))...)
 	}
 	return mc.recorder(ctx, mss, ros...)
diff --git a/metrics/metricskey/constants.go b/metrics/metricskey/constants.go
index d9e2a1ace..569067681 100644
--- a/metrics/metricskey/constants.go
+++ b/metrics/metricskey/constants.go
@@ -41,6 +41,12 @@ const (
 	// LabelResponseCodeClass is the label for the HTTP response status code class. For example, "2xx", "3xx", etc.
 	LabelResponseCodeClass = "response_code_class"
 
+	// LabelResponseError is the label for client error. For HTTP, A non-2xx status code doesn't cause an error.
+	LabelResponseError = "response_error"
+
+	// LabelResponseTimeout is the label timeout.
+	LabelResponseTimeout = "response_timeout"
+
 	// ValueUnknown is the default value if the field is unknown, e.g. project will be unknown if Knative
 	// is not running on GKE.
 	ValueUnknown = "unknown"
diff --git a/metrics/utils.go b/metrics/utils.go
index 7345cec92..e2e56a415 100644
--- a/metrics/utils.go
+++ b/metrics/utils.go
@@ -16,7 +16,11 @@ limitations under the License.
 
 package metrics
 
-import "strconv"
+import (
+	"strconv"
+
+	"go.opencensus.io/tag"
+)
 
 // ResponseCodeClass converts an HTTP response code to a string representing its response code class.
 // E.g., The response code class is "5xx" for response code 503.
@@ -24,3 +28,27 @@ func ResponseCodeClass(responseCode int) string {
 	// Get the hundred digit of the response code and concatenate "xx".
 	return strconv.Itoa(responseCode/100) + "xx"
 }
+
+// MaybeInsertIntTag conditionally insert the tag when cond is true.
+func MaybeInsertIntTag(key tag.Key, value int, cond bool) tag.Mutator {
+	if cond {
+		return tag.Insert(key, strconv.Itoa(value))
+	}
+	return tag.Insert(key, "")
+}
+
+// MaybeInsertBoolTag conditionally insert the tag when cond is true.
+func MaybeInsertBoolTag(key tag.Key, value bool, cond bool) tag.Mutator {
+	if cond {
+		return tag.Insert(key, strconv.FormatBool(value))
+	}
+	return tag.Insert(key, "")
+}
+
+// MaybeInsertStringTag conditionally insert the tag when cond is true.
+func MaybeInsertStringTag(key tag.Key, value string, cond bool) tag.Mutator {
+	if cond {
+		return tag.Insert(key, value)
+	}
+	return tag.Insert(key, "")
+}
diff --git a/network/network.go b/network/network.go
index 32015ada3..0c4749d49 100644
--- a/network/network.go
+++ b/network/network.go
@@ -17,6 +17,8 @@ limitations under the License.
 package network
 
 import (
+	"net/http"
+	"strings"
 	"time"
 )
 
@@ -42,4 +44,18 @@ const (
 	// with this header will not be passed to the user container or
 	// included in request metrics.
 	ProbeHeaderName = "K-Network-Probe"
+
+	// Since K8s 1.8, prober requests have
+	//   User-Agent = "kube-probe/{major-version}.{minor-version}".
+	KubeProbeUAPrefix = "kube-probe/"
+
+	// Istio with mTLS rewrites probes, but their probes pass a different
+	// user-agent.  So we augment the probes with this header.
+	KubeletProbeHeaderName = "K-Kubelet-Probe"
 )
+
+// IsKubeletProbe returns true if the request is a Kubernetes probe.
+func IsKubeletProbe(r *http.Request) bool {
+	return strings.HasPrefix(r.Header.Get("User-Agent"), KubeProbeUAPrefix) ||
+		r.Header.Get(KubeletProbeHeaderName) != ""
+}
diff --git a/network/transports.go b/network/transports.go
index 019f4ba87..745e6a5a8 100644
--- a/network/transports.go
+++ b/network/transports.go
@@ -18,7 +18,6 @@ package network
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net"
 	"net/http"
@@ -54,8 +53,6 @@ var backOffTemplate = wait.Backoff{
 	Steps:    15,
 }
 
-var errDialTimeout = errors.New("timed out dialing")
-
 // DialWithBackOff executes `net.Dialer.DialContext()` with exponentially increasing
 // dial timeouts. In addition it sleeps with random jitter between tries.
 var DialWithBackOff = NewBackoffDialer(backOffTemplate)
@@ -91,7 +88,7 @@ func dialBackOffHelper(ctx context.Context, network, address string, bo wait.Bac
 		}
 		return c, nil
 	}
-	elapsed := time.Now().Sub(start)
+	elapsed := time.Since(start)
 	return nil, fmt.Errorf("timed out dialing after %.2fs", elapsed.Seconds())
 }
 
diff --git a/reconciler/reconcile_common.go b/reconciler/reconcile_common.go
new file mode 100644
index 000000000..8af08f61a
--- /dev/null
+++ b/reconciler/reconcile_common.go
@@ -0,0 +1,55 @@
+/*
+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
+
+    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 reconciler
+
+import (
+	"context"
+
+	duckv1 "knative.dev/pkg/apis/duck/v1"
+	"knative.dev/pkg/logging"
+)
+
+const failedGenerationBump = "NewObservedGenFailure"
+
+// PreProcessReconcile contains logic to apply before reconciliation of a resource.
+func PreProcessReconcile(ctx context.Context, resource duckv1.KRShaped) {
+	newStatus := resource.GetStatus()
+
+	if newStatus.ObservedGeneration != resource.GetGeneration() {
+		condSet := resource.GetConditionSet()
+		manager := condSet.Manage(newStatus)
+
+		// Reset Ready/Successful to unknown. The reconciler is expected to overwrite this.
+		manager.MarkUnknown(condSet.GetTopLevelConditionType(), failedGenerationBump, "unsuccessfully observed a new generation")
+	}
+}
+
+// PostProcessReconcile contains logic to apply after reconciliation of a resource.
+func PostProcessReconcile(ctx context.Context, resource duckv1.KRShaped) {
+	logger := logging.FromContext(ctx)
+	newStatus := resource.GetStatus()
+	mgr := resource.GetConditionSet().Manage(newStatus)
+
+	// Bump observed generation to denote that we have processed this
+	// generation regardless of success or failure.
+	newStatus.ObservedGeneration = resource.GetGeneration()
+
+	rc := mgr.GetTopLevelCondition()
+	if rc.Reason == failedGenerationBump {
+		logger.Warn("A reconciler observed a new generation without updating the resource status")
+	}
+}
diff --git a/source/source_stats_reporter.go b/source/source_stats_reporter.go
index 482c5a42e..b22eff94a 100644
--- a/source/source_stats_reporter.go
+++ b/source/source_stats_reporter.go
@@ -18,7 +18,6 @@ package source
 
 import (
 	"context"
-	"strconv"
 
 	"go.opencensus.io/stats/view"
 	"knative.dev/pkg/metrics"
@@ -48,6 +47,8 @@ var (
 	sourceResourceGroupKey = tag.MustNewKey(metricskey.LabelResourceGroup)
 	responseCodeKey        = tag.MustNewKey(metricskey.LabelResponseCode)
 	responseCodeClassKey   = tag.MustNewKey(metricskey.LabelResponseCodeClass)
+	responseError          = tag.MustNewKey(metricskey.LabelResponseError)
+	responseTimeout        = tag.MustNewKey(metricskey.LabelResponseTimeout)
 )
 
 type ReportArgs struct {
@@ -56,6 +57,8 @@ type ReportArgs struct {
 	EventSource   string
 	Name          string
 	ResourceGroup string
+	Error         string
+	Timeout       bool
 }
 
 func init() {
@@ -103,8 +106,10 @@ func (r *reporter) generateTag(args *ReportArgs, responseCode int) (context.Cont
 		tag.Insert(eventTypeKey, args.EventType),
 		tag.Insert(sourceNameKey, args.Name),
 		tag.Insert(sourceResourceGroupKey, args.ResourceGroup),
-		tag.Insert(responseCodeKey, strconv.Itoa(responseCode)),
-		tag.Insert(responseCodeClassKey, metrics.ResponseCodeClass(responseCode)))
+		metrics.MaybeInsertIntTag(responseCodeKey, responseCode, responseCode > 0),
+		metrics.MaybeInsertStringTag(responseCodeClassKey, metrics.ResponseCodeClass(responseCode), responseCode > 0),
+		tag.Insert(responseError, args.Error),
+		metrics.MaybeInsertBoolTag(responseTimeout, args.Timeout, args.Error != ""))
 }
 
 func register() {
@@ -115,7 +120,9 @@ func register() {
 		sourceNameKey,
 		sourceResourceGroupKey,
 		responseCodeKey,
-		responseCodeClassKey}
+		responseCodeClassKey,
+		responseError,
+		responseTimeout}
 
 	// Create view to see our measurements.
 	if err := view.Register(
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 2cfed1919..9464bd40b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -914,7 +914,7 @@ k8s.io/utils/buffer
 k8s.io/utils/integer
 k8s.io/utils/pointer
 k8s.io/utils/trace
-# knative.dev/test-infra v0.0.0-20200505192244-75864c82db21
+# knative.dev/test-infra v0.0.0-20200513011557-d03429a76034
 ## explicit
 knative.dev/test-infra/scripts
 # sigs.k8s.io/yaml v1.1.0
diff --git a/webhook/webhook.go b/webhook/webhook.go
index 3315ba163..e29ea9611 100644
--- a/webhook/webhook.go
+++ b/webhook/webhook.go
@@ -22,6 +22,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"time"
 
 	// Injection stuff
 	kubeclient "knative.dev/pkg/client/injection/kube/client"
@@ -33,6 +34,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 	corelisters "k8s.io/client-go/listers/core/v1"
 	"knative.dev/pkg/logging"
+	"knative.dev/pkg/network"
 	"knative.dev/pkg/system"
 	certresources "knative.dev/pkg/webhook/certificates/resources"
 )
@@ -71,6 +73,13 @@ const (
 	Connect Operation = admissionv1beta1.Connect
 )
 
+var (
+	// GracePeriod is the duration that the webhook will wait after it's
+	// context is cancelled (and probes are failing) before shutting down
+	// the http server.
+	GracePeriod = 30 * time.Second
+)
+
 // Webhook implements the external webhook for validation of
 // resources and configuration.
 type Webhook struct {
@@ -81,6 +90,12 @@ type Webhook struct {
 	// synced is function that is called when the informers have been synced.
 	synced context.CancelFunc
 
+	// stopCh is closed when we should start failing readiness probes.
+	stopCh chan struct{}
+	// grace period is how long to wait after failing readiness probes
+	// before shutting down.
+	gracePeriod time.Duration
+
 	mux          http.ServeMux
 	secretlister corelisters.SecretLister
 }
@@ -129,6 +144,8 @@ func New(
 		secretlister: secretInformer.Lister(),
 		Logger:       logger,
 		synced:       cancel,
+		stopCh:       make(chan struct{}),
+		gracePeriod:  GracePeriod,
 	}
 
 	webhook.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@@ -193,8 +210,6 @@ func (wh *Webhook) Run(stop <-chan struct{}) error {
 		},
 	}
 
-	logger.Info("Found certificates for webhook...")
-
 	eg, ctx := errgroup.WithContext(ctx)
 	eg.Go(func() error {
 		if err := server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
@@ -206,16 +221,40 @@ func (wh *Webhook) Run(stop <-chan struct{}) error {
 
 	select {
 	case <-stop:
-		if err := server.Close(); err != nil {
-			return err
-		}
+		eg.Go(func() error {
+			// Start failing readiness probes immediately.
+			logger.Info("Starting to fail readiness probes...")
+			close(wh.stopCh)
+
+			// Wait for a grace period for the above to take effect and this Pod's
+			// endpoint to be removed from the webhook service's Endpoints.
+			// For this to be effective, it must be greater than the probe's
+			// periodSeconds times failureThreshold by a margin suitable to
+			// propagate the new Endpoints data across the cluster.
+			time.Sleep(wh.gracePeriod)
+
+			return server.Shutdown(context.Background())
+		})
+
+		// Wait for all outstanding go routined to terminate, including our new one.
 		return eg.Wait()
+
 	case <-ctx.Done():
 		return fmt.Errorf("webhook server bootstrap failed %w", ctx.Err())
 	}
 }
 
 func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// Respond to probes regardless of path.
+	if network.IsKubeletProbe(r) {
+		select {
+		case <-wh.stopCh:
+			http.Error(w, "shutting down", http.StatusInternalServerError)
+		default:
+			w.WriteHeader(http.StatusOK)
+		}
+	}
+
 	// Verify the content type is accurate.
 	contentType := r.Header.Get("Content-Type")
 	if contentType != "application/json" {
diff --git a/websocket/connection.go b/websocket/connection.go
index 9180d5fa9..914f62c0b 100644
--- a/websocket/connection.go
+++ b/websocket/connection.go
@@ -129,12 +129,12 @@ func NewDurableConnection(target string, messageChan chan []byte, logger *zap.Su
 		for {
 			select {
 			default:
-				logger.Infof("Connecting to %s", target)
+				logger.Info("Connecting to ", target)
 				if err := c.connect(); err != nil {
-					logger.With(zap.Error(err)).Errorf("Connecting to %s failed", target)
+					logger.Errorw("Failed connecting to "+target, zap.Error(err))
 					continue
 				}
-				logger.Debugf("Connected to %s", target)
+				logger.Debug("Connected to ", target)
 				if err := c.keepalive(); err != nil {
 					logger.With(zap.Error(err)).Errorf("Connection to %s broke down, reconnecting...", target)
 				}


Test patch:
diff --git a/apis/duck/v1/kresource_type_test.go b/apis/duck/v1/kresource_type_test.go
index 357353b14..e5c7f9371 100644
--- a/apis/duck/v1/kresource_type_test.go
+++ b/apis/duck/v1/kresource_type_test.go
@@ -22,22 +22,35 @@ import (
 	"knative.dev/pkg/apis"
 )
 
-func TestGetTopLevelCondition(t *testing.T) {
-	resource := KResource{}
-
-	condSet := apis.NewLivingConditionSet("Foo")
-	mgr := condSet.Manage(resource.GetStatus())
-	mgr.InitializeConditions()
-
-	if resource.GetTopLevelConditionType() != apis.ConditionReady {
-		t.Error("Expected Ready as happy condition for living condition set type")
+func TestGetConditionSet(t *testing.T) {
+	testCases := []struct {
+		name                      string
+		condSet                   apis.ConditionSet
+		expectedTopLevelCondition apis.ConditionType
+	}{
+		{
+			name:                      "living set",
+			condSet:                   apis.NewLivingConditionSet(),
+			expectedTopLevelCondition: apis.ConditionReady,
+		},
+		{
+			name:                      "batch set",
+			condSet:                   apis.NewBatchConditionSet(),
+			expectedTopLevelCondition: apis.ConditionSucceeded,
+		},
 	}
 
-	condSet = apis.NewBatchConditionSet("Foo")
-	mgr = condSet.Manage(resource.GetStatus())
-	mgr.InitializeConditions()
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			resource := KResource{}
+			test.condSet.Manage(resource.GetStatus()).InitializeConditions()
+
+			mgr := resource.GetConditionSet().Manage(resource.GetStatus())
 
-	if resource.GetTopLevelConditionType() != apis.ConditionSucceeded {
-		t.Error("Expected Succeeded as happy condition for living condition set type")
+			if mgr.GetTopLevelCondition().Type != test.expectedTopLevelCondition {
+				t.Errorf("wrong top-level condition got=%s want=%s",
+					mgr.GetTopLevelCondition().Type, test.expectedTopLevelCondition)
+			}
+		})
 	}
 }
diff --git a/apis/test/example/v1alpha1/fiz_types.go b/apis/test/example/v1alpha1/fiz_types.go
index 334e827a5..8b0fbe472 100644
--- a/apis/test/example/v1alpha1/fiz_types.go
+++ b/apis/test/example/v1alpha1/fiz_types.go
@@ -94,17 +94,12 @@ func (f *ClusterFiz) Validate(ctx context.Context) *apis.FieldError {
 	return nil
 }
 
-// GetTypeMeta retrieves the ObjectMeta of the ClusterFiz. Implements the KRShaped interface.
-func (f *ClusterFiz) GetTypeMeta() *metav1.TypeMeta {
-	return &f.TypeMeta
-}
-
 // GetStatus retrieves the status of the ClusterFiz. Implements the KRShaped interface.
 func (f *ClusterFiz) GetStatus() *duckv1.Status {
 	return &f.Status.Status
 }
 
-// GetTopLevelConditionType retrieves the happy condition of this resource. Implements the KRShaped interface.
-func (*ClusterFiz) GetTopLevelConditionType() apis.ConditionType {
-	return apis.ConditionReady
+// GetConditionSet retrieves the condition set for this resource. Implements the KRShaped interface.
+func (*ClusterFiz) GetConditionSet() apis.ConditionSet {
+	return apis.NewLivingConditionSet()
 }
diff --git a/apis/test/example/v1alpha1/foo_types.go b/apis/test/example/v1alpha1/foo_types.go
index 948254353..c681ea796 100644
--- a/apis/test/example/v1alpha1/foo_types.go
+++ b/apis/test/example/v1alpha1/foo_types.go
@@ -27,7 +27,7 @@ import (
 )
 
 // +genclient
-// +genreconciler
+// +genreconciler:krshapedlogic=true
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 
 // Foo is for testing.
@@ -93,17 +93,12 @@ func (f *Foo) Validate(ctx context.Context) *apis.FieldError {
 	return nil
 }
 
-// GetTypeMeta retrieves the ObjectMeta of the Foo. Implements the KRShaped interface.
-func (f *Foo) GetTypeMeta() *metav1.TypeMeta {
-	return &f.TypeMeta
-}
-
 // GetStatus retrieves the status of the Foo Implements the KRShaped interface.
 func (f *Foo) GetStatus() *duckv1.Status {
 	return &f.Status.Status
 }
 
-// GetTopLevelConditionType retrieves the happy condition of this resource. Implements the KRShaped interface.
-func (*Foo) GetTopLevelConditionType() apis.ConditionType {
-	return apis.ConditionSucceeded
+// GetConditionSet retrieves the condition set for this resource. Implements the KRShaped interface.
+func (*Foo) GetConditionSet() apis.ConditionSet {
+	return apis.NewLivingConditionSet()
 }
diff --git a/apis/test/pub/v1alpha1/bar_types.go b/apis/test/pub/v1alpha1/bar_types.go
index f27d5f660..48e4f081b 100644
--- a/apis/test/pub/v1alpha1/bar_types.go
+++ b/apis/test/pub/v1alpha1/bar_types.go
@@ -27,7 +27,7 @@ import (
 )
 
 // +genclient
-// +genreconciler:class=example.com/filter.class
+// +genreconciler:class=example.com/filter.class,krshapedlogic=true
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 
 // Bar is for testing.
@@ -93,17 +93,12 @@ func (b *Bar) Validate(ctx context.Context) *apis.FieldError {
 	return nil
 }
 
-// GetTypeMeta retrieves the ObjectMeta of the Bar. Implements the KRShaped interface.
-func (b *Bar) GetTypeMeta() *metav1.TypeMeta {
-	return &b.TypeMeta
-}
-
 // GetStatus retrieves the status of the Bar. Implements the KRShaped interface.
 func (b *Bar) GetStatus() *duckv1.Status {
 	return &b.Status.Status
 }
 
-// GetTopLevelConditionType retrieves the happy condition of this resource. Implements the KRShaped interface.
-func (*Bar) GetTopLevelConditionType() apis.ConditionType {
-	return apis.ConditionReady
+// GetConditionSet retrieves the condition set for this resource. Implements the KRShaped interface.
+func (*Bar) GetConditionSet() apis.ConditionSet {
+	return apis.NewLivingConditionSet()
 }
diff --git a/apis/volatile_time_test.go b/apis/volatile_time_test.go
index 88cd49862..6b9d25c6d 100644
--- a/apis/volatile_time_test.go
+++ b/apis/volatile_time_test.go
@@ -21,6 +21,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
 	"k8s.io/apimachinery/pkg/api/equality"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
@@ -84,4 +85,14 @@ func TestVolatileTimeEquality(t *testing.T) {
 	if !equality.Semantic.DeepEqual(tt1, tt2) {
 		t.Error("equality.Semantic.DeepEqual() = false, wanted true")
 	}
+	if cmp.Equal(tt1, tt2) {
+		t.Error("go-cmp.Equal should have still returned false")
+	}
+
+	opt := cmp.Comparer(func(VolatileTime, VolatileTime) bool {
+		return true
+	})
+	if !cmp.Equal(tt1, tt2, opt) {
+		t.Error("go-cmp.Equal with opt should returned true")
+	}
 }
diff --git a/configmap/parse_test.go b/configmap/parse_test.go
new file mode 100644
index 000000000..65b9ab56d
--- /dev/null
+++ b/configmap/parse_test.go
@@ -0,0 +1,128 @@
+/*
+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.
+*/
+
+package configmap
+
+import (
+	"testing"
+	"time"
+)
+
+type testConfig struct {
+	str string
+	boo bool
+	i32 int32
+	i64 int64
+	f64 float64
+	dur time.Duration
+}
+
+func TestParse(t *testing.T) {
+	tests := []struct {
+		name      string
+		conf      testConfig
+		data      map[string]string
+		want      testConfig
+		expectErr bool
+	}{{
+		name: "all good",
+		data: map[string]string{
+			"test-string":   "foo.bar",
+			"test-bool":     "true",
+			"test-int32":    "1",
+			"test-int64":    "2",
+			"test-float64":  "1.0",
+			"test-duration": "1m",
+		},
+		want: testConfig{
+			str: "foo.bar",
+			boo: true,
+			i32: 1,
+			i64: 2,
+			f64: 1.0,
+			dur: time.Minute,
+		},
+	}, {
+		name: "respect defaults",
+		conf: testConfig{
+			str: "foo.bar",
+			boo: true,
+			i32: 1,
+			i64: 2,
+			f64: 1.0,
+			dur: time.Minute,
+		},
+		want: testConfig{
+			str: "foo.bar",
+			boo: true,
+			i32: 1,
+			i64: 2,
+			f64: 1.0,
+			dur: time.Minute,
+		},
+	}, {
+		name: "bool defaults to false",
+		data: map[string]string{
+			"test-bool": "foo",
+		},
+		want: testConfig{
+			boo: false,
+		},
+	}, {
+		name: "int32 error",
+		data: map[string]string{
+			"test-int32": "foo",
+		},
+		expectErr: true,
+	}, {
+		name: "int64 error",
+		data: map[string]string{
+			"test-int64": "foo",
+		},
+		expectErr: true,
+	}, {
+		name: "float64 error",
+		data: map[string]string{
+			"test-float64": "foo",
+		},
+		expectErr: true,
+	}, {
+		name: "duration error",
+		data: map[string]string{
+			"test-duration": "foo",
+		},
+		expectErr: true,
+	}}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if err := Parse(test.data,
+				AsString("test-string", &test.conf.str),
+				AsBool("test-bool", &test.conf.boo),
+				AsInt32("test-int32", &test.conf.i32),
+				AsInt64("test-int64", &test.conf.i64),
+				AsFloat64("test-float64", &test.conf.f64),
+				AsDuration("test-duration", &test.conf.dur),
+			); (err == nil) == test.expectErr {
+				t.Fatal("Failed to parse data:", err)
+			}
+
+			if test.conf != test.want {
+				t.Fatalf("parsed = %v, want %v", test.conf, test.want)
+			}
+		})
+	}
+}
diff --git a/controller/stats_reporter_test.go b/controller/stats_reporter_test.go
index 495a3aae1..d82e7fe35 100644
--- a/controller/stats_reporter_test.go
+++ b/controller/stats_reporter_test.go
@@ -22,6 +22,8 @@ import (
 	"time"
 
 	"go.opencensus.io/stats/view"
+	"knative.dev/pkg/metrics/metricstest"
+	_ "knative.dev/pkg/metrics/testing"
 )
 
 func TestNewStatsReporterErrors(t *testing.T) {
@@ -53,13 +55,13 @@ func TestReportQueueDepth(t *testing.T) {
 
 	// Send statistics only once and observe the results
 	expectSuccess(t, func() error { return r.ReportQueueDepth(10) })
-	checkLastValueData(t, "work_queue_depth", wantTags, 10)
+	metricstest.CheckLastValueData(t, "work_queue_depth", wantTags, 10)
 
 	// Queue depth stats is a gauge - record multiple entries - last one should stick
 	expectSuccess(t, func() error { return r.ReportQueueDepth(1) })
 	expectSuccess(t, func() error { return r.ReportQueueDepth(2) })
 	expectSuccess(t, func() error { return r.ReportQueueDepth(3) })
-	checkLastValueData(t, "work_queue_depth", wantTags, 3)
+	metricstest.CheckLastValueData(t, "work_queue_depth", wantTags, 3)
 }
 
 func TestReportReconcile(t *testing.T) {
@@ -80,12 +82,12 @@ func TestReportReconcile(t *testing.T) {
 	}
 
 	expectSuccess(t, func() error { return r.ReportReconcile(10*time.Millisecond, "test/key", "true") })
-	checkCountData(t, "reconcile_count", wantTags, initialReconcileCount+1)
-	checkDistributionData(t, "reconcile_latency", wantTags, initialReconcileLatency+10)
+	metricstest.CheckCountData(t, "reconcile_count", wantTags, initialReconcileCount+1)
+	metricstest.CheckDistributionData(t, "reconcile_latency", wantTags, 1, initialReconcileLatency+10, initialReconcileLatency+10)
 
 	expectSuccess(t, func() error { return r.ReportReconcile(15*time.Millisecond, "test/key", "true") })
-	checkCountData(t, "reconcile_count", wantTags, initialReconcileCount+2)
-	checkDistributionData(t, "reconcile_latency", wantTags, initialReconcileLatency+25)
+	metricstest.CheckCountData(t, "reconcile_count", wantTags, initialReconcileCount+2)
+	metricstest.CheckDistributionData(t, "reconcile_latency", wantTags, 2, initialReconcileLatency+10, initialReconcileLatency+15)
 }
 
 func expectSuccess(t *testing.T, f func() error) {
@@ -94,69 +96,3 @@ func expectSuccess(t *testing.T, f func() error) {
 		t.Errorf("Reporter.Report() expected success but got error %v", err)
 	}
 }
-
-func checkLastValueData(t *testing.T, name string, wantTags map[string]string, wantValue float64) {
-	t.Helper()
-	if row := checkRow(t, name); row != nil {
-		checkTags(t, wantTags, row)
-		if s, ok := row.Data.(*view.LastValueData); !ok {
-			t.Error("Reporter.Report() expected a LastValueData type")
-		} else if s.Value != wantValue {
-			t.Errorf("Reporter.Report() expected %v got %v. metric: %v", s.Value, wantValue, name)
-		}
-	}
-}
-
-func checkCountData(t *testing.T, name string, wantTags map[string]string, wantValue int64) {
-	t.Helper()
-	row := checkRow(t, name)
-	if row == nil {
-		return
-	}
-
-	checkTags(t, wantTags, row)
-	if s, ok := row.Data.(*view.CountData); !ok {
-		t.Error("Reporter.Report() expected a LastValueData type")
-	} else if s.Value != wantValue {
-		t.Errorf("Reporter.Report() expected %v got %v. metric: %v", s.Value, (float64)(wantValue), name)
-	}
-}
-
-func checkDistributionData(t *testing.T, name string, wantTags map[string]string, wantValue float64) {
-	t.Helper()
-	row := checkRow(t, name)
-	if row == nil {
-		return
-	}
-
-	checkTags(t, wantTags, row)
-	if s, ok := row.Data.(*view.DistributionData); !ok {
-		t.Error("Reporter.Report() expected a LastValueData type")
-	} else if s.Sum() != wantValue {
-		t.Errorf("Reporter.Report() expected %v got %v. metric: %v", s.Sum(), wantValue, name)
-	}
-}
-
-func checkRow(t *testing.T, name string) *view.Row {
-	t.Helper()
-	d, err := view.RetrieveData(name)
-	if err != nil {
-		t.Fatalf("Reporter.Report() error = %v, wantErr %v", err, false)
-		return nil
-	}
-	if len(d) != 1 {
-		t.Errorf("Reporter.Report() len(d)=%v, want 1", len(d))
-	}
-	return d[0]
-}
-
-func checkTags(t *testing.T, wantTags map[string]string, row *view.Row) {
-	t.Helper()
-	for _, got := range row.Tags {
-		if want, ok := wantTags[got.Key.Name()]; !ok {
-			t.Errorf("Reporter.Report() got an extra tag %v: %v", got.Key.Name(), got.Value)
-		} else if got.Value != want {
-			t.Errorf("Reporter.Report() expected a different tag value. key:%v, got: %v, want: %v", got.Key.Name(), got.Value, want)
-		}
-	}
-}
diff --git a/kmp/reporters_test.go b/kmp/reporters_test.go
index 29e1b729b..2daaf42d7 100644
--- a/kmp/reporters_test.go
+++ b/kmp/reporters_test.go
@@ -31,7 +31,7 @@ type testStruct struct {
 	Omit        string      `json:"omit,omitempty"`
 	Ignore      string      `json:"-"`
 	Dash        string      `json:"-,"`
-	//lint:ignore SA5008 we actually want to test this broken json options work
+	//nolint:staticcheck // We actually want to test this broken json options work
 	MultiComma string `json:"multi,omitempty,somethingelse"`
 }
 
diff --git a/metrics/client_test.go b/metrics/client_test.go
index 2d847704f..36f05f7d3 100644
--- a/metrics/client_test.go
+++ b/metrics/client_test.go
@@ -58,7 +58,7 @@ func TestClientMetrics(t *testing.T) {
 	metrics.Register(cp.NewLatencyMetric(), cp.NewResultMetric())
 
 	// Reset the metrics configuration to avoid leaked state from other tests.
-	setCurMetricsConfig(nil)
+	InitForTesting()
 
 	views := cp.DefaultViews()
 	if got, want := len(views), 2; got != want {
diff --git a/metrics/memstats_test.go b/metrics/memstats_test.go
index 01fee7501..b7c7671e4 100644
--- a/metrics/memstats_test.go
+++ b/metrics/memstats_test.go
@@ -36,7 +36,7 @@ func TestMemStatsMetrics(t *testing.T) {
 	msp.Start(ctx, period)
 
 	// Reset the metrics configuration to avoid leaked state from other tests.
-	setCurMetricsConfig(nil)
+	InitForTesting()
 
 	views := msp.DefaultViews()
 	if got, want := len(views), 27; got != want {
diff --git a/metrics/record_test.go b/metrics/record_test.go
index 80c4d2f05..7a3568bc8 100644
--- a/metrics/record_test.go
+++ b/metrics/record_test.go
@@ -58,9 +58,6 @@ func TestRecordServing(t *testing.T) {
 			stackdriverMetricTypePrefix: "knative.dev/unsupported",
 		},
 		measurement: measure.M(3),
-	}, {
-		name:        "empty metricsConfig",
-		measurement: measure.M(4),
 	}}
 	testRecord(t, measure, shouldReportCases)
 }
@@ -87,9 +84,6 @@ func TestRecordEventing(t *testing.T) {
 			stackdriverMetricTypePrefix: "knative.dev/unsupported",
 		},
 		measurement: measure.M(3),
-	}, {
-		name:        "empty metricsConfig",
-		measurement: measure.M(4),
 	}}
 	testRecord(t, measure, shouldReportCases)
 }
@@ -149,6 +143,9 @@ func testRecord(t *testing.T, measure *stats.Int64Measure, shouldReportCases []c
 			},
 		},
 		measurement: measure.M(5),
+	}, {
+		name:        "empty metricsConfig",
+		measurement: measure.M(4),
 	}}
 
 	for _, test := range shouldNotReportCases {
diff --git a/metrics/testing.go b/metrics/testing.go
new file mode 100644
index 000000000..d33668ead
--- /dev/null
+++ b/metrics/testing.go
@@ -0,0 +1,26 @@
+/*
+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.
+*/
+
+package metrics
+
+// InitForTesting initialize the necessary global variables for unit tests.
+func InitForTesting() {
+	setCurMetricsConfig(&metricsConfig{
+		backendDestination: Prometheus,
+		component:          "test",
+		domain:             "test",
+	})
+}
diff --git a/metrics/testing/config.go b/metrics/testing/config.go
index a0932f005..2df88b6e0 100644
--- a/metrics/testing/config.go
+++ b/metrics/testing/config.go
@@ -24,4 +24,5 @@ import (
 
 func init() {
 	os.Setenv(metrics.DomainEnv, "knative.dev/testing")
+	metrics.InitForTesting()
 }
diff --git a/metrics/workqueue_test.go b/metrics/workqueue_test.go
index 6840a60ba..c08523965 100644
--- a/metrics/workqueue_test.go
+++ b/metrics/workqueue_test.go
@@ -48,7 +48,7 @@ func TestWorkqueueMetrics(t *testing.T) {
 	workqueue.SetProvider(wp)
 
 	// Reset the metrics configuration to avoid leaked state from other tests.
-	setCurMetricsConfig(nil)
+	InitForTesting()
 
 	views := wp.DefaultViews()
 	if got, want := len(views), 7; got != want {
diff --git a/network/network_test.go b/network/network_test.go
new file mode 100644
index 000000000..dc0eb2eb2
--- /dev/null
+++ b/network/network_test.go
@@ -0,0 +1,44 @@
+/*
+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
+
+    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 network
+
+import (
+	"net/http"
+	"testing"
+)
+
+func TestIsKubeletProbe(t *testing.T) {
+	req, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
+	if err != nil {
+		t.Fatal("Error building request:", err)
+	}
+	if IsKubeletProbe(req) {
+		t.Error("Not a kubelet probe but counted as such")
+	}
+	req.Header.Set("User-Agent", KubeProbeUAPrefix+"1.14")
+	if !IsKubeletProbe(req) {
+		t.Error("kubelet probe but not counted as such")
+	}
+	req.Header.Del("User-Agent")
+	if IsKubeletProbe(req) {
+		t.Error("Not a kubelet probe but counted as such")
+	}
+	req.Header.Set(KubeletProbeHeaderName, "no matter")
+	if !IsKubeletProbe(req) {
+		t.Error("kubelet probe but not counted as such")
+	}
+}
diff --git a/reconciler/reconcile_common_test.go b/reconciler/reconcile_common_test.go
new file mode 100644
index 000000000..1981e0815
--- /dev/null
+++ b/reconciler/reconcile_common_test.go
@@ -0,0 +1,96 @@
+/*
+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
+
+    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 reconciler
+
+import (
+	"context"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"knative.dev/pkg/apis"
+	duckv1 "knative.dev/pkg/apis/duck/v1"
+)
+
+func makeResource(topLevelCond string) *duckv1.KResource {
+	fooCond := apis.Condition{
+		Type:    "Foo",
+		Status:  corev1.ConditionTrue,
+		Message: "Something something foo",
+	}
+	readyCond := apis.Condition{
+		Type:    apis.ConditionType(topLevelCond),
+		Status:  corev1.ConditionTrue,
+		Message: "Something something bar",
+	}
+
+	return &duckv1.KResource{
+		ObjectMeta: metav1.ObjectMeta{
+			Generation: 42,
+		},
+
+		Status: duckv1.Status{
+			ObservedGeneration: 0,
+			Conditions:         duckv1.Conditions{fooCond, readyCond},
+		},
+	}
+}
+
+func TestPreProcessResetsReady(t *testing.T) {
+	testCases := []struct {
+		name                      string
+		initTopLevelCond          string
+		expectedTopLevelCondition apis.ConditionType
+	}{{
+		name:                      "top level Ready",
+		initTopLevelCond:          "Ready",
+		expectedTopLevelCondition: apis.ConditionReady,
+	}, {
+		name:                      "top level Succeeded",
+		initTopLevelCond:          "Succeeded",
+		expectedTopLevelCondition: apis.ConditionSucceeded,
+	}}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			resource := makeResource(test.initTopLevelCond)
+			krShape := duckv1.KRShaped(resource)
+
+			PreProcessReconcile(context.Background(), krShape)
+
+			if rc := resource.Status.GetCondition(test.expectedTopLevelCondition); rc.Status != "Unknown" {
+				t.Errorf("Expected unchanged ready status got=%s want=Unknown", rc.Status)
+			}
+		})
+	}
+}
+
+func TestPostProcessReconcileBumpsGeneration(t *testing.T) {
+	resource := makeResource("Ready")
+
+	krShape := duckv1.KRShaped(resource)
+	PostProcessReconcile(context.Background(), krShape)
+
+	if resource.Status.ObservedGeneration != resource.Generation {
+		t.Errorf("Expected observed generation bump got=%d want=%d", resource.Status.ObservedGeneration, resource.Generation)
+	}
+
+	if krShape.GetStatus().ObservedGeneration != krShape.GetGeneration() {
+		t.Errorf("Expected observed generation bump got=%d want=%d", resource.Status.ObservedGeneration, resource.Generation)
+	}
+}
diff --git a/source/source_stats_reporter_test.go b/source/source_stats_reporter_test.go
index ed3eb282c..79697eab6 100644
--- a/source/source_stats_reporter_test.go
+++ b/source/source_stats_reporter_test.go
@@ -22,6 +22,7 @@ import (
 
 	"knative.dev/pkg/metrics/metricskey"
 	"knative.dev/pkg/metrics/metricstest"
+	_ "knative.dev/pkg/metrics/testing"
 )
 
 func TestStatsReporter(t *testing.T) {
diff --git a/test/e2e_flags.go b/test/e2e_flags.go
index 3dfb2eb3d..bc6f90bff 100644
--- a/test/e2e_flags.go
+++ b/test/e2e_flags.go
@@ -32,7 +32,6 @@ import (
 )
 
 var (
-	klogFlags = flag.NewFlagSet("klog", flag.ExitOnError)
 	// Flags holds the command line flags or defaults for settings in the user's environment.
 	// See EnvironmentFlags for a list of supported fields.
 	Flags = initializeFlags()
diff --git a/test/ha/ha.go b/test/ha/ha.go
new file mode 100644
index 000000000..26326358f
--- /dev/null
+++ b/test/ha/ha.go
@@ -0,0 +1,45 @@
+/*
+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.
+*/
+
+package ha
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"knative.dev/pkg/test"
+	"knative.dev/pkg/test/logging"
+)
+
+// WaitForNewLeader waits until the holder of the given lease is different from the previousLeader.
+func WaitForNewLeader(client *test.KubeClient, lease, namespace, previousLeader string) (string, error) {
+	span := logging.GetEmitableSpan(context.Background(), "WaitForNewLeader/"+lease)
+	defer span.End()
+	var leader string
+	err := wait.PollImmediate(time.Second, time.Minute, func() (bool, error) {
+		lease, err := client.Kube.CoordinationV1().Leases(namespace).Get(lease, metav1.GetOptions{})
+		if err != nil {
+			return false, fmt.Errorf("error getting lease %s: %w", lease, err)
+		}
+		leader = strings.Split(*lease.Spec.HolderIdentity, "_")[0]
+		return leader != previousLeader, nil
+	})
+	return leader, err
+}
diff --git a/test/kube_checks.go b/test/kube_checks.go
index 35bcbd201..61a259ac9 100644
--- a/test/kube_checks.go
+++ b/test/kube_checks.go
@@ -25,8 +25,10 @@ import (
 	"strings"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
+	apierrs "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/wait"
 	k8styped "k8s.io/client-go/kubernetes/typed/core/v1"
@@ -93,6 +95,19 @@ func WaitForPodState(client *KubeClient, inState func(p *corev1.Pod) (bool, erro
 	})
 }
 
+// WaitForPodDeleted waits for the given pod to disappear from the given namespace.
+func WaitForPodDeleted(client *KubeClient, name, namespace string) error {
+	if err := WaitForPodState(client, func(p *corev1.Pod) (bool, error) {
+		// Always return false. We're oly interested in the error which indicates pod deletion or timeout.
+		return false, nil
+	}, name, namespace); err != nil {
+		if !apierrs.IsNotFound(err) {
+			return err
+		}
+	}
+	return nil
+}
+
 // WaitForServiceHasAtLeastOneEndpoint polls the status of the specified Service
 // from client every interval until number of service endpoints = numOfEndpoints
 func WaitForServiceEndpoints(client *KubeClient, svcName string, svcNamespace string, numOfEndpoints int) error {
@@ -121,6 +136,29 @@ func countEndpointsNum(e *corev1.Endpoints) int {
 	return num
 }
 
+// GetEndpointAddresses returns addresses of endpoints for the given service.
+func GetEndpointAddresses(client *KubeClient, svcName, svcNamespace string) ([]string, error) {
+	endpoints, err := client.Kube.CoreV1().Endpoints(svcNamespace).Get(svcName, metav1.GetOptions{})
+	if err != nil || countEndpointsNum(endpoints) == 0 {
+		return nil, fmt.Errorf("no endpoints or error: %w", err)
+	}
+	var hosts []string
+	for _, sub := range endpoints.Subsets {
+		for _, addr := range sub.Addresses {
+			hosts = append(hosts, addr.IP)
+		}
+	}
+	return hosts, nil
+}
+
+// WaitForChangedEndpoints waits until the endpoints for the given service differ from origEndpoints.
+func WaitForChangedEndpoints(client *KubeClient, svcName, svcNamespace string, origEndpoints []string) error {
+	return wait.PollImmediate(1*time.Second, 2*time.Minute, func() (bool, error) {
+		newEndpoints, err := GetEndpointAddresses(client, svcName, svcNamespace)
+		return !cmp.Equal(origEndpoints, newEndpoints), err
+	})
+}
+
 // GetConfigMap gets the configmaps for a given namespace
 func GetConfigMap(client *KubeClient, namespace string) k8styped.ConfigMapInterface {
 	return client.Kube.CoreV1().ConfigMaps(namespace)
@@ -176,3 +214,17 @@ func PodsRunning(podList *corev1.PodList) (bool, error) {
 func PodRunning(pod *corev1.Pod) bool {
 	return pod.Status.Phase == corev1.PodRunning || pod.Status.Phase == corev1.PodSucceeded
 }
+
+// WaitForDeploymentScale waits until the given deployment has the expected scale.
+func WaitForDeploymentScale(client *KubeClient, name, namespace string, scale int) error {
+	return WaitForDeploymentState(
+		client,
+		name,
+		func(d *appsv1.Deployment) (bool, error) {
+			return d.Status.ReadyReplicas == int32(scale), nil
+		},
+		"DeploymentIsScaled",
+		namespace,
+		time.Minute,
+	)
+}
diff --git a/test/logging/example_tlogger_test.go b/test/logging/example_tlogger_test.go
index 1395822fa..7c8660034 100644
--- a/test/logging/example_tlogger_test.go
+++ b/test/logging/example_tlogger_test.go
@@ -34,8 +34,7 @@ var (
 
 func init() { someStruct = testStruct{"hello", 42.0} }
 
-// godoc limitation; this would be named Test*(), of course
-
+// nolint:govet // godoc limitation; this would be named Test*(), of course
 func Example(legacy *testing.T) {
 	// Get our TLogger and ready the cleanup function
 	t, cancel := logging.NewTLogger(legacy)
diff --git a/test/webhook-apicoverage/resourcetree/test_util.go b/test/webhook-apicoverage/resourcetree/test_util.go
index 34fca0545..3b922d44a 100644
--- a/test/webhook-apicoverage/resourcetree/test_util.go
+++ b/test/webhook-apicoverage/resourcetree/test_util.go
@@ -49,11 +49,13 @@ type arrayType struct {
 	baseArr   []bool
 }
 
+//nolint:structcheck,unused // Disable warnings about unused fields.
 type otherType struct {
 	structMap map[string]baseType
 	baseMap   map[string]string
 }
 
+//nolint:structcheck,unused // Disable warnings about unused fields.
 type combinedNodeType struct {
 	b baseType
 	a arrayType
diff --git a/vendor/knative.dev/test-infra/scripts/e2e-tests.sh b/vendor/knative.dev/test-infra/scripts/e2e-tests.sh
index 917f5ebf9..01b2d0e40 100644
--- a/vendor/knative.dev/test-infra/scripts/e2e-tests.sh
+++ b/vendor/knative.dev/test-infra/scripts/e2e-tests.sh
@@ -167,11 +167,11 @@ function resolve_k8s_version() {
   local gke_versions=($(echo -n "${versions//;/ }"))
   echo "Available GKE versions in $2 are [${versions//;/, }]"
   if [[ "${target_version}" == "gke-latest" ]]; then
-    # Get first (latest) version, excluding the "-gke.#" suffix
+    # Get first (latest) version
     E2E_CLUSTER_VERSION="${gke_versions[0]}"
     echo "Using latest version, ${E2E_CLUSTER_VERSION}"
   else
-    local latest="$(echo "${gke_versions[@]}" | tr ' ' '\n' | grep -E ^${target_version} | cut -f1 -d- | sort | tail -1)"
+    local latest="$(echo "${gke_versions[@]}" | tr ' ' '\n' | grep -E ^${target_version} | sort -V | tail -1)"
     if [[ -z "${latest}" ]]; then
       echo "ERROR: version ${target_version} is not available"
       return 1
diff --git a/vendor/knative.dev/test-infra/scripts/library.sh b/vendor/knative.dev/test-infra/scripts/library.sh
index 34e9c6aa6..0b3927419 100644
--- a/vendor/knative.dev/test-infra/scripts/library.sh
+++ b/vendor/knative.dev/test-infra/scripts/library.sh
@@ -26,7 +26,7 @@ readonly SERVING_GKE_VERSION=gke-latest
 readonly SERVING_GKE_IMAGE=cos
 
 # Conveniently set GOPATH if unset
-if [[ -z "${GOPATH:-}" ]]; then
+if [[ ! -v GOPATH ]]; then
   export GOPATH="$(go env GOPATH)"
   if [[ -z "${GOPATH}" ]]; then
     echo "WARNING: GOPATH not set and go binary unable to provide it"
@@ -34,9 +34,9 @@ if [[ -z "${GOPATH:-}" ]]; then
 fi
 
 # Useful environment variables
-[[ -n "${PROW_JOB_ID:-}" ]] && IS_PROW=1 || IS_PROW=0
+[[ -v PROW_JOB_ID ]] && IS_PROW=1 || IS_PROW=0
 readonly IS_PROW
-[[ -z "${REPO_ROOT_DIR:-}" ]] && REPO_ROOT_DIR="$(git rev-parse --show-toplevel)"
+[[ ! -v REPO_ROOT_DIR ]] && REPO_ROOT_DIR="$(git rev-parse --show-toplevel)"
 readonly REPO_ROOT_DIR
 readonly REPO_NAME="$(basename ${REPO_ROOT_DIR})"
 
@@ -515,10 +515,7 @@ function start_latest_knative_eventing() {
 #             $3..$n - parameters passed to the tool.
 function run_go_tool() {
   local tool=$2
-  local action=get
-  [[ $1 =~ ^[\./].* ]] && action=install
-  # Avoid running `go get` from root dir of the repository, as it can change go.sum and go.mod files.
-  # See discussions in https://github.com/golang/go/issues/27643.
+  local install_failed=0
   if [[ -z "$(which ${tool})" ]]; then
     local action=get
     [[ $1 =~ ^[\./].* ]] && action=install
@@ -526,16 +523,15 @@ function run_go_tool() {
     # See discussions in https://github.com/golang/go/issues/27643.
     if [[ ${action} == "get" && $(pwd) == "${REPO_ROOT_DIR}" ]]; then
       local temp_dir="$(mktemp -d)"
-      local install_failed=0
       # Swallow the output as we are returning the stdout in the end.
       pushd "${temp_dir}" > /dev/null 2>&1
-      GOFLAGS="" go ${action} $1 || install_failed=1
+      GOFLAGS="" go ${action} "$1" || install_failed=1
       popd > /dev/null 2>&1
-      (( install_failed )) && return ${install_failed}
     else
-      GOFLAGS="" go ${action} $1
+      GOFLAGS="" go ${action} "$1" || install_failed=1
     fi
   fi
+  (( install_failed )) && return ${install_failed}
   shift 2
   ${tool} "$@"
 }
@@ -657,6 +653,21 @@ function get_canonical_path() {
   echo "$(cd ${path%/*} && echo $PWD/${path##*/})"
 }
 
+# List changed files in the current PR.
+# This is implemented as a function so it can be mocked in unit tests.
+# It will fail if a file name ever contained a newline character (which is bad practice anyway)
+function list_changed_files() {
+  if [[ -v PULL_BASE_SHA ]] && [[ -v PULL_PULL_SHA ]]; then
+    # Avoid warning when there are more than 1085 files renamed:
+    # https://stackoverflow.com/questions/7830728/warning-on-diff-renamelimit-variable-when-doing-git-push
+    git config diff.renames 0
+    git --no-pager diff --name-only ${PULL_BASE_SHA}..${PULL_PULL_SHA}
+  else
+    # Do our best if not running in Prow
+    git diff --name-only HEAD^
+  fi
+}
+
 # Returns the current branch.
 function current_branch() {
   local branch_name=""
@@ -699,6 +710,29 @@ function get_latest_knative_yaml_source() {
   echo "https://storage.googleapis.com/knative-nightly/${repo_name}/latest/${yaml_name}.yaml"
 }
 
+function shellcheck_new_files() {
+  declare -a array_of_files
+  local failed=0
+  readarray -t -d '\n' array_of_files < <(list_changed_files)
+  for filename in "${array_of_files[@]}"; do
+    if echo "${filename}" | grep -q "^vendor/"; then
+      continue
+    fi
+    if file "${filename}" | grep -q "shell script"; then
+      # SC1090 is "Can't follow non-constant source"; we will scan files individually
+      if shellcheck -e SC1090 "${filename}"; then
+        echo "--- PASS: shellcheck on ${filename}"
+      else
+        echo "--- FAIL: shellcheck on ${filename}"
+        failed=1
+      fi
+    fi
+  done
+  if [[ ${failed} -eq 1 ]]; then
+    fail_script "shellcheck failures"
+  fi
+}
+
 # Initializations that depend on previous functions.
 # These MUST come last.
 
diff --git a/vendor/knative.dev/test-infra/scripts/presubmit-tests.sh b/vendor/knative.dev/test-infra/scripts/presubmit-tests.sh
index 797f2fd3e..1435a3469 100644
--- a/vendor/knative.dev/test-infra/scripts/presubmit-tests.sh
+++ b/vendor/knative.dev/test-infra/scripts/presubmit-tests.sh
@@ -46,15 +46,6 @@ function pr_only_contains() {
   [[ -z "$(echo "${CHANGED_FILES}" | grep -v "\(${1// /\\|}\)$")" ]]
 }
 
-# List changed files in the current PR.
-# This is implemented as a function so it can be mocked in unit tests.
-function list_changed_files() {
-  # Avoid warning when there are more than 1085 files renamed:
-  # https://stackoverflow.com/questions/7830728/warning-on-diff-renamelimit-variable-when-doing-git-push
-  git config diff.renames 0
-  git --no-pager diff --name-only ${PULL_BASE_SHA}..${PULL_SHA}
-}
-
 # Initialize flags and context for presubmit tests:
 # CHANGED_FILES, IS_PRESUBMIT_EXEMPT_PR and IS_DOCUMENTATION_PR.
 function initialize_environment() {
@@ -311,6 +302,8 @@ function main() {
     kubectl version --client
     echo ">> go version"
     go version
+    echo ">> go env"
+    go env
     echo ">> python3 version"
     python3 --version
     echo ">> git version"
diff --git a/vendor/knative.dev/test-infra/scripts/shellcheck-presubmit.sh b/vendor/knative.dev/test-infra/scripts/shellcheck-presubmit.sh
new file mode 100644
index 000000000..ce1515bd3
--- /dev/null
+++ b/vendor/knative.dev/test-infra/scripts/shellcheck-presubmit.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+set -e
+
+source "$(dirname "${BASH_SOURCE[0]}")/library.sh"
+
+if (( ! IS_PROW )); then
+  echo "Local run of shellcheck-presubmit detected"
+  echo "This notably DOES NOT ACT LIKE THE GITHUB PRESUBMIT"
+  echo "The Github presubmit job only runs shellcheck on files you touch"
+  echo "There's no way to locally determine which files you touched:"
+  echo " as git is a distributed VCS, there is no notion of parent until merge"
+  echo " is attempted."
+  echo "So it checks the current content of all files changed in the previous commit"
+  echo " and/or currently staged."
+fi
+
+shellcheck_new_files
diff --git a/webhook/admission_integration_test.go b/webhook/admission_integration_test.go
index f938dfc59..4f741c408 100644
--- a/webhook/admission_integration_test.go
+++ b/webhook/admission_integration_test.go
@@ -34,6 +34,7 @@ import (
 	authenticationv1 "k8s.io/api/authentication/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"knative.dev/pkg/metrics/metricstest"
+	_ "knative.dev/pkg/metrics/testing"
 )
 
 type fixedAdmissionController struct {
diff --git a/webhook/stats_reporter_test.go b/webhook/stats_reporter_test.go
index 4250f6928..72786b49c 100644
--- a/webhook/stats_reporter_test.go
+++ b/webhook/stats_reporter_test.go
@@ -24,6 +24,7 @@ import (
 	admissionv1beta1 "k8s.io/api/admission/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"knative.dev/pkg/metrics/metricstest"
+	_ "knative.dev/pkg/metrics/testing"
 )
 
 func TestWebhookStatsReporter(t *testing.T) {
diff --git a/webhook/webhook_test.go b/webhook/webhook_test.go
index 3f2b29ed5..bcdef62d9 100644
--- a/webhook/webhook_test.go
+++ b/webhook/webhook_test.go
@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"net"
 	"testing"
+	"time"
 
 	"golang.org/x/sync/errgroup"
 	"knative.dev/pkg/controller"
@@ -44,6 +45,11 @@ const (
 	user1            = "brutto@knative.dev"
 )
 
+func init() {
+	// Don't hang forever when running tests.
+	GracePeriod = 100 * time.Millisecond
+}
+
 func newNonRunningTestWebhook(t *testing.T, options Options, acs ...interface{}) (
 	ctx context.Context, ac *Webhook, cancel context.CancelFunc) {
 	t.Helper()