Repository: knative/pkg
Pull request #2350 (2021-11-16 03:52:30)

codegen - support multiple annotation keys
- allow multiple values for comment tag keys
- add OR filter chain
- update codegen to support multiple annotation keys


Part of: https://github.com/knative/serving/issues/12103

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

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/2350#" title="Author self-approved">dprotaso</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative%2Fpkg).

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

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

- ~~[OWNERS](https://github.com/knative/pkg/blob/main/OWNERS)~~ [dprotaso]

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":[]} -->
dprotaso: /hold will test downstream with knative.dev/networking
knative-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/2350#" title="Author self-approved">dprotaso</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative%2Fpkg).

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

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

- ~~[codegen/OWNERS](https://github.com/knative/pkg/blob/main/codegen/OWNERS)~~ [dprotaso]
- ~~[reconciler/OWNERS](https://github.com/knative/pkg/blob/main/reconciler/OWNERS)~~ [dprotaso]

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":[]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/knative/pkg/pull/2350?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) Report
> Merging [#2350](https://codecov.io/gh/knative/pkg/pull/2350?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) (034d3f9) into [main](https://codecov.io/gh/knative/pkg/commit/0b0c3390a475ce44e4a285b9365e4056f3f484f0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) (0b0c339) will **decrease** coverage by `0.11%`.
> The diff coverage is `33.33%`.

> :exclamation: Current head 034d3f9 differs from pull request most recent head 92bae80. Consider uploading reports for the commit 92bae80 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/knative/pkg/pull/2350/graphs/tree.svg?width=650&height=150&src=pr&token=rlqpDnfxbO&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative)](https://codecov.io/gh/knative/pkg/pull/2350?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative)

```diff
@@            Coverage Diff             @@
##             main    #2350      +/-   ##
==========================================
- Coverage   64.52%   64.40%   -0.12%     
==========================================
  Files         226      226              
  Lines        9668     9686      +18     
==========================================
  Hits         6238     6238              
- Misses       3154     3172      +18     
  Partials      276      276              
```


| [Impacted Files](https://codecov.io/gh/knative/pkg/pull/2350?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) | Coverage Δ | |
|---|---|---|
| [codegen/cmd/injection-gen/generators/packages.go](https://codecov.io/gh/knative/pkg/pull/2350/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-Y29kZWdlbi9jbWQvaW5qZWN0aW9uLWdlbi9nZW5lcmF0b3JzL3BhY2thZ2VzLmdv) | `0.00% <0.00%> (ø)` | |
| [.../injection-gen/generators/reconciler\_controller.go](https://codecov.io/gh/knative/pkg/pull/2350/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-Y29kZWdlbi9jbWQvaW5qZWN0aW9uLWdlbi9nZW5lcmF0b3JzL3JlY29uY2lsZXJfY29udHJvbGxlci5nbw==) | `0.00% <0.00%> (ø)` | |
| [...ction-gen/generators/reconciler\_controller\_stub.go](https://codecov.io/gh/knative/pkg/pull/2350/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-Y29kZWdlbi9jbWQvaW5qZWN0aW9uLWdlbi9nZW5lcmF0b3JzL3JlY29uY2lsZXJfY29udHJvbGxlcl9zdHViLmdv) | `0.00% <0.00%> (ø)` | |
| [.../injection-gen/generators/reconciler\_reconciler.go](https://codecov.io/gh/knative/pkg/pull/2350/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-Y29kZWdlbi9jbWQvaW5qZWN0aW9uLWdlbi9nZW5lcmF0b3JzL3JlY29uY2lsZXJfcmVjb25jaWxlci5nbw==) | `0.00% <0.00%> (ø)` | |
| [reconciler/filter.go](https://codecov.io/gh/knative/pkg/pull/2350/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-cmVjb25jaWxlci9maWx0ZXIuZ28=) | `88.88% <0.00%> (-11.12%)` | :arrow_down: |
| [...gen/cmd/injection-gen/generators/comment\_parser.go](https://codecov.io/gh/knative/pkg/pull/2350/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-Y29kZWdlbi9jbWQvaW5qZWN0aW9uLWdlbi9nZW5lcmF0b3JzL2NvbW1lbnRfcGFyc2VyLmdv) | `100.00% <100.00%> (ø)` | |
| [controller/controller.go](https://codecov.io/gh/knative/pkg/pull/2350/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-Y29udHJvbGxlci9jb250cm9sbGVyLmdv) | `86.13% <0.00%> (-0.37%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/knative/pkg/pull/2350?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/knative/pkg/pull/2350?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative). Last update [0b0c339...92bae80](https://codecov.io/gh/knative/pkg/pull/2350?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative).

knative-prow-robot: @dprotaso: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-knative-pkg-unit-tests | 6d9abc291134e353129164693dcccc9144c29ef7 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/2350/pull-knative-pkg-unit-tests/1460651721784037376) | true | `/test pull-knative-pkg-unit-tests`

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

<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 -->
dprotaso: /retest metrics flake
knative-prow-robot: @dprotaso: The `/retest` command does not accept any targets.
The following commands are available to trigger required jobs:
* `/test pull-knative-pkg-build-tests`
* `/test pull-knative-pkg-integration-tests`
* `/test pull-knative-pkg-unit-tests`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/knative/pkg/pull/2350#issuecomment-970478096):

>/retest metrics flake


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
dprotaso: /retest
dprotaso: /assign @n3wscott @nak3 
/hold cancel

Looks like networking => net-* is good and things build properly

Patch:
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index f7c82a9b7..5aa770ebf 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -5,6 +5,7 @@ aliases:
   api-core-wg-leads:
   - dprotaso
   autoscaling-reviewers:
+  - nader-ziada
   - psschwei
   - taragu
   autoscaling-wg-leads:
@@ -39,11 +40,16 @@ aliases:
   - omerbensaadon
   - pmbanugo
   - snneji
-  docs-wg-leads: []
+  docs-wg-leads:
+  - csantanapr
+  - omerbensaadon
+  - snneji
   docs-writers:
   - RichardJJG
   - abrennan89
   - carieshmarie
+  - csantanapr
+  - omerbensaadon
   - richieescarez
   - snneji
   eventing-reviewers:
@@ -182,6 +188,7 @@ aliases:
   serving-reviewers:
   - carlisia
   - julz
+  - nader-ziada
   - nealhu
   - psschwei
   - whaught
@@ -212,6 +219,8 @@ aliases:
   ux-wg-leads:
   - csantanapr
   - omerbensaadon
+  - snneji
   ux-writers:
   - csantanapr
   - omerbensaadon
+  - snneji
diff --git a/codegen/cmd/injection-gen/generators/comment_parser.go b/codegen/cmd/injection-gen/generators/comment_parser.go
index ada0dff3e..68ac8b4cb 100644
--- a/codegen/cmd/injection-gen/generators/comment_parser.go
+++ b/codegen/cmd/injection-gen/generators/comment_parser.go
@@ -19,25 +19,41 @@ import "strings"
 
 // Adapted from the k8s.io comment parser https://github.com/kubernetes/gengo/blob/master/types/comments.go
 
+// CommentsTags maps marker prefixes to a set of tags containing keys and values
+type CommentTags map[string]CommentTag
+
+// CommentTags maps keys to a list of values
+type CommentTag map[string][]string
+
 // ExtractCommentTags parses comments for lines of the form:
 //
-//   'marker' + ':' "key=value,key2=value2".
+//   "marker" + "prefix" + ':' + "key=value,key2=value2".
 //
-// Values are optional; empty map is the default.  A tag can be specified more than
+//  In the following example the marker is '+' and the prefix is 'foo':
+//   +foo:key=value1,key2=value2,key=value3
+//
+// Values are optional; empty map is the default. A tag can be specified more than
 // one time and all values are returned.  If the resulting map has an entry for
 // a key, the value (a slice) is guaranteed to have at least 1 element.
 //
 // Example: if you pass "+" for 'marker', and the following lines are in
 // the comments:
-//   +foo:key=value1,key2=value2
+//   +foo:key=value1,key2=value2,key=value3
 //   +bar
 //
 // Then this function will return:
-//   map[string]map[string]string{"foo":{"key":value1","key2":"value2"}, "bar": nil}
+//   map[string]map[string]string{
+//     "foo":{
+//      "key":  []string{"value1", "value3"},
+//      "key2": []string{"value2"}
+//     },
+//     "bar": {},
+//  }
 //
 // Users are not expected to repeat values.
-func ExtractCommentTags(marker string, lines []string) map[string]map[string]string {
-	out := map[string]map[string]string{}
+func ExtractCommentTags(marker string, lines []string) CommentTags {
+	out := CommentTags{}
+
 	for _, line := range lines {
 		line = strings.TrimSpace(line)
 		if len(line) == 0 || !strings.HasPrefix(line, marker) {
@@ -45,29 +61,32 @@ func ExtractCommentTags(marker string, lines []string) map[string]map[string]str
 		}
 
 		options := strings.SplitN(line[len(marker):], ":", 2)
+		prefix := options[0]
+
 		if len(options) == 2 {
 			vals := strings.Split(options[1], ",")
 
-			opts := out[options[0]]
+			opts := out[prefix]
 			if opts == nil {
-				opts = make(map[string]string, len(vals))
+				opts = make(CommentTag, len(vals))
+				out[prefix] = opts
 			}
 
 			for _, pair := range vals {
-				if kv := strings.SplitN(pair, "=", 2); len(kv) == 2 {
-					opts[kv[0]] = kv[1]
-				} else if kv[0] != "" {
-					opts[kv[0]] = ""
+				kv := strings.SplitN(pair, "=", 2)
+				if len(kv) == 1 && kv[0] == "" {
+					continue
+				}
+				if _, ok := opts[kv[0]]; !ok {
+					opts[kv[0]] = []string{}
+				}
+				if len(kv) == 2 {
+					opts[kv[0]] = append(opts[kv[0]], kv[1])
 				}
-			}
-			if len(opts) == 0 {
-				out[options[0]] = nil
-			} else {
-				out[options[0]] = opts
 			}
 		} else if len(options) == 1 && options[0] != "" {
-			if _, has := out[options[0]]; !has {
-				out[options[0]] = nil
+			if _, has := out[prefix]; !has {
+				out[prefix] = CommentTag{}
 			}
 		}
 	}
diff --git a/codegen/cmd/injection-gen/generators/packages.go b/codegen/cmd/injection-gen/generators/packages.go
index 1900802fa..f79339824 100644
--- a/codegen/cmd/injection-gen/generators/packages.go
+++ b/codegen/cmd/injection-gen/generators/packages.go
@@ -193,29 +193,36 @@ func MustParseClientGenTags(lines []string) Tags {
 	return ret
 }
 
-func extractCommentTags(t *types.Type) map[string]map[string]string {
+func extractCommentTags(t *types.Type) CommentTags {
 	comments := append(append([]string{}, t.SecondClosestCommentLines...), t.CommentLines...)
 	return ExtractCommentTags("+", comments)
 }
 
-func extractReconcilerClassTag(tags map[string]map[string]string) (string, bool) {
+func extractReconcilerClassesTag(tags CommentTags) ([]string, bool) {
 	vals, ok := tags["genreconciler"]
 	if !ok {
-		return "", false
+		return nil, false
 	}
-	classname, has := vals["class"]
-	return classname, has
+	classnames, has := vals["class"]
+	if has && len(classnames) == 0 {
+		return nil, false
+	}
+	return classnames, has
 }
 
-func isKRShaped(tags map[string]map[string]string) bool {
+func isKRShaped(tags CommentTags) bool {
 	vals, has := tags["genreconciler"]
 	if !has {
 		return false
 	}
-	return vals["krshapedlogic"] != "false"
+	stringVals, has := vals["krshapedlogic"]
+	if !has || len(vals) == 0 {
+		return true // Default is true
+	}
+	return stringVals[0] != "false"
 }
 
-func isNonNamespaced(tags map[string]map[string]string) bool {
+func isNonNamespaced(tags CommentTags) bool {
 	vals, has := tags["genclient"]
 	if !has {
 		return false
@@ -224,7 +231,7 @@ func isNonNamespaced(tags map[string]map[string]string) bool {
 	return has
 }
 
-func stubs(tags map[string]map[string]string) bool {
+func stubs(tags CommentTags) bool {
 	vals, has := tags["genreconciler"]
 	if !has {
 		return false
@@ -545,7 +552,7 @@ func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentyp
 		// Fix for golang iterator bug.
 		t := t
 		extracted := extractCommentTags(t)
-		reconcilerClass, hasReconcilerClass := extractReconcilerClassTag(extracted)
+		reconcilerClasses, hasReconcilerClass := extractReconcilerClassesTag(extracted)
 		nonNamespaced := isNonNamespaced(extracted)
 		isKRShaped := isKRShaped(extracted)
 		stubs := stubs(extracted)
@@ -573,7 +580,7 @@ func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentyp
 					clientPkg:           clientPackagePath,
 					informerPackagePath: informerPackagePath,
 					schemePkg:           filepath.Join(customArgs.VersionedClientSetPackage, "scheme"),
-					reconcilerClass:     reconcilerClass,
+					reconcilerClasses:   reconcilerClasses,
 					hasReconcilerClass:  hasReconcilerClass,
 					hasStatus:           hasStatus(t),
 				})
@@ -602,7 +609,7 @@ func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentyp
 						outputPackage:       filepath.Join(packagePath, "stub"),
 						imports:             generator.NewImportTracker(),
 						informerPackagePath: informerPackagePath,
-						reconcilerClass:     reconcilerClass,
+						reconcilerClasses:   reconcilerClasses,
 						hasReconcilerClass:  hasReconcilerClass,
 					})
 					return generators
@@ -633,7 +640,7 @@ func reconcilerPackages(basePackage string, groupPkgName string, gv clientgentyp
 					listerPkg:          listerPackagePath,
 					groupGoName:        groupGoName,
 					groupVersion:       gv,
-					reconcilerClass:    reconcilerClass,
+					reconcilerClasses:  reconcilerClasses,
 					hasReconcilerClass: hasReconcilerClass,
 					nonNamespaced:      nonNamespaced,
 					isKRShaped:         isKRShaped,
diff --git a/codegen/cmd/injection-gen/generators/reconciler_controller.go b/codegen/cmd/injection-gen/generators/reconciler_controller.go
index a624275a5..63556dec6 100644
--- a/codegen/cmd/injection-gen/generators/reconciler_controller.go
+++ b/codegen/cmd/injection-gen/generators/reconciler_controller.go
@@ -38,7 +38,7 @@ type reconcilerControllerGenerator struct {
 	schemePkg           string
 	informerPackagePath string
 
-	reconcilerClass    string
+	reconcilerClasses  []string
 	hasReconcilerClass bool
 	hasStatus          bool
 }
@@ -69,7 +69,7 @@ func (g *reconcilerControllerGenerator) GenerateType(c *generator.Context, t *ty
 	m := map[string]interface{}{
 		"type":      t,
 		"group":     g.groupName,
-		"class":     g.reconcilerClass,
+		"classes":   g.reconcilerClasses,
 		"hasClass":  g.hasReconcilerClass,
 		"hasStatus": g.hasStatus,
 		"controllerImpl": c.Universe.Type(types.Name{
@@ -195,12 +195,24 @@ var reconcilerControllerNewImpl = `
 const (
 	defaultControllerAgentName = "{{.type|lowercaseSingular}}-controller"
 	defaultFinalizerName       = "{{.type|allLowercasePlural}}.{{.group}}"
-	{{if .hasClass}}
+	{{if .hasClass }}
 	// ClassAnnotationKey points to the annotation for the class of this resource.
-	ClassAnnotationKey = "{{ .class }}"
+	{{if gt (.classes | len) 1 }}// Deprecated: Use ClassAnnotationKeys given multiple keys exist
+	{{end -}}
+	ClassAnnotationKey = "{{ index .classes 0 }}"
 	{{end}}
 )
 
+{{if gt (.classes | len) 1 }}
+var (
+	// ClassAnnotationKeys points to the annotation for the class of this resource.
+	ClassAnnotationKeys = []string{
+		{{range $class := .classes}}"{{$class}}",
+		{{end}}
+	}
+)
+{{end}}
+
 // NewImpl returns a {{.controllerImpl|raw}} that handles queuing and feeding work from
 // the queue through an implementation of {{.controllerReconciler|raw}}, delegating to
 // the provided Interface and optional Finalizer methods. OptionsFn is used to return
diff --git a/codegen/cmd/injection-gen/generators/reconciler_controller_stub.go b/codegen/cmd/injection-gen/generators/reconciler_controller_stub.go
index 37db246c3..13dff4d51 100644
--- a/codegen/cmd/injection-gen/generators/reconciler_controller_stub.go
+++ b/codegen/cmd/injection-gen/generators/reconciler_controller_stub.go
@@ -35,7 +35,7 @@ type reconcilerControllerStubGenerator struct {
 
 	reconcilerPkg       string
 	informerPackagePath string
-	reconcilerClass     string
+	reconcilerClasses   []string
 	hasReconcilerClass  bool
 }
 
@@ -64,7 +64,7 @@ func (g *reconcilerControllerStubGenerator) GenerateType(c *generator.Context, t
 
 	m := map[string]interface{}{
 		"type":     t,
-		"class":    g.reconcilerClass,
+		"classes":  g.reconcilerClasses,
 		"hasClass": g.hasReconcilerClass,
 		"informerGet": c.Universe.Function(types.Name{
 			Package: g.informerPackagePath,
@@ -91,10 +91,18 @@ func (g *reconcilerControllerStubGenerator) GenerateType(c *generator.Context, t
 			Package: g.reconcilerPkg,
 			Name:    "ClassAnnotationKey",
 		}),
+		"classAnnotationKeys": c.Universe.Variable(types.Name{
+			Package: g.reconcilerPkg,
+			Name:    "ClassAnnotationKeys",
+		}),
 		"annotationFilterFunc": c.Universe.Function(types.Name{
 			Package: "knative.dev/pkg/reconciler",
 			Name:    "AnnotationFilterFunc",
 		}),
+		"orFilterFunc": c.Universe.Function(types.Name{
+			Package: "knative.dev/pkg/reconciler",
+			Name:    "Or",
+		}),
 		"filterHandler": c.Universe.Type(types.Name{
 			Package: "k8s.io/client-go/tools/cache",
 			Name:    "FilteringResourceEventHandler",
@@ -120,7 +128,17 @@ func NewController(
 
 	{{if .hasClass}}
 	classValue := "default" // TODO: update this to the appropriate value.
+	{{if len .classes | eq 1 }}
 	classFilter := {{.annotationFilterFunc|raw}}({{.classAnnotationKey|raw}}, classValue, false /*allowUnset*/)
+	{{else if gt (len .classes) 1 }}
+	filterFuncs := make([]func(interface{}) bool, 0, len({{.classAnnotationKeys|raw}}))
+
+	for _, key := range {{.classAnnotationKeys|raw}} {
+		filterFuncs = append(filterFuncs, {{.annotationFilterFunc|raw}}(key, classValue, false /*allowUnset*/))
+	}
+
+	classFilter := {{.orFilterFunc|raw}}(filterFuncs...)
+	{{end}}
 	{{end}}
 
 	// TODO: setup additional informers here.
diff --git a/codegen/cmd/injection-gen/generators/reconciler_reconciler.go b/codegen/cmd/injection-gen/generators/reconciler_reconciler.go
index e787096b3..5e3f23faf 100644
--- a/codegen/cmd/injection-gen/generators/reconciler_reconciler.go
+++ b/codegen/cmd/injection-gen/generators/reconciler_reconciler.go
@@ -37,7 +37,7 @@ type reconcilerReconcilerGenerator struct {
 	listerName     string
 	listerPkg      string
 
-	reconcilerClass    string
+	reconcilerClasses  []string
 	hasReconcilerClass bool
 	nonNamespaced      bool
 	isKRShaped         bool
@@ -74,7 +74,7 @@ func (g *reconcilerReconcilerGenerator) GenerateType(c *generator.Context, t *ty
 		"type":          t,
 		"group":         namer.IC(g.groupGoName),
 		"version":       namer.IC(g.groupVersion.Version.String()),
-		"class":         g.reconcilerClass,
+		"classes":       g.reconcilerClasses,
 		"hasClass":      g.hasReconcilerClass,
 		"isKRShaped":    g.isKRShaped,
 		"hasStatus":     g.hasStatus,
@@ -219,6 +219,9 @@ func (g *reconcilerReconcilerGenerator) GenerateType(c *generator.Context, t *ty
 	sw.Do(reconcilerInterfaceFactory, m)
 	sw.Do(reconcilerNewReconciler, m)
 	sw.Do(reconcilerImplFactory, m)
+	if len(g.reconcilerClasses) > 1 {
+		sw.Do(reconcilerLookupClass, m)
+	}
 	if g.hasStatus {
 		sw.Do(reconcilerStatusFactory, m)
 	}
@@ -227,6 +230,17 @@ func (g *reconcilerReconcilerGenerator) GenerateType(c *generator.Context, t *ty
 	return sw.Error()
 }
 
+var reconcilerLookupClass = `
+func lookupClass(annotations map[string]string) (string, bool) {
+	for _, key := range ClassAnnotationKeys {
+		 if val, ok := annotations[key]; ok {
+		   return val, true
+		 }
+	}
+	return "", false
+}
+`
+
 var reconcilerInterfaceFactory = `
 // Interface defines the strongly typed interfaces to be implemented by a
 // controller reconciling {{.type|raw}}.
@@ -293,8 +307,15 @@ type reconcilerImpl struct {
 	skipStatusUpdates bool
 	{{end}}
 
-	{{if .hasClass}}
-	// classValue is the resource annotation[{{ .class }}] instance value this reconciler instance filters on.
+	{{if len .classes | eq 1 }}
+	// classValue is the resource annotation[{{ index .classes 0 }}] instance value this reconciler instance filters on.
+	classValue string
+	{{else if gt (len .classes) 1 }}
+	// classValue is the resource annotation instance value this reconciler instance filters on.
+	// The annotations key are:
+	{{- range $class := .classes}}
+	//   {{$class}}
+	{{- end}}
 	classValue string
 	{{end}}
 }
@@ -416,14 +437,22 @@ func (r *reconcilerImpl) Reconcile(ctx {{.contextContext|raw}}, key string) erro
 	} else if err != nil {
 		return err
 	}
-	{{if .hasClass}}
+
+{{if len .classes | eq 1 }}
 	if classValue, found := original.GetAnnotations()[ClassAnnotationKey]; !found || classValue != r.classValue {
 		logger.Debugw("Skip reconciling resource, class annotation value does not match reconciler instance value.",
 			zap.String("classKey", ClassAnnotationKey),
 			zap.String("issue", classValue+"!="+r.classValue))
 		return nil
 	}
-	{{end}}
+{{else if gt (len .classes) 1 }}
+	if classValue, found := lookupClass(original.GetAnnotations()); !found || classValue != r.classValue {
+		logger.Debugw("Skip reconciling resource, class annotation value does not match reconciler instance value.",
+			zap.Strings("classKeys", ClassAnnotationKeys),
+			zap.String("issue", classValue+"!="+r.classValue))
+		return nil
+	}
+{{end}}
 
 	// Don't modify the informers copy.
 	resource := original.DeepCopy()
diff --git a/injection/interface.go b/injection/interface.go
index 433728298..158864015 100644
--- a/injection/interface.go
+++ b/injection/interface.go
@@ -103,6 +103,14 @@ type DynamicInterface interface {
 
 type ControllerConstructor func(context.Context, configmap.Watcher) *controller.Impl
 
+// NamedControllerConstructor is a ControllerConstructor with an associated name.
+type NamedControllerConstructor struct {
+	// Name is the name associated with the controller returned by ControllerConstructor.
+	Name string
+	// ControllerConstructor is a constructor for a controller.
+	ControllerConstructor ControllerConstructor
+}
+
 var (
 	// Check that impl implements Interface
 	_ Interface = (*impl)(nil)
diff --git a/injection/sharedmain/main.go b/injection/sharedmain/main.go
index 8cc6fb195..b2a1d5117 100644
--- a/injection/sharedmain/main.go
+++ b/injection/sharedmain/main.go
@@ -26,6 +26,8 @@ import (
 	"os"
 	"time"
 
+	"github.com/spf13/pflag"
+
 	"go.uber.org/automaxprocs/maxprocs" // automatically set GOMAXPROCS based on cgroups
 	"go.uber.org/zap"
 	"golang.org/x/sync/errgroup"
@@ -34,6 +36,7 @@ import (
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/rest"
 
@@ -111,6 +114,43 @@ var (
 	WebhookMainWithConfig  = MainWithConfig
 )
 
+// MainNamed runs the generic main flow for controllers and webhooks.
+//
+// In addition to the MainWithConfig flow, it defines a `disabled-controllers` flag that allows disabling controllers
+// by name.
+func MainNamed(ctx context.Context, component string, ctors ...injection.NamedControllerConstructor) {
+
+	disabledControllers := pflag.StringSlice("disable-controllers", []string{}, "Comma-separated list of disabled controllers.")
+
+	// HACK: This parses flags, so the above should be set once this runs.
+	cfg := injection.ParseAndGetRESTConfigOrDie()
+
+	enabledCtors := enabledControllers(*disabledControllers, ctors)
+
+	MainWithConfig(ctx, component, cfg, toControllerConstructors(enabledCtors)...)
+}
+
+func enabledControllers(disabledControllers []string, ctors []injection.NamedControllerConstructor) []injection.NamedControllerConstructor {
+	disabledControllersSet := sets.NewString(disabledControllers...)
+	activeCtors := make([]injection.NamedControllerConstructor, 0, len(ctors))
+	for _, ctor := range ctors {
+		if disabledControllersSet.Has(ctor.Name) {
+			log.Printf("Disabling controller %s", ctor.Name)
+			continue
+		}
+		activeCtors = append(activeCtors, ctor)
+	}
+	return activeCtors
+}
+
+func toControllerConstructors(namedCtors []injection.NamedControllerConstructor) []injection.ControllerConstructor {
+	ctors := make([]injection.ControllerConstructor, 0, len(namedCtors))
+	for _, ctor := range namedCtors {
+		ctors = append(ctors, ctor.ControllerConstructor)
+	}
+	return ctors
+}
+
 // MainWithContext runs the generic main flow for controllers and
 // webhooks. Use MainWithContext if you do not need to serve webhooks.
 func MainWithContext(ctx context.Context, component string, ctors ...injection.ControllerConstructor) {
diff --git a/reconciler/filter.go b/reconciler/filter.go
index 36c86c4d3..9f7d2fa36 100644
--- a/reconciler/filter.go
+++ b/reconciler/filter.go
@@ -77,6 +77,18 @@ func Not(f func(interface{}) bool) func(interface{}) bool {
 	}
 }
 
+// Or creates a FilterFunc which performs an OR of the passed in FilterFuncs
+func Or(funcs ...func(interface{}) bool) func(interface{}) bool {
+	return func(obj interface{}) bool {
+		for _, f := range funcs {
+			if f(obj) {
+				return true
+			}
+		}
+		return false
+	}
+}
+
 // ChainFilterFuncs creates a FilterFunc which performs an AND of the passed FilterFuncs.
 func ChainFilterFuncs(funcs ...func(interface{}) bool) func(interface{}) bool {
 	return func(obj interface{}) bool {


Test patch:
diff --git a/codegen/cmd/injection-gen/generators/comment_parser_test.go b/codegen/cmd/injection-gen/generators/comment_parser_test.go
index e45dd3004..bbe5fab45 100644
--- a/codegen/cmd/injection-gen/generators/comment_parser_test.go
+++ b/codegen/cmd/injection-gen/generators/comment_parser_test.go
@@ -15,7 +15,11 @@ limitations under the License.
 */
 package generators
 
-import "testing"
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
 
 func TestParseComments(t *testing.T) {
 	comment := []string{
@@ -26,28 +30,31 @@ func TestParseComments(t *testing.T) {
 		"+with:option",
 		"+pair:key=value",
 		"+manypairs:key1=value1,key2=value2",
+		"+duplicate:key1=value1,key1=value2",
 	}
 
 	extracted := ExtractCommentTags("+", comment)
 
-	if val, ok := extracted["foo"]; !ok || val != nil {
-		t.Errorf("Failed to extract single key got=%t,%v want=true,nil", ok, val)
-	}
-
-	if val, ok := extracted["bar"]; !ok || val != nil {
-		t.Errorf("Failed to extract single key got=%t,%v want=true,nil", ok, val)
+	expected := CommentTags{
+		"foo": {},
+		"bar": {},
+		"with": {
+			"option": {},
+		},
+		"pair": {
+			"key": []string{"value"},
+		},
+		"manypairs": {
+			"key1": []string{"value1"},
+			"key2": []string{"value2"},
+		},
+		"duplicate": {
+			"key1": []string{"value1", "value2"},
+		},
 	}
 
-	if val, ok := extracted["with"]; !ok || val["option"] != "" {
-		t.Errorf(`Failed to extract single key got=%t,%v want=true,{"option":""}`, ok, val)
-	}
-
-	if val, ok := extracted["pair"]; !ok || val["key"] != "value" {
-		t.Errorf(`Failed to extract single key got=%t,%v want=true,{"key":"value"}`, ok, val)
-	}
-
-	if val, ok := extracted["manypairs"]; !ok || val["key1"] != "value1" || val["key2"] != "value2" {
-		t.Errorf(`Failed to extract single key got=%t,%v want=true,{"key":"value"}`, ok, val)
+	if diff := cmp.Diff(expected, extracted); diff != "" {
+		t.Error("diff (-want, +got): ", diff)
 	}
 }
 
@@ -61,24 +68,25 @@ func TestMergeDuplicates(t *testing.T) {
 		"+bar",
 		"+manypairs:key1=value1",
 		"+manypairs:key2=value2",
+		"+manypairs:key1=value3",
 		"+oops:,,,",
 	}
 
 	extracted := ExtractCommentTags("+", comment)
 
-	if val, ok := extracted["foo"]; !ok || val != nil {
-		t.Errorf("Failed to extract single key got=%t,%v want=true,nil", ok, val)
-	}
-
-	if val, ok := extracted["bar"]; !ok || val["key"] != "value" {
-		t.Errorf(`Failed to extract single key got=%t,%v want=true,{"key":"value"}`, ok, val)
-	}
-
-	if val, ok := extracted["manypairs"]; !ok || val["key1"] != "value1" || val["key2"] != "value2" {
-		t.Errorf(`Failed to extract single key got=%t,%v want=true,{"key":"value"}`, ok, val)
+	expected := CommentTags{
+		"foo": {},
+		"bar": {
+			"key": []string{"value"},
+		},
+		"manypairs": {
+			"key1": []string{"value1", "value3"},
+			"key2": []string{"value2"},
+		},
+		"oops": {},
 	}
 
-	if val, ok := extracted["oops"]; !ok || val != nil {
-		t.Errorf(`Failed to extract single key got=%t,%v want=true,{"oops":nil}`, ok, val)
+	if diff := cmp.Diff(expected, extracted); diff != "" {
+		t.Error("diff (-want, +got): ", diff)
 	}
 }
diff --git a/injection/sharedmain/main_test.go b/injection/sharedmain/main_test.go
new file mode 100644
index 000000000..673bda005
--- /dev/null
+++ b/injection/sharedmain/main_test.go
@@ -0,0 +1,72 @@
+/*
+Copyright 2021 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 sharedmain
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"knative.dev/pkg/injection"
+)
+
+func TestEnabledControllers(t *testing.T) {
+	tests := []struct {
+		name                string
+		disabledControllers []string
+		ctors               []injection.NamedControllerConstructor
+		wantNames           []string
+	}{
+		{
+			name:                "zero",
+			disabledControllers: []string{"foo"},
+			ctors:               []injection.NamedControllerConstructor{{Name: "bar"}},
+			wantNames:           []string{"bar"},
+		},
+		{
+			name:                "one",
+			disabledControllers: []string{"foo"},
+			ctors:               []injection.NamedControllerConstructor{{Name: "foo"}},
+			wantNames:           []string{},
+		},
+		{
+			name:                "two",
+			disabledControllers: []string{"foo"},
+			ctors: []injection.NamedControllerConstructor{
+				{Name: "foo"},
+				{Name: "bar"},
+			},
+			wantNames: []string{"bar"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := enabledControllers(tt.disabledControllers, tt.ctors)
+			if diff := cmp.Diff(tt.wantNames, namesOf(got)); diff != "" {
+				t.Error("(-want, +got)", diff)
+			}
+		})
+	}
+}
+
+func namesOf(ctors []injection.NamedControllerConstructor) []string {
+	names := make([]string, 0, len(ctors))
+	for _, x := range ctors {
+		names = append(names, x.Name)
+	}
+	return names
+}
diff --git a/test/spoof/error_checks.go b/test/spoof/error_checks.go
index 378bf5824..b0638ad34 100644
--- a/test/spoof/error_checks.go
+++ b/test/spoof/error_checks.go
@@ -21,6 +21,7 @@ package spoof
 import (
 	"errors"
 	"net"
+	"net/http"
 	"strings"
 )
 
@@ -62,3 +63,8 @@ func isConnectionReset(err error) bool {
 func isNoRouteToHostError(err error) bool {
 	return err != nil && strings.Contains(err.Error(), "connect: no route to host")
 }
+
+func isResponseDNSError(resp *Response) bool {
+	// no such host with 502 is sent back from istio-ingressgateway when it fails to resolve domain.
+	return resp.StatusCode == http.StatusBadGateway && strings.Contains(string(resp.Body), "no such host")
+}
diff --git a/test/spoof/spoof.go b/test/spoof/spoof.go
index f2baa4edf..147a64adc 100644
--- a/test/spoof/spoof.go
+++ b/test/spoof/spoof.go
@@ -164,7 +164,7 @@ func (sc *SpoofingClient) Do(req *http.Request, errorRetryCheckers ...interface{
 // If no retry checkers are specified `DefaultErrorRetryChecker` will be used.
 func (sc *SpoofingClient) Poll(req *http.Request, inState ResponseChecker, checkers ...interface{}) (*Response, error) {
 	if len(checkers) == 0 {
-		checkers = []interface{}{ErrorRetryChecker(DefaultErrorRetryChecker)}
+		checkers = []interface{}{ErrorRetryChecker(DefaultErrorRetryChecker), ResponseRetryChecker(DefaultResponseRetryChecker)}
 	}
 
 	var resp *Response
@@ -255,6 +255,14 @@ func DefaultErrorRetryChecker(err error) (bool, error) {
 	return false, err
 }
 
+// DefaultResponseRetryChecker implements the defaults for retrying on response.
+func DefaultResponseRetryChecker(resp *Response) (bool, error) {
+	if isResponseDNSError(resp) {
+		return true, fmt.Errorf("retrying for DNS related failure response: %v", resp)
+	}
+	return false, nil
+}
+
 // logZipkinTrace provides support to log Zipkin Trace for param: spoofResponse
 // We only log Zipkin trace for HTTP server errors i.e for HTTP status codes between 500 to 600
 func (sc *SpoofingClient) logZipkinTrace(spoofResp *Response) {