Repository: operator-framework/operator-lifecycle-manager
Pull request #2180 (2021-06-02 17:36:46)

Surface API server warnings when applying plan steps.
Responses from the API server may include RFC2616 14.46 warnings (for
example, due to deprecation). Since the catalog-operator creates and
updates cluster resources on behalf of users, these warnings should be
surfaced to users.

When at least one warning is returned during the creation or update of
a step resource, catalog-operator now creates an Event with reason
"AppliedWithWarnings", involvedObject and fieldPath set to the plan
and step that generated the warning, respectively, and its message
containing the warning text.

In addition, a new metric, "installplan_warnings_total", counts the
number of steps that have been applied with warnings.

openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/operator-framework/operator-lifecycle-manager/pull/2180#" title="Author self-approved">benluddy</a>*

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

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/operator-framework/operator-lifecycle-manager/blob/master/OWNERS)~~ [benluddy]

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":[]} -->
exdx: /lgtm
benluddy: I suspect https://github.com/operator-framework/operator-lifecycle-manager/pull/2183 will improve the end-to-end test flakiness. Doesn't seem related to this change.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.
openshift-bot: /retest

Please review the [full test history](https://prow.ci.openshift.org/pr-history/?org=operator-framework&repo=operator-lifecycle-manager&pr=2180) for this PR and help us cut down flakes.

Patch:
diff --git a/deploy/chart/templates/0000_90_olm_01-prometheus-rule.yaml b/deploy/chart/templates/0000_90_olm_01-prometheus-rule.yaml
index 1dab10a0d..0d5001a16 100644
--- a/deploy/chart/templates/0000_90_olm_01-prometheus-rule.yaml
+++ b/deploy/chart/templates/0000_90_olm_01-prometheus-rule.yaml
@@ -9,23 +9,30 @@ metadata:
     role: alert-rules
 spec:
   groups:
-    - name: olm.csv_abnormal.rules
-      rules:
-        - alert: CsvAbnormalFailedOver2Min
-          expr: csv_abnormal{phase=~"^Failed$"}
-          for: 2m
-          labels:
-            severity: warning
-            namespace: "{{ "{{ $labels.namespace }}" }}"
-          annotations:
-            message: Failed to install Operator {{ printf "{{ $labels.name }}"  }} version {{ printf "{{ $labels.version }}"  }}. Reason-{{ printf "{{ $labels.reason }}" }}
-
-        - alert: CsvAbnormalOver30Min
-          expr: csv_abnormal{phase=~"(^Replacing$|^Pending$|^Deleting$|^Unknown$)"}
-          for: 30m
-          labels:
-            severity: warning
-            namespace: "{{ "{{ $labels.namespace }}" }}"
-          annotations:
-            message: Failed to install Operator {{ printf "{{ $labels.name }}"  }} version {{ printf "{{ $labels.version }}"  }}. Phase-{{ printf "{{ $labels.phase }}" }} Reason-{{ printf "{{ $labels.reason }}" }}
+  - name: olm.csv_abnormal.rules
+    rules:
+      - alert: CsvAbnormalFailedOver2Min
+        expr: csv_abnormal{phase=~"^Failed$"}
+        for: 2m
+        labels:
+          severity: warning
+          namespace: "{{ "{{ $labels.namespace }}" }}"
+        annotations:
+          message: Failed to install Operator {{ printf "{{ $labels.name }}"  }} version {{ printf "{{ $labels.version }}"  }}. Reason-{{ printf "{{ $labels.reason }}" }}
+      - alert: CsvAbnormalOver30Min
+        expr: csv_abnormal{phase=~"(^Replacing$|^Pending$|^Deleting$|^Unknown$)"}
+        for: 30m
+        labels:
+          severity: warning
+          namespace: "{{ "{{ $labels.namespace }}" }}"
+        annotations:
+          message: Failed to install Operator {{ printf "{{ $labels.name }}"  }} version {{ printf "{{ $labels.version }}"  }}. Phase-{{ printf "{{ $labels.phase }}" }} Reason-{{ printf "{{ $labels.reason }}" }}
+  - name: olm.installplan.rules
+    rules:
+    - alert: InstallPlanStepAppliedWithWarnings
+      expr: sum(sum_over_time(installplan_warnings_total[5m])) > 0
+      labels:
+        severity: warning
+      annotations:
+        message: The API server returned a warning during installation or upgrade of an operator. An Event with reason "AppliedWithWarnings" has been created with complete details, including a reference to the InstallPlan step that generated the warning.
 {{ end }}
diff --git a/go.mod b/go.mod
index 2ca09802d..e3a4f052f 100644
--- a/go.mod
+++ b/go.mod
@@ -55,7 +55,7 @@ require (
 	rsc.io/letsencrypt v0.0.3 // indirect
 	sigs.k8s.io/controller-runtime v0.8.3
 	sigs.k8s.io/controller-tools v0.4.1
-	sigs.k8s.io/kind v0.10.0
+	sigs.k8s.io/kind v0.11.1
 )
 
 replace (
diff --git a/go.sum b/go.sum
index b76ee7258..8691ee6bf 100644
--- a/go.sum
+++ b/go.sum
@@ -99,8 +99,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alessio/shellescape v1.2.2 h1:8LnL+ncxhWT2TR00dfJRT25JWWrhkMZXneHVWnetDZg=
-github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
+github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
+github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -275,8 +275,8 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
 github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch/v5 v5.1.0 h1:B0aXl1o/1cP8NbviYiBMkcHBtUjIJ1/Ccg6b+SwCLQg=
-github.com/evanphx/json-patch/v5 v5.1.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
+github.com/evanphx/json-patch/v5 v5.2.0 h1:8ozOH5xxoMYDt5/u+yMTsVXydVCbTORFnOOoq2lumco=
+github.com/evanphx/json-patch/v5 v5.2.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
 github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
 github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
 github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU=
@@ -1169,11 +1169,11 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200928205150-006507a75852/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M=
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -1362,8 +1362,9 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
@@ -1464,8 +1465,8 @@ sigs.k8s.io/controller-runtime v0.8.3/go.mod h1:U/l+DUopBc1ecfRZ5aviA9JDmGFQKvLf
 sigs.k8s.io/controller-tools v0.3.0/go.mod h1:enhtKGfxZD1GFEoMgP8Fdbu+uKQ/cq1/WGJhdVChfvI=
 sigs.k8s.io/controller-tools v0.4.1 h1:VkuV0MxlRPmRu5iTgBZU4UxUX2LiR99n3sdQGRxZF4w=
 sigs.k8s.io/controller-tools v0.4.1/go.mod h1:G9rHdZMVlBDocIxGkK3jHLWqcTMNvveypYJwrvYKjWU=
-sigs.k8s.io/kind v0.10.0 h1:Tm+QITIqdRd+efLOsxZHMAfLnr5K4e3/RH8MePspEXs=
-sigs.k8s.io/kind v0.10.0/go.mod h1:fb32zUw7ewC47bPwLnwhf47wd/vADtv3c38KP7sjIlo=
+sigs.k8s.io/kind v0.11.1 h1:pVzOkhUwMBrCB0Q/WllQDO3v14Y+o2V0tFgjTqIUjwA=
+sigs.k8s.io/kind v0.11.1/go.mod h1:fRpgVhtqAWrtLB9ED7zQahUimpUXuG/iHT88xYqEGIA=
 sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
diff --git a/pkg/controller/operators/catalog/operator.go b/pkg/controller/operators/catalog/operator.go
index fb88ef4de..2a48ed3cf 100644
--- a/pkg/controller/operators/catalog/operator.go
+++ b/pkg/controller/operators/catalog/operator.go
@@ -51,6 +51,7 @@ import (
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/reconciler"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/solver"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/clients"
 	controllerclient "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/controller-runtime/client"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/event"
 	index "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/index"
@@ -108,6 +109,7 @@ type Operator struct {
 	bundleUnpacker           bundle.Unpacker
 	installPlanTimeout       time.Duration
 	bundleUnpackTimeout      time.Duration
+	clientFactory            clients.Factory
 }
 
 type CatalogSourceSyncFunc func(logger *logrus.Entry, in *v1alpha1.CatalogSource) (out *v1alpha1.CatalogSource, continueSync bool, syncError error)
@@ -133,7 +135,11 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo
 	}
 
 	// Create a new queueinformer-based operator.
-	opClient := operatorclient.NewClientFromConfig(kubeconfigPath, logger)
+	opClient, err := operatorclient.NewClientFromRestConfig(config)
+	if err != nil {
+		return nil, err
+	}
+
 	queueOperator, err := queueinformer.NewOperator(opClient.KubernetesInterface().Discovery(), queueinformer.WithOperatorLogger(logger))
 	if err != nil {
 		return nil, err
@@ -170,9 +176,10 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo
 		csvProvidedAPIsIndexer:   map[string]cache.Indexer{},
 		catalogSubscriberIndexer: map[string]cache.Indexer{},
 		serviceAccountQuerier:    scoped.NewUserDefinedServiceAccountQuerier(logger, crClient),
-		clientAttenuator:         scoped.NewClientAttenuator(logger, config, opClient, crClient, dynamicClient),
+		clientAttenuator:         scoped.NewClientAttenuator(logger, config, opClient),
 		installPlanTimeout:       installPlanTimeout,
 		bundleUnpackTimeout:      bundleUnpackTimeout,
+		clientFactory:            clients.NewFactory(config),
 	}
 	op.sources = grpc.NewSourceStore(logger, 10*time.Second, 10*time.Minute, op.syncSourceState)
 	op.reconciler = reconciler.NewRegistryReconcilerFactory(lister, opClient, configmapRegistryImage, op.now, ssaClient)
@@ -1677,6 +1684,29 @@ func validateExistingCRs(dynamicClient dynamic.Interface, gvr schema.GroupVersio
 	return nil
 }
 
+type warningRecorder struct {
+	m        sync.Mutex
+	warnings []string
+}
+
+func (wr *warningRecorder) HandleWarningHeader(code int, agent string, text string) {
+	if code != 299 {
+		return
+	}
+	wr.m.Lock()
+	defer wr.m.Unlock()
+	wr.warnings = append(wr.warnings, text)
+}
+
+func (wr *warningRecorder) PopWarnings() []string {
+	wr.m.Lock()
+	defer wr.m.Unlock()
+
+	result := wr.warnings
+	wr.warnings = nil
+	return result
+}
+
 // ExecutePlan applies a planned InstallPlan to a namespace.
 func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
 	if plan.Status.Phase != v1alpha1.InstallPlanPhaseInstalling {
@@ -1693,12 +1723,31 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
 		return err
 	}
 
+	var wr warningRecorder
+	factory := o.clientFactory.WithConfigTransformer(clients.SetWarningHandler(&wr))
+
 	// Does the namespace have an operator group that specifies a user defined
 	// service account? If so, then we should use a scoped client for plan
 	// execution.
-	kubeclient, crclient, dynamicClient, err := o.clientAttenuator.AttenuateClientWithServiceAccount(plan.Status.AttenuatedServiceAccountRef)
+	attenuate, err := o.clientAttenuator.AttenuateToServiceAccount(scoped.StaticQuerier(plan.Status.AttenuatedServiceAccountRef))
 	if err != nil {
-		o.logger.Errorf("failed to get a client for plan execution- %v", err)
+		o.logger.Errorf("failed to get a client for plan execution: %v", err)
+		return err
+	}
+	attenuatedFactory := factory.WithConfigTransformer(attenuate)
+	kubeclient, err := attenuatedFactory.NewOperatorClient()
+	if err != nil {
+		o.logger.Errorf("failed to get a client for plan execution: %v", err)
+		return err
+	}
+	crclient, err := attenuatedFactory.NewKubernetesClient()
+	if err != nil {
+		o.logger.Errorf("failed to get a client for plan execution: %v", err)
+		return err
+	}
+	dynamicClient, err := attenuatedFactory.NewDynamicClient()
+	if err != nil {
+		o.logger.Errorf("failed to get a client for plan execution: %v", err)
 		return err
 	}
 
@@ -1708,7 +1757,12 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
 	// CRDs should be installed via the default OLM (cluster-admin) client and not the scoped client specified by the AttenuatedServiceAccount
 	// the StepBuilder is currently only implemented for CRD types
 	// TODO give the StepBuilder both OLM and scoped clients when it supports new scoped types
-	builderKubeClient, _, builderDynamicClient, err := o.clientAttenuator.AttenuateClientWithServiceAccount(nil)
+	builderKubeClient, err := factory.NewOperatorClient()
+	if err != nil {
+		o.logger.Errorf("failed to get a client for plan execution- %v", err)
+		return err
+	}
+	builderDynamicClient, err := factory.NewDynamicClient()
 	if err != nil {
 		o.logger.Errorf("failed to get a client for plan execution- %v", err)
 		return err
@@ -1716,363 +1770,390 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error {
 	b := newBuilder(plan, o.lister.OperatorsV1alpha1().ClusterServiceVersionLister(), builderKubeClient, builderDynamicClient, r, o.logger)
 
 	for i, step := range plan.Status.Plan {
-		doStep := true
-		s, err := b.create(*step)
-		if err != nil {
-			if _, ok := err.(notSupportedStepperErr); ok {
-				// stepper not implemented for this type yet
-				// stepper currently only implemented for CRD types
-				doStep = false
-			} else {
-				return err
-			}
-		}
-		if doStep {
-			status, err := s.Status()
-			if err != nil {
-				return err
-			}
-			plan.Status.Plan[i].Status = status
-			continue
-		}
+		if err := func(i int, step *v1alpha1.Step) error {
+			wr.PopWarnings()
+			defer func() {
+				warnings := wr.PopWarnings()
+				if len(warnings) == 0 {
+					return
+				}
+				var obj runtime.Object
+				if ref, err := reference.GetReference(plan); err != nil {
+					o.logger.WithError(err).Warnf("error getting plan reference")
+					obj = plan
+				} else {
+					ref.FieldPath = fmt.Sprintf("status.plan[%d]", i)
+					obj = ref
+				}
+				msg := fmt.Sprintf("%d warning(s) generated during operator installation (%s %q): %s", len(warnings), step.Resource.Kind, step.Resource.Name, strings.Join(warnings, ", "))
+				if step.Resolving != "" {
+					msg = fmt.Sprintf("%d warning(s) generated during installation of operator %q (%s %q): %s", len(warnings), step.Resolving, step.Resource.Kind, step.Resource.Name, strings.Join(warnings, ", "))
+				}
+				o.recorder.Event(obj, corev1.EventTypeWarning, "AppliedWithWarnings", msg)
+				metrics.EmitInstallPlanWarning()
+			}()
 
-		switch step.Status {
-		case v1alpha1.StepStatusPresent, v1alpha1.StepStatusCreated, v1alpha1.StepStatusWaitingForAPI:
-			continue
-		case v1alpha1.StepStatusUnknown, v1alpha1.StepStatusNotPresent:
-			manifest, err := r.ManifestForStep(step)
+			doStep := true
+			s, err := b.create(*step)
 			if err != nil {
-				return err
+				if _, ok := err.(notSupportedStepperErr); ok {
+					// stepper not implemented for this type yet
+					// stepper currently only implemented for CRD types
+					doStep = false
+				} else {
+					return err
+				}
 			}
-			o.logger.WithFields(logrus.Fields{"kind": step.Resource.Kind, "name": step.Resource.Name}).Debug("execute resource")
-			switch step.Resource.Kind {
-			case v1alpha1.ClusterServiceVersionKind:
-				// Marshal the manifest into a CSV instance.
-				var csv v1alpha1.ClusterServiceVersion
-				err := json.Unmarshal([]byte(manifest), &csv)
+			if doStep {
+				status, err := s.Status()
 				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					return err
 				}
+				plan.Status.Plan[i].Status = status
+				return nil
+			}
 
-				// Check if the resolved CSV is in the initial set
-				if _, ok := initialCSVNames[csv.GetName()]; !ok {
-					// Check for pre-existing CSVs that own the same CRDs
-					competingOwners, err := competingCRDOwnersExist(plan.GetNamespace(), &csv, existingCRDOwners)
+			switch step.Status {
+			case v1alpha1.StepStatusPresent, v1alpha1.StepStatusCreated, v1alpha1.StepStatusWaitingForAPI:
+				return nil
+			case v1alpha1.StepStatusUnknown, v1alpha1.StepStatusNotPresent:
+				manifest, err := r.ManifestForStep(step)
+				if err != nil {
+					return err
+				}
+				o.logger.WithFields(logrus.Fields{"kind": step.Resource.Kind, "name": step.Resource.Name}).Debug("execute resource")
+				switch step.Resource.Kind {
+				case v1alpha1.ClusterServiceVersionKind:
+					// Marshal the manifest into a CSV instance.
+					var csv v1alpha1.ClusterServiceVersion
+					err := json.Unmarshal([]byte(manifest), &csv)
 					if err != nil {
-						return errorwrap.Wrapf(err, "error checking crd owners for: %s", csv.GetName())
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
 					}
 
-					// TODO: decide on fail/continue logic for pre-existing dependent CSVs that own the same CRD(s)
-					if competingOwners {
-						// For now, error out
-						return fmt.Errorf("pre-existing CRD owners found for owned CRD(s) of dependent CSV %s", csv.GetName())
-					}
-				}
-
-				// Attempt to create the CSV.
-				csv.SetNamespace(namespace)
+					// Check if the resolved CSV is in the initial set
+					if _, ok := initialCSVNames[csv.GetName()]; !ok {
+						// Check for pre-existing CSVs that own the same CRDs
+						competingOwners, err := competingCRDOwnersExist(plan.GetNamespace(), &csv, existingCRDOwners)
+						if err != nil {
+							return errorwrap.Wrapf(err, "error checking crd owners for: %s", csv.GetName())
+						}
 
-				status, err := ensurer.EnsureClusterServiceVersion(&csv)
-				if err != nil {
-					return err
-				}
+						// TODO: decide on fail/continue logic for pre-existing dependent CSVs that own the same CRD(s)
+						if competingOwners {
+							// For now, error out
+							return fmt.Errorf("pre-existing CRD owners found for owned CRD(s) of dependent CSV %s", csv.GetName())
+						}
+					}
 
-				plan.Status.Plan[i].Status = status
+					// Attempt to create the CSV.
+					csv.SetNamespace(namespace)
 
-			case v1alpha1.SubscriptionKind:
-				// Marshal the manifest into a subscription instance.
-				var sub v1alpha1.Subscription
-				err := json.Unmarshal([]byte(manifest), &sub)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
-				}
+					status, err := ensurer.EnsureClusterServiceVersion(&csv)
+					if err != nil {
+						return err
+					}
 
-				// Add the InstallPlan's name as an annotation
-				if annotations := sub.GetAnnotations(); annotations != nil {
-					annotations[generatedByKey] = plan.GetName()
-				} else {
-					sub.SetAnnotations(map[string]string{generatedByKey: plan.GetName()})
-				}
+					plan.Status.Plan[i].Status = status
 
-				// Attempt to create the Subscription
-				sub.SetNamespace(namespace)
+				case v1alpha1.SubscriptionKind:
+					// Marshal the manifest into a subscription instance.
+					var sub v1alpha1.Subscription
+					err := json.Unmarshal([]byte(manifest), &sub)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					}
 
-				status, err := ensurer.EnsureSubscription(&sub)
-				if err != nil {
-					return err
-				}
+					// Add the InstallPlan's name as an annotation
+					if annotations := sub.GetAnnotations(); annotations != nil {
+						annotations[generatedByKey] = plan.GetName()
+					} else {
+						sub.SetAnnotations(map[string]string{generatedByKey: plan.GetName()})
+					}
 
-				plan.Status.Plan[i].Status = status
+					// Attempt to create the Subscription
+					sub.SetNamespace(namespace)
 
-			case resolver.BundleSecretKind:
-				var s corev1.Secret
-				err := json.Unmarshal([]byte(manifest), &s)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
-				}
+					status, err := ensurer.EnsureSubscription(&sub)
+					if err != nil {
+						return err
+					}
 
-				// add ownerrefs on the secret that point to the CSV in the bundle
-				if step.Resolving != "" {
-					owner := &v1alpha1.ClusterServiceVersion{}
-					owner.SetNamespace(plan.GetNamespace())
-					owner.SetName(step.Resolving)
-					ownerutil.AddNonBlockingOwner(&s, owner)
-				}
+					plan.Status.Plan[i].Status = status
 
-				// Update UIDs on all CSV OwnerReferences
-				updated, err := o.getUpdatedOwnerReferences(s.OwnerReferences, plan.Namespace)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error generating ownerrefs for secret %s", s.GetName())
-				}
-				s.SetOwnerReferences(updated)
-				s.SetNamespace(namespace)
+				case resolver.BundleSecretKind:
+					var s corev1.Secret
+					err := json.Unmarshal([]byte(manifest), &s)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					}
 
-				status, err := ensurer.EnsureBundleSecret(plan.Namespace, &s)
-				if err != nil {
-					return err
-				}
+					// add ownerrefs on the secret that point to the CSV in the bundle
+					if step.Resolving != "" {
+						owner := &v1alpha1.ClusterServiceVersion{}
+						owner.SetNamespace(plan.GetNamespace())
+						owner.SetName(step.Resolving)
+						ownerutil.AddNonBlockingOwner(&s, owner)
+					}
 
-				plan.Status.Plan[i].Status = status
+					// Update UIDs on all CSV OwnerReferences
+					updated, err := o.getUpdatedOwnerReferences(s.OwnerReferences, plan.Namespace)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error generating ownerrefs for secret %s", s.GetName())
+					}
+					s.SetOwnerReferences(updated)
+					s.SetNamespace(namespace)
 
-			case secretKind:
-				status, err := ensurer.EnsureSecret(o.namespace, plan.GetNamespace(), step.Resource.Name)
-				if err != nil {
-					return err
-				}
+					status, err := ensurer.EnsureBundleSecret(plan.Namespace, &s)
+					if err != nil {
+						return err
+					}
 
-				plan.Status.Plan[i].Status = status
+					plan.Status.Plan[i].Status = status
 
-			case clusterRoleKind:
-				// Marshal the manifest into a ClusterRole instance.
-				var cr rbacv1.ClusterRole
-				err := json.Unmarshal([]byte(manifest), &cr)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
-				}
+				case secretKind:
+					status, err := ensurer.EnsureSecret(o.namespace, plan.GetNamespace(), step.Resource.Name)
+					if err != nil {
+						return err
+					}
 
-				status, err := ensurer.EnsureClusterRole(&cr, step)
-				if err != nil {
-					return err
-				}
+					plan.Status.Plan[i].Status = status
 
-				plan.Status.Plan[i].Status = status
+				case clusterRoleKind:
+					// Marshal the manifest into a ClusterRole instance.
+					var cr rbacv1.ClusterRole
+					err := json.Unmarshal([]byte(manifest), &cr)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					}
 
-			case clusterRoleBindingKind:
-				// Marshal the manifest into a RoleBinding instance.
-				var rb rbacv1.ClusterRoleBinding
-				err := json.Unmarshal([]byte(manifest), &rb)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
-				}
+					status, err := ensurer.EnsureClusterRole(&cr, step)
+					if err != nil {
+						return err
+					}
 
-				status, err := ensurer.EnsureClusterRoleBinding(&rb, step)
-				if err != nil {
-					return err
-				}
+					plan.Status.Plan[i].Status = status
 
-				plan.Status.Plan[i].Status = status
+				case clusterRoleBindingKind:
+					// Marshal the manifest into a RoleBinding instance.
+					var rb rbacv1.ClusterRoleBinding
+					err := json.Unmarshal([]byte(manifest), &rb)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					}
 
-			case roleKind:
-				// Marshal the manifest into a Role instance.
-				var r rbacv1.Role
-				err := json.Unmarshal([]byte(manifest), &r)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
-				}
+					status, err := ensurer.EnsureClusterRoleBinding(&rb, step)
+					if err != nil {
+						return err
+					}
 
-				// Update UIDs on all CSV OwnerReferences
-				updated, err := o.getUpdatedOwnerReferences(r.OwnerReferences, plan.Namespace)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error generating ownerrefs for role %s", r.GetName())
-				}
-				r.SetOwnerReferences(updated)
-				r.SetNamespace(namespace)
+					plan.Status.Plan[i].Status = status
 
-				status, err := ensurer.EnsureRole(plan.Namespace, &r)
-				if err != nil {
-					return err
-				}
+				case roleKind:
+					// Marshal the manifest into a Role instance.
+					var r rbacv1.Role
+					err := json.Unmarshal([]byte(manifest), &r)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					}
 
-				plan.Status.Plan[i].Status = status
+					// Update UIDs on all CSV OwnerReferences
+					updated, err := o.getUpdatedOwnerReferences(r.OwnerReferences, plan.Namespace)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error generating ownerrefs for role %s", r.GetName())
+					}
+					r.SetOwnerReferences(updated)
+					r.SetNamespace(namespace)
 
-			case roleBindingKind:
-				// Marshal the manifest into a RoleBinding instance.
-				var rb rbacv1.RoleBinding
-				err := json.Unmarshal([]byte(manifest), &rb)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
-				}
+					status, err := ensurer.EnsureRole(plan.Namespace, &r)
+					if err != nil {
+						return err
+					}
 
-				// Update UIDs on all CSV OwnerReferences
-				updated, err := o.getUpdatedOwnerReferences(rb.OwnerReferences, plan.Namespace)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error generating ownerrefs for rolebinding %s", rb.GetName())
-				}
-				rb.SetOwnerReferences(updated)
-				rb.SetNamespace(namespace)
+					plan.Status.Plan[i].Status = status
 
-				status, err := ensurer.EnsureRoleBinding(plan.Namespace, &rb)
-				if err != nil {
-					return err
-				}
+				case roleBindingKind:
+					// Marshal the manifest into a RoleBinding instance.
+					var rb rbacv1.RoleBinding
+					err := json.Unmarshal([]byte(manifest), &rb)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					}
 
-				plan.Status.Plan[i].Status = status
+					// Update UIDs on all CSV OwnerReferences
+					updated, err := o.getUpdatedOwnerReferences(rb.OwnerReferences, plan.Namespace)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error generating ownerrefs for rolebinding %s", rb.GetName())
+					}
+					rb.SetOwnerReferences(updated)
+					rb.SetNamespace(namespace)
 
-			case serviceAccountKind:
-				// Marshal the manifest into a ServiceAccount instance.
-				var sa corev1.ServiceAccount
-				err := json.Unmarshal([]byte(manifest), &sa)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
-				}
+					status, err := ensurer.EnsureRoleBinding(plan.Namespace, &rb)
+					if err != nil {
+						return err
+					}
 
-				// Update UIDs on all CSV OwnerReferences
-				updated, err := o.getUpdatedOwnerReferences(sa.OwnerReferences, plan.Namespace)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error generating ownerrefs for service account: %s", sa.GetName())
-				}
-				sa.SetOwnerReferences(updated)
-				sa.SetNamespace(namespace)
+					plan.Status.Plan[i].Status = status
 
-				status, err := ensurer.EnsureServiceAccount(namespace, &sa)
-				if err != nil {
-					return err
-				}
+				case serviceAccountKind:
+					// Marshal the manifest into a ServiceAccount instance.
+					var sa corev1.ServiceAccount
+					err := json.Unmarshal([]byte(manifest), &sa)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					}
 
-				plan.Status.Plan[i].Status = status
+					// Update UIDs on all CSV OwnerReferences
+					updated, err := o.getUpdatedOwnerReferences(sa.OwnerReferences, plan.Namespace)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error generating ownerrefs for service account: %s", sa.GetName())
+					}
+					sa.SetOwnerReferences(updated)
+					sa.SetNamespace(namespace)
 
-			case serviceKind:
-				// Marshal the manifest into a Service instance
-				var s corev1.Service
-				err := json.Unmarshal([]byte(manifest), &s)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
-				}
+					status, err := ensurer.EnsureServiceAccount(namespace, &sa)
+					if err != nil {
+						return err
+					}
 
-				// add ownerrefs on the service that point to the CSV in the bundle
-				if step.Resolving != "" {
-					owner := &v1alpha1.ClusterServiceVersion{}
-					owner.SetNamespace(plan.GetNamespace())
-					owner.SetName(step.Resolving)
-					ownerutil.AddNonBlockingOwner(&s, owner)
-				}
+					plan.Status.Plan[i].Status = status
 
-				// Update UIDs on all CSV OwnerReferences
-				updated, err := o.getUpdatedOwnerReferences(s.OwnerReferences, plan.Namespace)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error generating ownerrefs for service: %s", s.GetName())
-				}
-				s.SetOwnerReferences(updated)
-				s.SetNamespace(namespace)
+				case serviceKind:
+					// Marshal the manifest into a Service instance
+					var s corev1.Service
+					err := json.Unmarshal([]byte(manifest), &s)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					}
 
-				status, err := ensurer.EnsureService(namespace, &s)
-				if err != nil {
-					return err
-				}
+					// add ownerrefs on the service that point to the CSV in the bundle
+					if step.Resolving != "" {
+						owner := &v1alpha1.ClusterServiceVersion{}
+						owner.SetNamespace(plan.GetNamespace())
+						owner.SetName(step.Resolving)
+						ownerutil.AddNonBlockingOwner(&s, owner)
+					}
 
-				plan.Status.Plan[i].Status = status
+					// Update UIDs on all CSV OwnerReferences
+					updated, err := o.getUpdatedOwnerReferences(s.OwnerReferences, plan.Namespace)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error generating ownerrefs for service: %s", s.GetName())
+					}
+					s.SetOwnerReferences(updated)
+					s.SetNamespace(namespace)
 
-			case configMapKind:
-				var cfg corev1.ConfigMap
-				err := json.Unmarshal([]byte(manifest), &cfg)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
-				}
+					status, err := ensurer.EnsureService(namespace, &s)
+					if err != nil {
+						return err
+					}
 
-				// add ownerrefs on the configmap that point to the CSV in the bundle
-				if step.Resolving != "" {
-					owner := &v1alpha1.ClusterServiceVersion{}
-					owner.SetNamespace(plan.GetNamespace())
-					owner.SetName(step.Resolving)
-					ownerutil.AddNonBlockingOwner(&cfg, owner)
-				}
+					plan.Status.Plan[i].Status = status
 
-				// Update UIDs on all CSV OwnerReferences
-				updated, err := o.getUpdatedOwnerReferences(cfg.OwnerReferences, plan.Namespace)
-				if err != nil {
-					return errorwrap.Wrapf(err, "error generating ownerrefs for configmap: %s", cfg.GetName())
-				}
-				cfg.SetOwnerReferences(updated)
-				cfg.SetNamespace(namespace)
+				case configMapKind:
+					var cfg corev1.ConfigMap
+					err := json.Unmarshal([]byte(manifest), &cfg)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error parsing step manifest: %s", step.Resource.Name)
+					}
 
-				status, err := ensurer.EnsureConfigMap(plan.Namespace, &cfg)
-				if err != nil {
-					return err
-				}
+					// add ownerrefs on the configmap that point to the CSV in the bundle
+					if step.Resolving != "" {
+						owner := &v1alpha1.ClusterServiceVersion{}
+						owner.SetNamespace(plan.GetNamespace())
+						owner.SetName(step.Resolving)
+						ownerutil.AddNonBlockingOwner(&cfg, owner)
+					}
 
-				plan.Status.Plan[i].Status = status
+					// Update UIDs on all CSV OwnerReferences
+					updated, err := o.getUpdatedOwnerReferences(cfg.OwnerReferences, plan.Namespace)
+					if err != nil {
+						return errorwrap.Wrapf(err, "error generating ownerrefs for configmap: %s", cfg.GetName())
+					}
+					cfg.SetOwnerReferences(updated)
+					cfg.SetNamespace(namespace)
 
-			default:
-				if !isSupported(step.Resource.Kind) {
-					// Not a supported resource
-					plan.Status.Plan[i].Status = v1alpha1.StepStatusUnsupportedResource
-					return v1alpha1.ErrInvalidInstallPlan
-				}
+					status, err := ensurer.EnsureConfigMap(plan.Namespace, &cfg)
+					if err != nil {
+						return err
+					}
 
-				// Marshal the manifest into an unstructured object
-				dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(manifest), 10)
-				unstructuredObject := &unstructured.Unstructured{}
-				if err := dec.Decode(unstructuredObject); err != nil {
-					return errorwrap.Wrapf(err, "error decoding %s object to an unstructured object", step.Resource.Name)
-				}
+					plan.Status.Plan[i].Status = status
 
-				// Get the resource from the GVK.
-				gvk := unstructuredObject.GroupVersionKind()
-				r, err := o.apiresourceFromGVK(gvk)
-				if err != nil {
-					return err
-				}
+				default:
+					if !isSupported(step.Resource.Kind) {
+						// Not a supported resource
+						plan.Status.Plan[i].Status = v1alpha1.StepStatusUnsupportedResource
+						return v1alpha1.ErrInvalidInstallPlan
+					}
 
-				// Create the GVR
-				gvr := schema.GroupVersionResource{
-					Group:    gvk.Group,
-					Version:  gvk.Version,
-					Resource: r.Name,
-				}
+					// Marshal the manifest into an unstructured object
+					dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(manifest), 10)
+					unstructuredObject := &unstructured.Unstructured{}
+					if err := dec.Decode(unstructuredObject); err != nil {
+						return errorwrap.Wrapf(err, "error decoding %s object to an unstructured object", step.Resource.Name)
+					}
 
-				if step.Resolving != "" {
-					owner := &v1alpha1.ClusterServiceVersion{}
-					owner.SetNamespace(plan.GetNamespace())
-					owner.SetName(step.Resolving)
+					// Get the resource from the GVK.
+					gvk := unstructuredObject.GroupVersionKind()
+					r, err := o.apiresourceFromGVK(gvk)
+					if err != nil {
+						return err
+					}
 
-					if r.Namespaced {
-						// Set OwnerReferences for namespace-scoped resource
-						ownerutil.AddNonBlockingOwner(unstructuredObject, owner)
+					// Create the GVR
+					gvr := schema.GroupVersionResource{
+						Group:    gvk.Group,
+						Version:  gvk.Version,
+						Resource: r.Name,
+					}
 
-						// Update UIDs on all CSV OwnerReferences
-						updated, err := o.getUpdatedOwnerReferences(unstructuredObject.GetOwnerReferences(), plan.Namespace)
-						if err != nil {
-							return errorwrap.Wrapf(err, "error generating ownerrefs for unstructured object: %s", unstructuredObject.GetName())
+					if step.Resolving != "" {
+						owner := &v1alpha1.ClusterServiceVersion{}
+						owner.SetNamespace(plan.GetNamespace())
+						owner.SetName(step.Resolving)
+
+						if r.Namespaced {
+							// Set OwnerReferences for namespace-scoped resource
+							ownerutil.AddNonBlockingOwner(unstructuredObject, owner)
+
+							// Update UIDs on all CSV OwnerReferences
+							updated, err := o.getUpdatedOwnerReferences(unstructuredObject.GetOwnerReferences(), plan.Namespace)
+							if err != nil {
+								return errorwrap.Wrapf(err, "error generating ownerrefs for unstructured object: %s", unstructuredObject.GetName())
+							}
+
+							unstructuredObject.SetOwnerReferences(updated)
+						} else {
+							// Add owner labels to cluster-scoped resource
+							if err := ownerutil.AddOwnerLabels(unstructuredObject, owner); err != nil {
+								return err
+							}
 						}
+					}
 
-						unstructuredObject.SetOwnerReferences(updated)
+					// Set up the dynamic client ResourceInterface and set ownerrefs
+					var resourceInterface dynamic.ResourceInterface
+					if r.Namespaced {
+						unstructuredObject.SetNamespace(namespace)
+						resourceInterface = dynamicClient.Resource(gvr).Namespace(namespace)
 					} else {
-						// Add owner labels to cluster-scoped resource
-						if err := ownerutil.AddOwnerLabels(unstructuredObject, owner); err != nil {
-							return err
-						}
+						resourceInterface = dynamicClient.Resource(gvr)
 					}
-				}
 
-				// Set up the dynamic client ResourceInterface and set ownerrefs
-				var resourceInterface dynamic.ResourceInterface
-				if r.Namespaced {
-					unstructuredObject.SetNamespace(namespace)
-					resourceInterface = o.dynamicClient.Resource(gvr).Namespace(namespace)
-				} else {
-					resourceInterface = o.dynamicClient.Resource(gvr)
-				}
+					// Ensure Unstructured Object
+					status, err := ensurer.EnsureUnstructuredObject(resourceInterface, unstructuredObject)
+					if err != nil {
+						return err
+					}
 
-				// Ensure Unstructured Object
-				status, err := ensurer.EnsureUnstructuredObject(resourceInterface, unstructuredObject)
-				if err != nil {
-					return err
+					plan.Status.Plan[i].Status = status
 				}
-
-				plan.Status.Plan[i].Status = status
+			default:
+				return v1alpha1.ErrInvalidInstallPlan
 			}
-		default:
-			return v1alpha1.ErrInvalidInstallPlan
+			return nil
+		}(i, step); err != nil {
+			return err
 		}
 	}
 
diff --git a/pkg/controller/operators/olm/operator.go b/pkg/controller/operators/olm/operator.go
index 0c54b7bb5..c79023eef 100644
--- a/pkg/controller/operators/olm/operator.go
+++ b/pkg/controller/operators/olm/operator.go
@@ -36,6 +36,7 @@ import (
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/olm/overrides"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/clients"
 	csvutility "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/csv"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/event"
 	index "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/index"
@@ -82,6 +83,7 @@ type Operator struct {
 	serviceAccountSyncer  *scoped.UserDefinedServiceAccountSyncer
 	clientAttenuator      *scoped.ClientAttenuator
 	serviceAccountQuerier *scoped.UserDefinedServiceAccountQuerier
+	clientFactory         clients.Factory
 }
 
 func NewOperator(ctx context.Context, options ...OperatorOption) (*Operator, error) {
@@ -134,8 +136,9 @@ func newOperatorWithConfig(ctx context.Context, config *operatorConfig) (*Operat
 		csvSetGenerator:       csvutility.NewSetGenerator(config.logger, lister),
 		csvReplaceFinder:      csvutility.NewReplaceFinder(config.logger, config.externalClient),
 		serviceAccountSyncer:  scoped.NewUserDefinedServiceAccountSyncer(config.logger, scheme, config.operatorClient, config.externalClient),
-		clientAttenuator:      scoped.NewClientAttenuator(config.logger, config.restConfig, config.operatorClient, config.externalClient, nil),
+		clientAttenuator:      scoped.NewClientAttenuator(config.logger, config.restConfig, config.operatorClient),
 		serviceAccountQuerier: scoped.NewUserDefinedServiceAccountQuerier(config.logger, config.externalClient),
+		clientFactory:         clients.NewFactory(config.restConfig),
 	}
 
 	// Set up syncing for namespace-scoped resources
@@ -1886,11 +1889,12 @@ func (a *Operator) parseStrategiesAndUpdateStatus(csv *v1alpha1.ClusterServiceVe
 	// associated with the namespace then we should use a scoped client that is
 	// bound to the service account.
 	querierFunc := a.serviceAccountQuerier.NamespaceQuerier(csv.GetNamespace())
-	kubeclient, err := a.clientAttenuator.AttenuateOperatorClient(querierFunc)
+	attenuate, err := a.clientAttenuator.AttenuateToServiceAccount(querierFunc)
 	if err != nil {
 		a.logger.Errorf("failed to get a client for operator deployment- %v", err)
 		return nil, nil
 	}
+	kubeclient, err := a.clientFactory.WithConfigTransformer(attenuate).NewOperatorClient()
 
 	strName := strategy.GetStrategyName()
 	installer := a.resolver.InstallerForStrategy(strName, kubeclient, a.lister, csv, csv.GetAnnotations(), csv.GetAllAPIServiceDescriptions(), csv.Spec.WebhookDefinitions, previousStrategy)
diff --git a/pkg/lib/clients/factory.go b/pkg/lib/clients/factory.go
new file mode 100644
index 000000000..fa4cfa844
--- /dev/null
+++ b/pkg/lib/clients/factory.go
@@ -0,0 +1,57 @@
+package clients
+
+import (
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/rest"
+
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
+)
+
+type ConfigTransformer interface {
+	// Applies a transformation to the provided Config and returns
+	// the resulting Config. The provided Config is safe to return
+	// directly (i.e., without making a copy) and will never be
+	// nil. Implementations must never return nil.
+	TransformConfig(config *rest.Config) *rest.Config
+}
+
+type ConfigTransformerFunc func(config *rest.Config) *rest.Config
+
+func (t ConfigTransformerFunc) TransformConfig(config *rest.Config) *rest.Config {
+	return t(config)
+}
+
+type Factory interface {
+	WithConfigTransformer(ConfigTransformer) Factory
+	NewOperatorClient() (operatorclient.ClientInterface, error)
+	NewKubernetesClient() (versioned.Interface, error)
+	NewDynamicClient() (dynamic.Interface, error)
+}
+
+type factory struct {
+	config *rest.Config
+}
+
+func NewFactory(config *rest.Config) Factory {
+	return &factory{config: config}
+}
+
+// WithConfigTransformer returns a new factory that produces clients
+// using a copy of the receiver's REST config that has been
+// transformed by the given ConfigTransformer.
+func (f *factory) WithConfigTransformer(t ConfigTransformer) Factory {
+	return &factory{config: t.TransformConfig(rest.CopyConfig(f.config))}
+}
+
+func (f *factory) NewOperatorClient() (operatorclient.ClientInterface, error) {
+	return operatorclient.NewClientFromRestConfig(f.config)
+}
+
+func (f *factory) NewKubernetesClient() (versioned.Interface, error) {
+	return versioned.NewForConfig(f.config)
+}
+
+func (f *factory) NewDynamicClient() (dynamic.Interface, error) {
+	return dynamic.NewForConfig(f.config)
+}
diff --git a/pkg/lib/clients/transformers.go b/pkg/lib/clients/transformers.go
new file mode 100644
index 000000000..6029dfa81
--- /dev/null
+++ b/pkg/lib/clients/transformers.go
@@ -0,0 +1,12 @@
+package clients
+
+import (
+	"k8s.io/client-go/rest"
+)
+
+func SetWarningHandler(wh rest.WarningHandler) ConfigTransformer {
+	return ConfigTransformerFunc(func(config *rest.Config) *rest.Config {
+		config.WarningHandler = wh
+		return config
+	})
+}
diff --git a/pkg/lib/scoped/attenuator.go b/pkg/lib/scoped/attenuator.go
index ecae79392..bebc568d1 100644
--- a/pkg/lib/scoped/attenuator.go
+++ b/pkg/lib/scoped/attenuator.go
@@ -1,29 +1,19 @@
 package scoped
 
 import (
-	"errors"
-
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/scopedclient"
 	"github.com/sirupsen/logrus"
+
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/rest"
-)
 
-var (
-	errQuerierNotSpecified = errors.New("no service account querier func specified")
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/clients"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
 )
 
 // NewClientAttenuator returns a new instance of ClientAttenuator.
-func NewClientAttenuator(logger *logrus.Logger, config *rest.Config, kubeclient operatorclient.ClientInterface, crclient versioned.Interface, dynamicClient dynamic.Interface) *ClientAttenuator {
+func NewClientAttenuator(logger logrus.FieldLogger, config *rest.Config, kubeclient operatorclient.ClientInterface) *ClientAttenuator {
 	return &ClientAttenuator{
-		logger:        logger,
-		kubeclient:    kubeclient,
-		crclient:      crclient,
-		dynamicClient: dynamicClient,
-		factory:       scopedclient.NewFactory(config),
+		logger: logger,
 		retriever: &BearerTokenRetriever{
 			kubeclient: kubeclient,
 			logger:     logger,
@@ -38,109 +28,39 @@ func NewClientAttenuator(logger *logrus.Logger, config *rest.Config, kubeclient
 // operator group, subscription and CSV.
 type ServiceAccountQuerierFunc func() (reference *corev1.ObjectReference, err error)
 
+func StaticQuerier(ref *corev1.ObjectReference) ServiceAccountQuerierFunc {
+	return func() (*corev1.ObjectReference, error) {
+		return ref, nil
+	}
+}
+
 // ClientAttenuator returns appropriately scoped client(s) to be used for an
 // operator that is being installed.
 type ClientAttenuator struct {
-	// default operator client used by the operator.
-	kubeclient operatorclient.ClientInterface
-
-	// default CR client used by the operator.
-	crclient versioned.Interface
-
-	// default Dynamic Client Runtime used by the operator.
-	dynamicClient dynamic.Interface
-
-	factory   *scopedclient.Factory
 	retriever *BearerTokenRetriever
-	logger    *logrus.Logger
+	logger    logrus.FieldLogger
 }
 
-// AttenuateClientWithServiceAccount returns appropriately scoped client(s) to the caller.
-//
-// client(s) that are bound to OLM cluster-admin role are returned if the querier
-// returns no error and reference to a service account is nil.
-// Otherwise an attempt is made to return attenuated client instance(s).
-func (s *ClientAttenuator) AttenuateClientWithServiceAccount(reference *corev1.ObjectReference) (kubeclient operatorclient.ClientInterface, crclient versioned.Interface, dynamicClient dynamic.Interface, err error) {
-	if reference == nil {
-		// No service account/token has been provided. Return the default client(s).
-		kubeclient = s.kubeclient
-		crclient = s.crclient
-		dynamicClient = s.dynamicClient
-		return
-	}
-
-	token, err := s.retriever.Retrieve(reference)
-	if err != nil {
-		return
-	}
-
-	// Create client(s) bound to the user defined service account.
-	crclient, err = s.factory.NewKubernetesClient(token)
+func (a *ClientAttenuator) AttenuateToServiceAccount(querier ServiceAccountQuerierFunc) (clients.ConfigTransformer, error) {
+	ref, err := querier()
 	if err != nil {
-		return
+		return nil, err
 	}
 
-	kubeclient, err = s.factory.NewOperatorClient(token)
-	if err != nil {
-		return
-	}
-
-	dynamicClient, err = s.factory.NewDynamicClient(token)
-	if err != nil {
-		return
-	}
-
-	return
-}
-
-// AttenuateClient returns appropriately scoped client(s) to the caller.
-//
-// client(s) that are bound to OLM cluster-admin role are returned if the querier
-// returns no error and reference to a service account is nil.
-// Otherwise an attempt is made to return attenuated client instance(s).
-func (s *ClientAttenuator) AttenuateClient(querier ServiceAccountQuerierFunc) (kubeclient operatorclient.ClientInterface, crclient versioned.Interface, dynamicClient dynamic.Interface, err error) {
-	if querier == nil {
-		err = errQuerierNotSpecified
-		return
-	}
-
-	reference, err := querier()
-	if err != nil {
-		return
-	}
-
-	return s.AttenuateClientWithServiceAccount(reference)
-}
-
-// AttenuateOperatorClient returns a scoped operator client instance based on the
-// service account returned by the querier specified.
-func (s *ClientAttenuator) AttenuateOperatorClient(querier ServiceAccountQuerierFunc) (kubeclient operatorclient.ClientInterface, err error) {
-	if querier == nil {
-		err = errQuerierNotSpecified
-		return
-	}
-
-	reference, err := querier()
-	if err != nil {
-		return
-	}
-
-	if reference == nil {
-		// No service account/token has been provided. Return the default client(s).
-		kubeclient = s.kubeclient
-		return
-	}
-
-	token, err := s.retriever.Retrieve(reference)
-	if err != nil {
-		return
+	if ref == nil {
+		return clients.ConfigTransformerFunc(func(config *rest.Config) *rest.Config {
+			return config
+		}), nil
 	}
 
-	// Create client(s) bound to the user defined service account.
-	kubeclient, err = s.factory.NewOperatorClient(token)
+	token, err := a.retriever.Retrieve(ref)
 	if err != nil {
-		return
+		return nil, err
 	}
 
-	return
+	return clients.ConfigTransformerFunc(func(config *rest.Config) *rest.Config {
+		out := rest.AnonymousClientConfig(config)
+		out.BearerToken = token
+		return out
+	}), nil
 }
diff --git a/pkg/lib/scoped/token_retriever.go b/pkg/lib/scoped/token_retriever.go
index 6a3ce39c9..9a1ae1283 100644
--- a/pkg/lib/scoped/token_retriever.go
+++ b/pkg/lib/scoped/token_retriever.go
@@ -14,7 +14,7 @@ import (
 // BearerTokenRetriever retrieves bearer token from a service account.
 type BearerTokenRetriever struct {
 	kubeclient operatorclient.ClientInterface
-	logger     *logrus.Logger
+	logger     logrus.FieldLogger
 }
 
 // Retrieve returns the bearer token for API access from a given service account reference.
@@ -49,7 +49,7 @@ func (r *BearerTokenRetriever) Retrieve(reference *corev1.ObjectReference) (toke
 	return
 }
 
-func getAPISecret(logger *logrus.Entry, kubeclient operatorclient.ClientInterface, sa *corev1.ServiceAccount) (APISecret *corev1.Secret, err error) {
+func getAPISecret(logger logrus.FieldLogger, kubeclient operatorclient.ClientInterface, sa *corev1.ServiceAccount) (APISecret *corev1.Secret, err error) {
 	for _, ref := range sa.Secrets {
 		// corev1.ObjectReference only has Name populated.
 		secret, getErr := kubeclient.KubernetesInterface().CoreV1().Secrets(sa.GetNamespace()).Get(context.TODO(), ref.Name, metav1.GetOptions{})
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index f8126ea83..ec1a18884 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -25,6 +25,8 @@ const (
 	Succeeded       = "succeeded"
 	Failed          = "failed"
 	APPROVAL_LABEL  = "approval"
+	WARNING_LABEL   = "warning"
+	GVK_LABEL       = "gvk"
 )
 
 type MetricsProvider interface {
@@ -191,6 +193,13 @@ var (
 		[]string{Outcome},
 	)
 
+	installPlanWarningCount = prometheus.NewCounter(
+		prometheus.CounterOpts{
+			Name: "installplan_warnings_total",
+			Help: "monotonic count of resources that generated warnings when applied as part of an InstallPlan (for example, due to deprecation)",
+		},
+	)
+
 	// subscriptionSyncCounters keeps a record of the promethues counters emitted by
 	// Subscription objects. The key of a record is the Subscription name, while the value
 	//  is struct containing label values used in the counter
@@ -218,6 +227,7 @@ func RegisterCatalog() {
 	prometheus.MustRegister(catalogSourceReady)
 	prometheus.MustRegister(SubscriptionSyncCount)
 	prometheus.MustRegister(dependencyResolutionSummary)
+	prometheus.MustRegister(installPlanWarningCount)
 }
 
 func CounterForSubscription(name, installedCSV, channelName, packageName, planApprovalStrategy string) prometheus.Counter {
@@ -320,3 +330,7 @@ func RegisterDependencyResolutionSuccess(duration time.Duration) {
 func RegisterDependencyResolutionFailure(duration time.Duration) {
 	dependencyResolutionSummary.WithLabelValues(Failed).Observe(duration.Seconds())
 }
+
+func EmitInstallPlanWarning() {
+	installPlanWarningCount.Inc()
+}
diff --git a/vendor/github.com/alessio/shellescape/.gitignore b/vendor/github.com/alessio/shellescape/.gitignore
index daf913b1b..4ba7c2d13 100644
--- a/vendor/github.com/alessio/shellescape/.gitignore
+++ b/vendor/github.com/alessio/shellescape/.gitignore
@@ -22,3 +22,7 @@ _testmain.go
 *.exe
 *.test
 *.prof
+
+.idea/
+
+escargs
diff --git a/vendor/github.com/alessio/shellescape/.golangci.yml b/vendor/github.com/alessio/shellescape/.golangci.yml
new file mode 100644
index 000000000..cd4a17e44
--- /dev/null
+++ b/vendor/github.com/alessio/shellescape/.golangci.yml
@@ -0,0 +1,64 @@
+# run:
+#   # timeout for analysis, e.g. 30s, 5m, default is 1m
+#   timeout: 5m
+
+linters:
+  disable-all: true
+  enable:
+    - bodyclose
+    - deadcode
+    - depguard
+    - dogsled
+    - goconst
+    - gocritic
+    - gofmt
+    - goimports
+    - golint
+    - gosec
+    - gosimple
+    - govet
+    - ineffassign
+    - interfacer
+    - maligned
+    - misspell
+    - prealloc
+    - scopelint
+    - staticcheck
+    - structcheck
+    - stylecheck
+    - typecheck
+    - unconvert
+    - unparam
+    - unused
+    - misspell
+    - wsl
+
+issues:
+  exclude-rules:
+    - text: "Use of weak random number generator"
+      linters:
+        - gosec
+    - text: "comment on exported var"
+      linters:
+        - golint
+    - text: "don't use an underscore in package name"
+      linters:
+        - golint
+    - text: "ST1003:"
+      linters:
+        - stylecheck
+    # FIXME: Disabled until golangci-lint updates stylecheck with this fix:
+    # https://github.com/dominikh/go-tools/issues/389
+    - text: "ST1016:"
+      linters:
+        - stylecheck
+
+linters-settings:
+  dogsled:
+    max-blank-identifiers: 3
+  maligned:
+    # print struct with more effective memory layout or not, false by default
+    suggest-new: true
+
+run:
+  tests: false
diff --git a/vendor/github.com/alessio/shellescape/.goreleaser.yml b/vendor/github.com/alessio/shellescape/.goreleaser.yml
new file mode 100644
index 000000000..064c9374d
--- /dev/null
+++ b/vendor/github.com/alessio/shellescape/.goreleaser.yml
@@ -0,0 +1,33 @@
+# This is an example goreleaser.yaml file with some sane defaults.
+# Make sure to check the documentation at http://goreleaser.com
+before:
+  hooks:
+    # You may remove this if you don't use go modules.
+    - go mod download
+    # you may remove this if you don't need go generate
+    - go generate ./...
+builds:
+  - env:
+      - CGO_ENABLED=0
+    main: ./cmd/escargs
+    goos:
+      - linux
+      - windows
+      - darwin
+archives:
+  - replacements:
+      darwin: Darwin
+      linux: Linux
+      windows: Windows
+      386: i386
+      amd64: x86_64
+checksum:
+  name_template: 'checksums.txt'
+snapshot:
+  name_template: "{{ .Tag }}-next"
+changelog:
+  sort: asc
+  filters:
+    exclude:
+      - '^docs:'
+      - '^test:'
diff --git a/vendor/github.com/alessio/shellescape/.travis.yml b/vendor/github.com/alessio/shellescape/.travis.yml
deleted file mode 100644
index f5763c5aa..000000000
--- a/vendor/github.com/alessio/shellescape/.travis.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-language: go
-
-go:
-  - 1.14
diff --git a/vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md b/vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..e8eda6062
--- /dev/null
+++ b/vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at alessio@debian.org. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/vendor/github.com/alessio/shellescape/README.md b/vendor/github.com/alessio/shellescape/README.md
index ce9e33f42..910bb253b 100644
--- a/vendor/github.com/alessio/shellescape/README.md
+++ b/vendor/github.com/alessio/shellescape/README.md
@@ -1,7 +1,10 @@
-[![GoDoc](https://godoc.org/github.com/alessio/shellescape?status.svg)](https://godoc.org/github.com/alessio/shellescape) 
-[![Travis-CI Status](https://api.travis-ci.org/alessio/shellescape.png?branch=master)](http://travis-ci.org/#!/alessio/shellescape)
+![Build](https://github.com/alessio/shellescape/workflows/Build/badge.svg)
+[![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/alessio/shellescape?tab=overview)
+[![sourcegraph](https://sourcegraph.com/github.com/alessio/shellescape/-/badge.svg)](https://sourcegraph.com/github.com/alessio/shellescape)
+[![codecov](https://codecov.io/gh/alessio/shellescape/branch/master/graph/badge.svg)](https://codecov.io/gh/alessio/shellescape)
 [![Coverage](https://gocover.io/_badge/github.com/alessio/shellescape)](https://gocover.io/github.com/alessio/shellescape)
-[![Coverage Status](https://coveralls.io/repos/github/alessio/shellescape/badge.svg?branch=master)](https://coveralls.io/github/alessio/shellescape?branch=master)
+[![Go Report Card](https://goreportcard.com/badge/github.com/alessio/shellescape)](https://goreportcard.com/report/github.com/alessio/shellescape)
+
 # shellescape
 Escape arbitrary strings for safe use as command line arguments.
 ## Contents of the package
@@ -11,8 +14,8 @@ shell-escaped copy of a string. This functionality could be helpful
 in those cases where it is known that the output of a Go program will
 be appended to/used in the context of shell programs' command line arguments.
 
-This work was inspired by the Python original package [shellescape] 
-(https://pypi.python.org/pypi/shellescape).
+This work was inspired by the Python original package
+[shellescape](https://pypi.python.org/pypi/shellescape).
 
 ## Usage
 
@@ -33,7 +36,7 @@ func main() {
 _[See in Go Playground](https://play.golang.org/p/Wj2WoUfH_d)_
 
 Especially when creating pipeline of commands which might end up being
-executed by a shell interpreter, tt is particularly unsafe to not
+executed by a shell interpreter, it is particularly unsafe to not
 escape arguments.
 
 `shellescape.Quote()` comes in handy and to safely escape strings:
diff --git a/vendor/github.com/alessio/shellescape/shellescape.go b/vendor/github.com/alessio/shellescape/shellescape.go
index 56725d182..dc34a556a 100644
--- a/vendor/github.com/alessio/shellescape/shellescape.go
+++ b/vendor/github.com/alessio/shellescape/shellescape.go
@@ -17,6 +17,7 @@ be appended to/used in the context of shell programs' command line arguments.
 import (
 	"regexp"
 	"strings"
+	"unicode"
 )
 
 var pattern *regexp.Regexp
@@ -31,9 +32,35 @@ func Quote(s string) string {
 	if len(s) == 0 {
 		return "''"
 	}
+
 	if pattern.MatchString(s) {
-		return "'" + strings.Replace(s, "'", "'\"'\"'", -1) + "'"
+		return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
 	}
 
 	return s
 }
+
+// QuoteCommand returns a shell-escaped version of the slice of strings.
+// The returned value is a string that can safely be used as shell command arguments.
+func QuoteCommand(args []string) string {
+	l := make([]string, len(args))
+
+	for i, s := range args {
+		l[i] = Quote(s)
+	}
+
+	return strings.Join(l, " ")
+}
+
+// StripUnsafe remove non-printable runes, e.g. control characters in
+// a string that is meant  for consumption by terminals that support
+// control characters.
+func StripUnsafe(s string) string {
+	return strings.Map(func(r rune) rune {
+		if unicode.IsPrint(r) {
+			return r
+		}
+
+		return -1
+	}, s)
+}
diff --git a/vendor/github.com/evanphx/json-patch/v5/LICENSE b/vendor/github.com/evanphx/json-patch/v5/LICENSE
index 0eb9b72d8..df76d7d77 100644
--- a/vendor/github.com/evanphx/json-patch/v5/LICENSE
+++ b/vendor/github.com/evanphx/json-patch/v5/LICENSE
@@ -6,7 +6,7 @@ modification, are permitted provided that the following conditions are met:
 
 * Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
-* Redistributions in binary form must reproduce the above copyright notice
+* Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 * Neither the name of the Evan Phoenix nor the names of its contributors 
diff --git a/vendor/github.com/evanphx/json-patch/v5/merge.go b/vendor/github.com/evanphx/json-patch/v5/merge.go
index 14e8bb5ce..7219316d6 100644
--- a/vendor/github.com/evanphx/json-patch/v5/merge.go
+++ b/vendor/github.com/evanphx/json-patch/v5/merge.go
@@ -27,21 +27,31 @@ func merge(cur, patch *lazyNode, mergeMerge bool) *lazyNode {
 }
 
 func mergeDocs(doc, patch *partialDoc, mergeMerge bool) {
-	for k, v := range *patch {
+	for k, v := range patch.obj {
 		if v == nil {
 			if mergeMerge {
-				(*doc)[k] = nil
+				idx := -1
+				for i, key := range doc.keys {
+					if key == k {
+						idx = i
+						break
+					}
+				}
+				if idx == -1 {
+					doc.keys = append(doc.keys, k)
+				}
+				doc.obj[k] = nil
 			} else {
-				delete(*doc, k)
+				_ = doc.remove(k, &ApplyOptions{})
 			}
 		} else {
-			cur, ok := (*doc)[k]
+			cur, ok := doc.obj[k]
 
 			if !ok || cur == nil {
 				pruneNulls(v)
-				(*doc)[k] = v
+				_ = doc.set(k, v, &ApplyOptions{})
 			} else {
-				(*doc)[k] = merge(cur, v, mergeMerge)
+				_ = doc.set(k, merge(cur, v, mergeMerge), &ApplyOptions{})
 			}
 		}
 	}
@@ -62,9 +72,9 @@ func pruneNulls(n *lazyNode) {
 }
 
 func pruneDocNulls(doc *partialDoc) *partialDoc {
-	for k, v := range *doc {
+	for k, v := range doc.obj {
 		if v == nil {
-			delete(*doc, k)
+			_ = doc.remove(k, &ApplyOptions{})
 		} else {
 			pruneNulls(v)
 		}
@@ -113,19 +123,19 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
 
 	patchErr := json.Unmarshal(patchData, patch)
 
-	if _, ok := docErr.(*json.SyntaxError); ok {
+	if isSyntaxError(docErr) {
 		return nil, errBadJSONDoc
 	}
 
-	if _, ok := patchErr.(*json.SyntaxError); ok {
+	if isSyntaxError(patchErr) {
 		return nil, errBadJSONPatch
 	}
 
-	if docErr == nil && *doc == nil {
+	if docErr == nil && doc.obj == nil {
 		return nil, errBadJSONDoc
 	}
 
-	if patchErr == nil && *patch == nil {
+	if patchErr == nil && patch.obj == nil {
 		return nil, errBadJSONPatch
 	}
 
@@ -162,6 +172,16 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
 	return json.Marshal(doc)
 }
 
+func isSyntaxError(err error) bool {
+	if _, ok := err.(*json.SyntaxError); ok {
+		return true
+	}
+	if _, ok := err.(*syntaxError); ok {
+		return true
+	}
+	return false
+}
+
 // resemblesJSONArray indicates whether the byte-slice "appears" to be
 // a JSON array or not.
 // False-positives are possible, as this function does not check the internal
diff --git a/vendor/github.com/evanphx/json-patch/v5/patch.go b/vendor/github.com/evanphx/json-patch/v5/patch.go
index e73675987..fc860aca3 100644
--- a/vendor/github.com/evanphx/json-patch/v5/patch.go
+++ b/vendor/github.com/evanphx/json-patch/v5/patch.go
@@ -24,6 +24,10 @@ var (
 	// AccumulatedCopySizeLimit limits the total size increase in bytes caused by
 	// "copy" operations in a patch.
 	AccumulatedCopySizeLimit int64 = 0
+	startObject                    = json.Delim('{')
+	endObject                      = json.Delim('}')
+	startArray                     = json.Delim('[')
+	endArray                     = json.Delim(']')
 )
 
 var (
@@ -32,11 +36,15 @@ var (
 	ErrUnknownType  = errors.New("unknown object type")
 	ErrInvalid      = errors.New("invalid state detected")
 	ErrInvalidIndex = errors.New("invalid index referenced")
+
+	rawJSONArray  = []byte("[]")
+	rawJSONObject = []byte("{}")
+	rawJSONNull   = []byte("null")
 )
 
 type lazyNode struct {
 	raw   *json.RawMessage
-	doc   partialDoc
+	doc   *partialDoc
 	ary   partialArray
 	which int
 }
@@ -47,20 +55,58 @@ type Operation map[string]*json.RawMessage
 // Patch is an ordered collection of Operations.
 type Patch []Operation
 
-type partialDoc map[string]*lazyNode
+type partialDoc struct {
+	keys []string
+	obj   map[string]*lazyNode
+}
+
 type partialArray []*lazyNode
 
 type container interface {
-	get(key string) (*lazyNode, error)
-	set(key string, val *lazyNode) error
-	add(key string, val *lazyNode) error
-	remove(key string) error
+	get(key string, options *ApplyOptions) (*lazyNode, error)
+	set(key string, val *lazyNode, options *ApplyOptions) error
+	add(key string, val *lazyNode, options *ApplyOptions) error
+	remove(key string, options *ApplyOptions) error
+}
+
+// ApplyOptions specifies options for calls to ApplyWithOptions.
+// Use NewApplyOptions to obtain default values for ApplyOptions.
+type ApplyOptions struct {
+	// SupportNegativeIndices decides whether to support non-standard practice of
+	// allowing negative indices to mean indices starting at the end of an array.
+	// Default to true.
+	SupportNegativeIndices bool
+	// AccumulatedCopySizeLimit limits the total size increase in bytes caused by
+	// "copy" operations in a patch.
+	AccumulatedCopySizeLimit int64
+	// AllowMissingPathOnRemove indicates whether to fail "remove" operations when the target path is missing.
+	// Default to false.
+	AllowMissingPathOnRemove bool
+	// EnsurePathExistsOnAdd instructs json-patch to recursively create the missing parts of path on "add" operation.
+	// Default to false.
+	EnsurePathExistsOnAdd bool
+}
+
+// NewApplyOptions creates a default set of options for calls to ApplyWithOptions.
+func NewApplyOptions() *ApplyOptions {
+	return &ApplyOptions{
+		SupportNegativeIndices:   SupportNegativeIndices,
+		AccumulatedCopySizeLimit: AccumulatedCopySizeLimit,
+		AllowMissingPathOnRemove: false,
+		EnsurePathExistsOnAdd:    false,
+	}
 }
 
 func newLazyNode(raw *json.RawMessage) *lazyNode {
 	return &lazyNode{raw: raw, doc: nil, ary: nil, which: eRaw}
 }
 
+func newRawMessage(buf []byte) *json.RawMessage {
+	ra := make(json.RawMessage, len(buf))
+	copy(ra, buf)
+	return &ra
+}
+
 func (n *lazyNode) MarshalJSON() ([]byte, error) {
 	switch n.which {
 	case eRaw:
@@ -82,6 +128,109 @@ func (n *lazyNode) UnmarshalJSON(data []byte) error {
 	return nil
 }
 
+func (n *partialDoc) MarshalJSON() ([]byte, error) {
+	var buf bytes.Buffer
+	if _, err := buf.WriteString("{"); err != nil {
+		return nil, err
+	}
+	for i, k := range n.keys {
+		if i > 0 {
+			if _, err := buf.WriteString(", "); err != nil {
+				return nil, err
+			}
+		}
+		key, err := json.Marshal(k)
+		if err != nil {
+			return nil, err
+		}
+		if _, err := buf.Write(key); err != nil {
+			return nil, err
+		}
+		if _, err := buf.WriteString(": "); err != nil {
+			return nil, err
+		}
+		value, err := json.Marshal(n.obj[k])
+		if err != nil {
+			return nil, err
+		}
+		if _, err := buf.Write(value); err != nil {
+			return nil, err
+		}
+	}
+	if _, err := buf.WriteString("}"); err != nil {
+		return nil, err
+	}
+	return buf.Bytes(), nil
+}
+
+type syntaxError struct {
+	msg string
+}
+
+func (err *syntaxError) Error() string {
+	return err.msg
+}
+
+func (n *partialDoc) UnmarshalJSON(data []byte) error {
+	if err := json.Unmarshal(data, &n.obj); err != nil {
+		return err
+	}
+	buffer := bytes.NewBuffer(data)
+	d := json.NewDecoder(buffer)
+	if t, err := d.Token(); err != nil {
+		return err
+	} else if t != startObject {
+		return &syntaxError{fmt.Sprintf("unexpected JSON token in document node: %s", t)}
+	}
+	for d.More() {
+		k, err := d.Token()
+		if err != nil {
+			return err
+		}
+		key, ok := k.(string)
+		if !ok {
+			return &syntaxError{fmt.Sprintf("unexpected JSON token as document node key: %s", k)}
+		}
+		if err := skipValue(d); err != nil {
+			return err
+		}
+		n.keys = append(n.keys, key)
+	}
+	return nil
+}
+
+func skipValue(d *json.Decoder) error {
+	t, err := d.Token()
+	if err != nil {
+		return err
+	}
+	if t != startObject && t != startArray {
+		return nil
+	}
+	for d.More() {
+		if t == startObject {
+			// consume key token
+			if _, err := d.Token(); err != nil {
+				return err
+			}
+		}
+		if err := skipValue(d); err != nil {
+			return err
+		}
+	}
+	end, err := d.Token()
+	if err != nil {
+		return err
+	}
+	if t == startObject && end != endObject {
+		return &syntaxError{msg: "expected close object token"}
+	}
+	if t == startArray && end != endArray {
+		return &syntaxError{msg: "expected close object token"}
+	}
+	return nil
+}
+
 func deepCopy(src *lazyNode) (*lazyNode, int, error) {
 	if src == nil {
 		return nil, 0, nil
@@ -91,14 +240,12 @@ func deepCopy(src *lazyNode) (*lazyNode, int, error) {
 		return nil, 0, err
 	}
 	sz := len(a)
-	ra := make(json.RawMessage, sz)
-	copy(ra, a)
-	return newLazyNode(&ra), sz, nil
+	return newLazyNode(newRawMessage(a)), sz, nil
 }
 
 func (n *lazyNode) intoDoc() (*partialDoc, error) {
 	if n.which == eDoc {
-		return &n.doc, nil
+		return n.doc, nil
 	}
 
 	if n.raw == nil {
@@ -112,7 +259,7 @@ func (n *lazyNode) intoDoc() (*partialDoc, error) {
 	}
 
 	n.which = eDoc
-	return &n.doc, nil
+	return n.doc, nil
 }
 
 func (n *lazyNode) intoAry() (*partialArray, error) {
@@ -202,12 +349,12 @@ func (n *lazyNode) equal(o *lazyNode) bool {
 			return false
 		}
 
-		if len(n.doc) != len(o.doc) {
+		if len(n.doc.obj) != len(o.doc.obj) {
 			return false
 		}
 
-		for k, v := range n.doc {
-			ov, ok := o.doc[k]
+		for k, v := range n.doc.obj {
+			ov, ok := o.doc.obj[k]
 
 			if !ok {
 				return false
@@ -340,7 +487,7 @@ Loop:
 	return false
 }
 
-func findObject(pd *container, path string) (container, string) {
+func findObject(pd *container, path string, options *ApplyOptions) (container, string) {
 	doc := *pd
 
 	split := strings.Split(path, "/")
@@ -357,7 +504,7 @@ func findObject(pd *container, path string) (container, string) {
 
 	for _, part := range parts {
 
-		next, ok := doc.get(decodePatchKey(part))
+		next, ok := doc.get(decodePatchKey(part), options)
 
 		if next == nil || ok != nil {
 			return nil, ""
@@ -381,46 +528,76 @@ func findObject(pd *container, path string) (container, string) {
 	return doc, decodePatchKey(key)
 }
 
-func (d *partialDoc) set(key string, val *lazyNode) error {
-	(*d)[key] = val
+func (d *partialDoc) set(key string, val *lazyNode, options *ApplyOptions) error {
+	found := false
+	for _, k := range d.keys {
+		if k == key {
+			found = true
+			break
+		}
+	}
+	if !found {
+		d.keys = append(d.keys, key)
+	}
+	d.obj[key] = val
 	return nil
 }
 
-func (d *partialDoc) add(key string, val *lazyNode) error {
-	(*d)[key] = val
-	return nil
+func (d *partialDoc) add(key string, val *lazyNode, options *ApplyOptions) error {
+	return d.set(key, val, options)
 }
 
-func (d *partialDoc) get(key string) (*lazyNode, error) {
-	v, ok := (*d)[key]
+func (d *partialDoc) get(key string, options *ApplyOptions) (*lazyNode, error) {
+	v, ok := d.obj[key]
 	if !ok {
 		return v, errors.Wrapf(ErrMissing, "unable to get nonexistent key: %s", key)
 	}
 	return v, nil
 }
 
-func (d *partialDoc) remove(key string) error {
-	_, ok := (*d)[key]
+func (d *partialDoc) remove(key string, options *ApplyOptions) error {
+	_, ok := d.obj[key]
 	if !ok {
+		if options.AllowMissingPathOnRemove {
+			return nil
+		}
 		return errors.Wrapf(ErrMissing, "unable to remove nonexistent key: %s", key)
 	}
-
-	delete(*d, key)
+	idx := -1
+	for i, k := range d.keys {
+		if k == key {
+			idx = i
+			break
+		}
+	}
+	d.keys = append(d.keys[0:idx], d.keys[idx+1:]...)
+	delete(d.obj, key)
 	return nil
 }
 
 // set should only be used to implement the "replace" operation, so "key" must
 // be an already existing index in "d".
-func (d *partialArray) set(key string, val *lazyNode) error {
+func (d *partialArray) set(key string, val *lazyNode, options *ApplyOptions) error {
 	idx, err := strconv.Atoi(key)
 	if err != nil {
 		return err
 	}
+
+	if idx < 0 {
+		if !options.SupportNegativeIndices {
+			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		if idx < -len(*d) {
+			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		idx += len(*d)
+	}
+
 	(*d)[idx] = val
 	return nil
 }
 
-func (d *partialArray) add(key string, val *lazyNode) error {
+func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) error {
 	if key == "-" {
 		*d = append(*d, val)
 		return nil
@@ -442,7 +619,7 @@ func (d *partialArray) add(key string, val *lazyNode) error {
 	}
 
 	if idx < 0 {
-		if !SupportNegativeIndices {
+		if !options.SupportNegativeIndices {
 			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
 		}
 		if idx < -len(ary) {
@@ -459,13 +636,23 @@ func (d *partialArray) add(key string, val *lazyNode) error {
 	return nil
 }
 
-func (d *partialArray) get(key string) (*lazyNode, error) {
+func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error) {
 	idx, err := strconv.Atoi(key)
 
 	if err != nil {
 		return nil, err
 	}
 
+	if idx < 0 {
+		if !options.SupportNegativeIndices {
+			return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		if idx < -len(*d) {
+			return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		idx += len(*d)
+	}
+
 	if idx >= len(*d) {
 		return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
 	}
@@ -473,7 +660,7 @@ func (d *partialArray) get(key string) (*lazyNode, error) {
 	return (*d)[idx], nil
 }
 
-func (d *partialArray) remove(key string) error {
+func (d *partialArray) remove(key string, options *ApplyOptions) error {
 	idx, err := strconv.Atoi(key)
 	if err != nil {
 		return err
@@ -482,14 +669,20 @@ func (d *partialArray) remove(key string) error {
 	cur := *d
 
 	if idx >= len(cur) {
+		if options.AllowMissingPathOnRemove {
+			return nil
+		}
 		return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
 	}
 
 	if idx < 0 {
-		if !SupportNegativeIndices {
+		if !options.SupportNegativeIndices {
 			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
 		}
 		if idx < -len(cur) {
+			if options.AllowMissingPathOnRemove {
+				return nil
+			}
 			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
 		}
 		idx += len(cur)
@@ -502,22 +695,29 @@ func (d *partialArray) remove(key string) error {
 
 	*d = ary
 	return nil
-
 }
 
-func (p Patch) add(doc *container, op Operation) error {
+func (p Patch) add(doc *container, op Operation, options *ApplyOptions) error {
 	path, err := op.Path()
 	if err != nil {
 		return errors.Wrapf(ErrMissing, "add operation failed to decode path")
 	}
 
-	con, key := findObject(doc, path)
+	if options.EnsurePathExistsOnAdd {
+		err = ensurePathExists(doc, path, options)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	con, key := findObject(doc, path, options)
 
 	if con == nil {
 		return errors.Wrapf(ErrMissing, "add operation does not apply: doc is missing path: \"%s\"", path)
 	}
 
-	err = con.add(key, op.value())
+	err = con.add(key, op.value(), options)
 	if err != nil {
 		return errors.Wrapf(err, "error in add for path: '%s'", path)
 	}
@@ -525,19 +725,113 @@ func (p Patch) add(doc *container, op Operation) error {
 	return nil
 }
 
-func (p Patch) remove(doc *container, op Operation) error {
+// Given a document and a path to a key, walk the path and create all missing elements
+// creating objects and arrays as needed.
+func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
+	doc := *pd
+
+	var err error
+	var arrIndex int
+
+	split := strings.Split(path, "/")
+
+	if len(split) < 2 {
+		return nil
+	}
+
+	parts := split[1:]
+
+	for pi, part := range parts {
+
+		// Have we reached the key part of the path?
+		// If yes, we're done.
+		if pi == len(parts)-1 {
+			return nil
+		}
+
+		target, ok := doc.get(decodePatchKey(part), options)
+
+		if target == nil || ok != nil {
+
+			// If the current container is an array which has fewer elements than our target index,
+			// pad the current container with nulls.
+			if arrIndex, err = strconv.Atoi(part); err == nil {
+				pa, ok := doc.(*partialArray)
+
+				if ok && arrIndex >= len(*pa)+1 {
+					// Pad the array with null values up to the required index.
+					for i := len(*pa); i <= arrIndex-1; i++ {
+						doc.add(strconv.Itoa(i), newLazyNode(newRawMessage(rawJSONNull)), options)
+					}
+				}
+			}
+
+			// Check if the next part is a numeric index.
+			// If yes, then create an array, otherwise, create an object.
+			if arrIndex, err = strconv.Atoi(parts[pi+1]); err == nil {
+				if arrIndex < 0 {
+
+					if !options.SupportNegativeIndices {
+						return errors.Wrapf(ErrInvalidIndex, "Unable to ensure path for invalid index: %d", arrIndex)
+					}
+
+					if arrIndex < -1 {
+						return errors.Wrapf(ErrInvalidIndex, "Unable to ensure path for negative index other than -1: %d", arrIndex)
+					}
+
+					arrIndex = 0
+				}
+
+				newNode := newLazyNode(newRawMessage(rawJSONArray))
+				doc.add(part, newNode, options)
+				doc, _ = newNode.intoAry()
+
+				// Pad the new array with null values up to the required index.
+				for i := 0; i < arrIndex; i++ {
+					doc.add(strconv.Itoa(i), newLazyNode(newRawMessage(rawJSONNull)), options)
+				}
+			} else {
+				newNode := newLazyNode(newRawMessage(rawJSONObject))
+
+				doc.add(part, newNode, options)
+				doc, _ = newNode.intoDoc()
+			}
+		} else {
+			if isArray(*target.raw) {
+				doc, err = target.intoAry()
+
+				if err != nil {
+					return err
+				}
+			} else {
+				doc, err = target.intoDoc()
+
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func (p Patch) remove(doc *container, op Operation, options *ApplyOptions) error {
 	path, err := op.Path()
 	if err != nil {
 		return errors.Wrapf(ErrMissing, "remove operation failed to decode path")
 	}
 
-	con, key := findObject(doc, path)
+	con, key := findObject(doc, path, options)
 
 	if con == nil {
+		if options.AllowMissingPathOnRemove {
+			return nil
+		}
 		return errors.Wrapf(ErrMissing, "remove operation does not apply: doc is missing path: \"%s\"", path)
 	}
 
-	err = con.remove(key)
+	err = con.remove(key, options)
 	if err != nil {
 		return errors.Wrapf(err, "error in remove for path: '%s'", path)
 	}
@@ -545,24 +839,24 @@ func (p Patch) remove(doc *container, op Operation) error {
 	return nil
 }
 
-func (p Patch) replace(doc *container, op Operation) error {
+func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) error {
 	path, err := op.Path()
 	if err != nil {
 		return errors.Wrapf(err, "replace operation failed to decode path")
 	}
 
-	con, key := findObject(doc, path)
+	con, key := findObject(doc, path, options)
 
 	if con == nil {
 		return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing path: %s", path)
 	}
 
-	_, ok := con.get(key)
+	_, ok := con.get(key, options)
 	if ok != nil {
 		return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing key: %s", path)
 	}
 
-	err = con.set(key, op.value())
+	err = con.set(key, op.value(), options)
 	if err != nil {
 		return errors.Wrapf(err, "error in remove for path: '%s'", path)
 	}
@@ -570,24 +864,24 @@ func (p Patch) replace(doc *container, op Operation) error {
 	return nil
 }
 
-func (p Patch) move(doc *container, op Operation) error {
+func (p Patch) move(doc *container, op Operation, options *ApplyOptions) error {
 	from, err := op.From()
 	if err != nil {
 		return errors.Wrapf(err, "move operation failed to decode from")
 	}
 
-	con, key := findObject(doc, from)
+	con, key := findObject(doc, from, options)
 
 	if con == nil {
 		return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing from path: %s", from)
 	}
 
-	val, err := con.get(key)
+	val, err := con.get(key, options)
 	if err != nil {
 		return errors.Wrapf(err, "error in move for path: '%s'", key)
 	}
 
-	err = con.remove(key)
+	err = con.remove(key, options)
 	if err != nil {
 		return errors.Wrapf(err, "error in move for path: '%s'", key)
 	}
@@ -597,13 +891,13 @@ func (p Patch) move(doc *container, op Operation) error {
 		return errors.Wrapf(err, "move operation failed to decode path")
 	}
 
-	con, key = findObject(doc, path)
+	con, key = findObject(doc, path, options)
 
 	if con == nil {
 		return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing destination path: %s", path)
 	}
 
-	err = con.add(key, val)
+	err = con.add(key, val, options)
 	if err != nil {
 		return errors.Wrapf(err, "error in move for path: '%s'", path)
 	}
@@ -611,19 +905,19 @@ func (p Patch) move(doc *container, op Operation) error {
 	return nil
 }
 
-func (p Patch) test(doc *container, op Operation) error {
+func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error {
 	path, err := op.Path()
 	if err != nil {
 		return errors.Wrapf(err, "test operation failed to decode path")
 	}
 
-	con, key := findObject(doc, path)
+	con, key := findObject(doc, path, options)
 
 	if con == nil {
 		return errors.Wrapf(ErrMissing, "test operation does not apply: is missing path: %s", path)
 	}
 
-	val, err := con.get(key)
+	val, err := con.get(key, options)
 	if err != nil && errors.Cause(err) != ErrMissing {
 		return errors.Wrapf(err, "error in test for path: '%s'", path)
 	}
@@ -644,19 +938,19 @@ func (p Patch) test(doc *container, op Operation) error {
 	return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
 }
 
-func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) error {
+func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64, options *ApplyOptions) error {
 	from, err := op.From()
 	if err != nil {
 		return errors.Wrapf(err, "copy operation failed to decode from")
 	}
 
-	con, key := findObject(doc, from)
+	con, key := findObject(doc, from, options)
 
 	if con == nil {
 		return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing from path: %s", from)
 	}
 
-	val, err := con.get(key)
+	val, err := con.get(key, options)
 	if err != nil {
 		return errors.Wrapf(err, "error in copy for from: '%s'", from)
 	}
@@ -666,7 +960,7 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) er
 		return errors.Wrapf(ErrMissing, "copy operation failed to decode path")
 	}
 
-	con, key = findObject(doc, path)
+	con, key = findObject(doc, path, options)
 
 	if con == nil {
 		return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing destination path: %s", path)
@@ -678,11 +972,11 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) er
 	}
 
 	(*accumulatedCopySize) += int64(sz)
-	if AccumulatedCopySizeLimit > 0 && *accumulatedCopySize > AccumulatedCopySizeLimit {
-		return NewAccumulatedCopySizeError(AccumulatedCopySizeLimit, *accumulatedCopySize)
+	if options.AccumulatedCopySizeLimit > 0 && *accumulatedCopySize > options.AccumulatedCopySizeLimit {
+		return NewAccumulatedCopySizeError(options.AccumulatedCopySizeLimit, *accumulatedCopySize)
 	}
 
-	err = con.add(key, valCopy)
+	err = con.add(key, valCopy, options)
 	if err != nil {
 		return errors.Wrapf(err, "error while adding value during copy")
 	}
@@ -692,13 +986,8 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) er
 
 // Equal indicates if 2 JSON documents have the same structural equality.
 func Equal(a, b []byte) bool {
-	ra := make(json.RawMessage, len(a))
-	copy(ra, a)
-	la := newLazyNode(&ra)
-
-	rb := make(json.RawMessage, len(b))
-	copy(rb, b)
-	lb := newLazyNode(&rb)
+	la := newLazyNode(newRawMessage(a))
+	lb := newLazyNode(newRawMessage(b))
 
 	return la.equal(lb)
 }
@@ -719,12 +1008,24 @@ func DecodePatch(buf []byte) (Patch, error) {
 // Apply mutates a JSON document according to the patch, and returns the new
 // document.
 func (p Patch) Apply(doc []byte) ([]byte, error) {
-	return p.ApplyIndent(doc, "")
+	return p.ApplyWithOptions(doc, NewApplyOptions())
+}
+
+// ApplyWithOptions mutates a JSON document according to the patch and the passed in ApplyOptions.
+// It returns the new document.
+func (p Patch) ApplyWithOptions(doc []byte, options *ApplyOptions) ([]byte, error) {
+	return p.ApplyIndentWithOptions(doc, "", options)
 }
 
 // ApplyIndent mutates a JSON document according to the patch, and returns the new
 // document indented.
 func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) {
+	return p.ApplyIndentWithOptions(doc, indent, NewApplyOptions())
+}
+
+// ApplyIndentWithOptions mutates a JSON document according to the patch and the passed in ApplyOptions.
+// It returns the new document indented.
+func (p Patch) ApplyIndentWithOptions(doc []byte, indent string, options *ApplyOptions) ([]byte, error) {
 	var pd container
 	if doc[0] == '[' {
 		pd = &partialArray{}
@@ -745,17 +1046,17 @@ func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) {
 	for _, op := range p {
 		switch op.Kind() {
 		case "add":
-			err = p.add(&pd, op)
+			err = p.add(&pd, op, options)
 		case "remove":
-			err = p.remove(&pd, op)
+			err = p.remove(&pd, op, options)
 		case "replace":
-			err = p.replace(&pd, op)
+			err = p.replace(&pd, op, options)
 		case "move":
-			err = p.move(&pd, op)
+			err = p.move(&pd, op, options)
 		case "test":
-			err = p.test(&pd, op)
+			err = p.test(&pd, op, options)
 		case "copy":
-			err = p.copy(&pd, op, &accumulatedCopySize)
+			err = p.copy(&pd, op, &accumulatedCopySize, options)
 		default:
 			err = fmt.Errorf("Unexpected kind: %s", op.Kind())
 		}
diff --git a/vendor/gopkg.in/yaml.v3/.travis.yml b/vendor/gopkg.in/yaml.v3/.travis.yml
deleted file mode 100644
index a130fe883..000000000
--- a/vendor/gopkg.in/yaml.v3/.travis.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-language: go
-
-go:
-    - "1.4.x"
-    - "1.5.x"
-    - "1.6.x"
-    - "1.7.x"
-    - "1.8.x"
-    - "1.9.x"
-    - "1.10.x"
-    - "1.11.x"
-    - "1.12.x"
-    - "1.13.x"
-    - "1.14.x"
-    - "tip"
-
-go_import_path: gopkg.in/yaml.v3
diff --git a/vendor/gopkg.in/yaml.v3/decode.go b/vendor/gopkg.in/yaml.v3/decode.go
index 21c0dacfd..df36e3a30 100644
--- a/vendor/gopkg.in/yaml.v3/decode.go
+++ b/vendor/gopkg.in/yaml.v3/decode.go
@@ -399,7 +399,7 @@ func (d *decoder) callObsoleteUnmarshaler(n *Node, u obsoleteUnmarshaler) (good
 //
 // If n holds a null value, prepare returns before doing anything.
 func (d *decoder) prepare(n *Node, out reflect.Value) (newout reflect.Value, unmarshaled, good bool) {
-	if n.ShortTag() == nullTag || n.Kind == 0 && n.IsZero() {
+	if n.ShortTag() == nullTag {
 		return out, false, false
 	}
 	again := true
@@ -808,8 +808,10 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) {
 		}
 	}
 
+	mapIsNew := false
 	if out.IsNil() {
 		out.Set(reflect.MakeMap(outt))
+		mapIsNew = true
 	}
 	for i := 0; i < l; i += 2 {
 		if isMerge(n.Content[i]) {
@@ -826,7 +828,7 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) {
 				failf("invalid map key: %#v", k.Interface())
 			}
 			e := reflect.New(et).Elem()
-			if d.unmarshal(n.Content[i+1], e) {
+			if d.unmarshal(n.Content[i+1], e) || n.Content[i+1].ShortTag() == nullTag && (mapIsNew || !out.MapIndex(k).IsValid()) {
 				out.SetMapIndex(k, e)
 			}
 		}
diff --git a/vendor/gopkg.in/yaml.v3/emitterc.go b/vendor/gopkg.in/yaml.v3/emitterc.go
index c29217ef5..0f47c9ca8 100644
--- a/vendor/gopkg.in/yaml.v3/emitterc.go
+++ b/vendor/gopkg.in/yaml.v3/emitterc.go
@@ -814,26 +814,24 @@ func yaml_emitter_emit_block_mapping_value(emitter *yaml_emitter_t, event *yaml_
 		}
 	}
 	if len(emitter.key_line_comment) > 0 {
-		// [Go] A line comment was previously provided for the key. Handle it before
-		//      the value so the inline comments are placed correctly.
-		if yaml_emitter_silent_nil_event(emitter, event) && len(emitter.line_comment) == 0 {
-			// Nothing other than the line comment will be written on the line.
-			emitter.line_comment = emitter.key_line_comment
-			emitter.key_line_comment = nil
-		} else {
-			// An actual value is coming, so emit the comment line.
+		// [Go] Line comments are generally associated with the value, but when there's
+		//      no value on the same line as a mapping key they end up attached to the
+		//      key itself.
+		if event.typ == yaml_SCALAR_EVENT {
+			if len(emitter.line_comment) == 0 {
+				// A scalar is coming and it has no line comments by itself yet,
+				// so just let it handle the line comment as usual. If it has a
+				// line comment, we can't have both so the one from the key is lost.
+				emitter.line_comment = emitter.key_line_comment
+				emitter.key_line_comment = nil
+			}
+		} else if event.sequence_style() != yaml_FLOW_SEQUENCE_STYLE && (event.typ == yaml_MAPPING_START_EVENT || event.typ == yaml_SEQUENCE_START_EVENT) {
+			// An indented block follows, so write the comment right now.
 			emitter.line_comment, emitter.key_line_comment = emitter.key_line_comment, emitter.line_comment
 			if !yaml_emitter_process_line_comment(emitter) {
 				return false
 			}
 			emitter.line_comment, emitter.key_line_comment = emitter.key_line_comment, emitter.line_comment
-			// Indent in unless it's a block that will reindent anyway.
-			if event.sequence_style() == yaml_FLOW_SEQUENCE_STYLE || (event.typ != yaml_MAPPING_START_EVENT && event.typ != yaml_SEQUENCE_START_EVENT) {
-				emitter.indent = emitter.best_indent*((emitter.indent+emitter.best_indent)/emitter.best_indent)
-				if !yaml_emitter_write_indent(emitter) {
-					return false
-				}
-			}
 		}
 	}
 	emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_KEY_STATE)
@@ -1896,7 +1894,7 @@ func yaml_emitter_write_literal_scalar(emitter *yaml_emitter_t, value []byte) bo
 	if !yaml_emitter_write_block_scalar_hints(emitter, value) {
 		return false
 	}
-	if !put_break(emitter) {
+	if !yaml_emitter_process_line_comment(emitter) {
 		return false
 	}
 	//emitter.indention = true
@@ -1933,10 +1931,10 @@ func yaml_emitter_write_folded_scalar(emitter *yaml_emitter_t, value []byte) boo
 	if !yaml_emitter_write_block_scalar_hints(emitter, value) {
 		return false
 	}
-
-	if !put_break(emitter) {
+	if !yaml_emitter_process_line_comment(emitter) {
 		return false
 	}
+
 	//emitter.indention = true
 	emitter.whitespace = true
 
diff --git a/vendor/gopkg.in/yaml.v3/encode.go b/vendor/gopkg.in/yaml.v3/encode.go
index 45e8d1e1b..de9e72a3e 100644
--- a/vendor/gopkg.in/yaml.v3/encode.go
+++ b/vendor/gopkg.in/yaml.v3/encode.go
@@ -120,6 +120,11 @@ func (e *encoder) marshal(tag string, in reflect.Value) {
 		e.nodev(in)
 		return
 	case Node:
+		if !in.CanAddr() {
+			var n = reflect.New(in.Type()).Elem()
+			n.Set(in)
+			in = n
+		}
 		e.nodev(in.Addr())
 		return
 	case time.Time:
diff --git a/vendor/gopkg.in/yaml.v3/scannerc.go b/vendor/gopkg.in/yaml.v3/scannerc.go
index d9a539c39..ca0070108 100644
--- a/vendor/gopkg.in/yaml.v3/scannerc.go
+++ b/vendor/gopkg.in/yaml.v3/scannerc.go
@@ -2260,10 +2260,9 @@ func yaml_parser_scan_block_scalar(parser *yaml_parser_t, token *yaml_token_t, l
 		}
 	}
 	if parser.buffer[parser.buffer_pos] == '#' {
-		// TODO Test this and then re-enable it.
-		//if !yaml_parser_scan_line_comment(parser, start_mark) {
-		//	return false
-		//}
+		if !yaml_parser_scan_line_comment(parser, start_mark) {
+			return false
+		}
 		for !is_breakz(parser.buffer, parser.buffer_pos) {
 			skip(parser)
 			if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
@@ -2892,6 +2891,10 @@ func yaml_parser_scan_comments(parser *yaml_parser_t, scan_mark yaml_mark_t) boo
 
 	var token_mark = token.start_mark
 	var start_mark yaml_mark_t
+	var next_indent = parser.indent
+	if next_indent < 0 {
+		next_indent = 0
+	}
 
 	var recent_empty = false
 	var first_empty = parser.newlines <= 1
@@ -2923,15 +2926,18 @@ func yaml_parser_scan_comments(parser *yaml_parser_t, scan_mark yaml_mark_t) boo
 			continue
 		}
 		c := parser.buffer[parser.buffer_pos+peek]
-		if is_breakz(parser.buffer, parser.buffer_pos+peek) || parser.flow_level > 0 && (c == ']' || c == '}') {
+		var close_flow = parser.flow_level > 0 && (c == ']' || c == '}')
+		if close_flow || is_breakz(parser.buffer, parser.buffer_pos+peek) {
 			// Got line break or terminator.
-			if !recent_empty {
-				if first_empty && (start_mark.line == foot_line || start_mark.column-1 < parser.indent) {
+			if close_flow || !recent_empty {
+				if close_flow || first_empty && (start_mark.line == foot_line && token.typ != yaml_VALUE_TOKEN || start_mark.column-1 < next_indent) {
 					// This is the first empty line and there were no empty lines before,
 					// so this initial part of the comment is a foot of the prior token
 					// instead of being a head for the following one. Split it up.
+					// Alternatively, this might also be the last comment inside a flow
+					// scope, so it must be a footer.
 					if len(text) > 0 {
-						if start_mark.column-1 < parser.indent {
+						if start_mark.column-1 < next_indent {
 							// If dedented it's unrelated to the prior token.
 							token_mark = start_mark
 						}
@@ -2962,7 +2968,7 @@ func yaml_parser_scan_comments(parser *yaml_parser_t, scan_mark yaml_mark_t) boo
 			continue
 		}
 
-		if len(text) > 0 && column < parser.indent+1 && column != start_mark.column {
+		if len(text) > 0 && (close_flow || column-1 < next_indent && column != start_mark.column) {
 			// The comment at the different indentation is a foot of the
 			// preceding data rather than a head of the upcoming one.
 			parser.comments = append(parser.comments, yaml_comment_t{
@@ -3013,6 +3019,10 @@ func yaml_parser_scan_comments(parser *yaml_parser_t, scan_mark yaml_mark_t) boo
 		peek = 0
 		column = 0
 		line = parser.mark.line
+		next_indent = parser.indent
+		if next_indent < 0 {
+			next_indent = 0
+		}
 	}
 
 	if len(text) > 0 {
diff --git a/vendor/gopkg.in/yaml.v3/yaml.go b/vendor/gopkg.in/yaml.v3/yaml.go
index 56e8a8490..8cec6da48 100644
--- a/vendor/gopkg.in/yaml.v3/yaml.go
+++ b/vendor/gopkg.in/yaml.v3/yaml.go
@@ -449,6 +449,11 @@ func (n *Node) ShortTag() string {
 		case ScalarNode:
 			tag, _ := resolve("", n.Value)
 			return tag
+		case 0:
+			// Special case to make the zero value convenient.
+			if n.IsZero() {
+				return nullTag
+			}
 		}
 		return ""
 	}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 741a4c507..f98e05425 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -59,7 +59,7 @@ github.com/NYTimes/gziphandler
 github.com/PuerkitoBio/purell
 # github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578
 github.com/PuerkitoBio/urlesc
-# github.com/alessio/shellescape v1.2.2
+# github.com/alessio/shellescape v1.4.1
 github.com/alessio/shellescape
 # github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
 github.com/asaskevich/govalidator
@@ -186,7 +186,7 @@ github.com/emicklei/go-restful
 github.com/emicklei/go-restful/log
 # github.com/evanphx/json-patch v4.9.0+incompatible
 github.com/evanphx/json-patch
-# github.com/evanphx/json-patch/v5 v5.1.0
+# github.com/evanphx/json-patch/v5 v5.2.0
 github.com/evanphx/json-patch/v5
 # github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d
 github.com/exponent-io/jsonpath
@@ -812,7 +812,7 @@ gopkg.in/tomb.v1
 # gopkg.in/yaml.v2 v2.4.0
 ## explicit
 gopkg.in/yaml.v2
-# gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
+# gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 gopkg.in/yaml.v3
 # helm.sh/helm/v3 v3.1.0-rc.1.0.20201215141456-e71d38b414eb
 ## explicit
@@ -1573,7 +1573,7 @@ sigs.k8s.io/controller-tools/pkg/schemapatcher
 sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml
 sigs.k8s.io/controller-tools/pkg/version
 sigs.k8s.io/controller-tools/pkg/webhook
-# sigs.k8s.io/kind v0.10.0
+# sigs.k8s.io/kind v0.11.1
 ## explicit
 sigs.k8s.io/kind/pkg/apis/config/defaults
 sigs.k8s.io/kind/pkg/apis/config/v1alpha4
diff --git a/vendor/sigs.k8s.io/kind/pkg/apis/config/defaults/image.go b/vendor/sigs.k8s.io/kind/pkg/apis/config/defaults/image.go
index 0d4559d4a..96371c2c9 100644
--- a/vendor/sigs.k8s.io/kind/pkg/apis/config/defaults/image.go
+++ b/vendor/sigs.k8s.io/kind/pkg/apis/config/defaults/image.go
@@ -18,4 +18,4 @@ limitations under the License.
 package defaults
 
 // Image is the default for the Config.Image field, aka the default node image.
-const Image = "kindest/node:v1.20.2@sha256:8f7ea6e7642c0da54f04a7ee10431549c0257315b3a634f6ef2fecaaedb19bab"
+const Image = "kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6"
diff --git a/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/default.go b/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/default.go
index e06f1eeb1..4626fdd41 100644
--- a/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/default.go
+++ b/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/default.go
@@ -37,24 +37,27 @@ func SetDefaultsCluster(obj *Cluster) {
 		SetDefaultsNode(a)
 	}
 	if obj.Networking.IPFamily == "" {
-		obj.Networking.IPFamily = "ipv4"
+		obj.Networking.IPFamily = IPv4Family
 	}
 	// default to listening on 127.0.0.1:randomPort on ipv4
 	// and [::1]:randomPort on ipv6
 	if obj.Networking.APIServerAddress == "" {
 		obj.Networking.APIServerAddress = "127.0.0.1"
-		if obj.Networking.IPFamily == "ipv6" {
+		if obj.Networking.IPFamily == IPv6Family {
 			obj.Networking.APIServerAddress = "::1"
 		}
 	}
 	// default the pod CIDR
 	if obj.Networking.PodSubnet == "" {
 		obj.Networking.PodSubnet = "10.244.0.0/16"
-		if obj.Networking.IPFamily == "ipv6" {
+		if obj.Networking.IPFamily == IPv6Family {
 			// node-mask cidr default is /64 so we need a larger subnet, we use /56 following best practices
 			// xref: https://www.ripe.net/publications/docs/ripe-690#4--size-of-end-user-prefix-assignment---48---56-or-something-else-
 			obj.Networking.PodSubnet = "fd00:10:244::/56"
 		}
+		if obj.Networking.IPFamily == DualStackFamily {
+			obj.Networking.PodSubnet = "10.244.0.0/16,fd00:10:244::/56"
+		}
 	}
 	// default the service CIDR using a different subnet than kubeadm default
 	// https://github.com/kubernetes/kubernetes/blob/746404f82a28e55e0b76ffa7e40306fb88eb3317/cmd/kubeadm/app/apis/kubeadm/v1beta2/defaults.go#L32
@@ -62,13 +65,16 @@ func SetDefaultsCluster(obj *Cluster) {
 	// we allocate a /16 subnet that allows 65535 services (current Kubernetes tested limit is O(10k) services)
 	if obj.Networking.ServiceSubnet == "" {
 		obj.Networking.ServiceSubnet = "10.96.0.0/16"
-		if obj.Networking.IPFamily == "ipv6" {
+		if obj.Networking.IPFamily == IPv6Family {
 			obj.Networking.ServiceSubnet = "fd00:10:96::/112"
 		}
+		if obj.Networking.IPFamily == DualStackFamily {
+			obj.Networking.ServiceSubnet = "10.96.0.0/16,fd00:10:96::/112"
+		}
 	}
 	// default the KubeProxyMode using iptables as it's already the default
 	if obj.Networking.KubeProxyMode == "" {
-		obj.Networking.KubeProxyMode = IPTablesMode
+		obj.Networking.KubeProxyMode = IPTablesProxyMode
 	}
 }
 
diff --git a/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/types.go b/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/types.go
index cd831b3f1..c6cfafbde 100644
--- a/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/types.go
+++ b/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/types.go
@@ -104,6 +104,9 @@ type Node struct {
 	// If unset a default image will be used, see defaults.Image
 	Image string `yaml:"image,omitempty"`
 
+	// Labels are the labels with which the respective node will be labeled
+	Labels map[string]string `yaml:"labels,omitempty"`
+
 	/* Advanced fields */
 
 	// TODO: cri-like types should be inline instead
@@ -196,16 +199,18 @@ const (
 	IPv4Family ClusterIPFamily = "ipv4"
 	// IPv6Family sets ClusterIPFamily to ipv6
 	IPv6Family ClusterIPFamily = "ipv6"
+	// DualStackFamily sets ClusterIPFamily to dual
+	DualStackFamily ClusterIPFamily = "dual"
 )
 
 // ProxyMode defines a proxy mode for kube-proxy
 type ProxyMode string
 
 const (
-	// IPTablesMode sets ProxyMode to iptables
-	IPTablesMode ProxyMode = "iptables"
-	// IPVSMode sets ProxyMode to iptables
-	IPVSMode ProxyMode = "ipvs"
+	// IPTablesProxyMode sets ProxyMode to iptables
+	IPTablesProxyMode ProxyMode = "iptables"
+	// IPVSProxyMode sets ProxyMode to ipvs
+	IPVSProxyMode ProxyMode = "ipvs"
 )
 
 // PatchJSON6902 represents an inline kustomize json 6902 patch
diff --git a/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/zz_generated.deepcopy.go b/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/zz_generated.deepcopy.go
index 6fa61d4e3..8aab0fa12 100644
--- a/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/zz_generated.deepcopy.go
+++ b/vendor/sigs.k8s.io/kind/pkg/apis/config/v1alpha4/zz_generated.deepcopy.go
@@ -114,6 +114,13 @@ func (in *Networking) DeepCopy() *Networking {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Node) DeepCopyInto(out *Node) {
 	*out = *in
+	if in.Labels != nil {
+		in, out := &in.Labels, &out.Labels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 	if in.ExtraMounts != nil {
 		in, out := &in.ExtraMounts, &out.ExtraMounts
 		*out = make([]Mount, len(*in))
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/config/config.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/config/config.go
index cfe970f38..b6f1e4dd1 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/config/config.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/config/config.go
@@ -48,6 +48,11 @@ func (a *Action) Execute(ctx *actions.ActionContext) error {
 	ctx.Status.Start("Writing configuration 📜")
 	defer ctx.Status.End(false)
 
+	providerInfo, err := ctx.Provider.Info()
+	if err != nil {
+		return err
+	}
+
 	allNodes, err := ctx.Nodes()
 	if err != nil {
 		return err
@@ -73,9 +78,10 @@ func (a *Action) Execute(ctx *actions.ActionContext) error {
 		KubeProxyMode:        string(ctx.Config.Networking.KubeProxyMode),
 		ServiceSubnet:        ctx.Config.Networking.ServiceSubnet,
 		ControlPlane:         true,
-		IPv6:                 ctx.Config.Networking.IPFamily == "ipv6",
+		IPFamily:             ctx.Config.Networking.IPFamily,
 		FeatureGates:         ctx.Config.FeatureGates,
 		RuntimeConfig:        ctx.Config.RuntimeConfig,
+		RootlessProvider:     providerInfo.Rootless,
 	}
 
 	kubeadmConfigPlusPatches := func(node nodes.Node, data kubeadm.ConfigData) func() error {
@@ -92,15 +98,42 @@ func (a *Action) Execute(ctx *actions.ActionContext) error {
 		}
 	}
 
+	// Populate the list of control-plane node labels and the list of worker node labels respectively.
+	// controlPlaneLabels is an array of maps (labels, read from config) associated with all the control-plane nodes.
+	// workerLabels is an array of maps (labels, read from config) associated with all the worker nodes.
+	controlPlaneLabels := []map[string]string{}
+	workerLabels := []map[string]string{}
+	for _, node := range ctx.Config.Nodes {
+		if node.Role == config.ControlPlaneRole {
+			controlPlaneLabels = append(controlPlaneLabels, node.Labels)
+		} else if node.Role == config.WorkerRole {
+			workerLabels = append(workerLabels, node.Labels)
+		} else {
+			continue
+		}
+	}
+
+	// hashMapLabelsToCommaSeparatedLabels converts labels in hashmap form to labels in a comma-separated string form like "key1=value1,key2=value2"
+	hashMapLabelsToCommaSeparatedLabels := func(labels map[string]string) string {
+		output := ""
+		for key, value := range labels {
+			output += fmt.Sprintf("%s=%s,", key, value)
+		}
+		return strings.TrimSuffix(output, ",") // remove the last character (comma) in the output string
+	}
+
 	// create the kubeadm join configuration for control plane nodes
 	controlPlanes, err := nodeutils.ControlPlaneNodes(allNodes)
 	if err != nil {
 		return err
 	}
 
-	for _, node := range controlPlanes {
+	for i, node := range controlPlanes {
 		node := node             // capture loop variable
 		configData := configData // copy config data
+		if len(controlPlaneLabels[i]) > 0 {
+			configData.NodeLabels = hashMapLabelsToCommaSeparatedLabels(controlPlaneLabels[i]) // updating the config with the respective labels to be written over the current control-plane node in consideration
+		}
 		fns = append(fns, kubeadmConfigPlusPatches(node, configData))
 	}
 
@@ -111,10 +144,13 @@ func (a *Action) Execute(ctx *actions.ActionContext) error {
 	}
 	if len(workers) > 0 {
 		// create the workers concurrently
-		for _, node := range workers {
+		for i, node := range workers {
 			node := node             // capture loop variable
 			configData := configData // copy config data
 			configData.ControlPlane = false
+			if len(workerLabels[i]) > 0 {
+				configData.NodeLabels = hashMapLabelsToCommaSeparatedLabels(workerLabels[i]) // updating the config with the respective labels to be written over the current worker node in consideration
+			}
 			fns = append(fns, kubeadmConfigPlusPatches(node, configData))
 		}
 	}
@@ -201,11 +237,14 @@ func getKubeadmConfig(cfg *config.Cluster, data kubeadm.ConfigData, node nodes.N
 
 	data.NodeAddress = nodeAddress
 	// configure the right protocol addresses
-	if cfg.Networking.IPFamily == "ipv6" {
+	if cfg.Networking.IPFamily == config.IPv6Family || cfg.Networking.IPFamily == config.DualStackFamily {
 		if ip := net.ParseIP(nodeAddressIPv6); ip.To16() == nil {
 			return "", errors.Errorf("failed to get IPv6 address for node %s; is %s configured to use IPv6 correctly?", node.String(), provider)
 		}
 		data.NodeAddress = nodeAddressIPv6
+		if cfg.Networking.IPFamily == config.DualStackFamily {
+			data.NodeAddress = fmt.Sprintf("%s,%s", nodeAddress, nodeAddressIPv6)
+		}
 	}
 
 	// generate the config contents
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/installcni/cni.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/installcni/cni.go
index 94f9ed41c..f7f6da0fc 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/installcni/cni.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/installcni/cni.go
@@ -23,8 +23,10 @@ import (
 	"text/template"
 
 	"sigs.k8s.io/kind/pkg/errors"
+	"sigs.k8s.io/kind/pkg/internal/apis/config"
 
 	"sigs.k8s.io/kind/pkg/cluster/internal/create/actions"
+	"sigs.k8s.io/kind/pkg/cluster/internal/patch"
 	"sigs.k8s.io/kind/pkg/cluster/nodeutils"
 )
 
@@ -83,6 +85,40 @@ func (a *action) Execute(ctx *actions.ActionContext) error {
 		manifest = out.String()
 	}
 
+	// NOTE: this is intentionally undocumented, as an internal implementation
+	// detail. Going forward users should disable the default CNI and install
+	// their own, or use the default. The internal templating mechanism is
+	// not intended for external usage and is unstable.
+	if strings.Contains(manifest, "would you kindly patch this file") {
+		// Add the controlplane endpoint so kindnet doesn´t have to wait for kube-proxy
+		controlPlaneEndpoint, err := ctx.Provider.GetAPIServerInternalEndpoint(ctx.Config.Name)
+		if err != nil {
+			return err
+		}
+
+		patchValue := `
+- op: add
+  path: /spec/template/spec/containers/0/env/-
+  value:
+    name: CONTROL_PLANE_ENDPOINT
+    value: ` + controlPlaneEndpoint
+
+		controlPlanePatch6902 := config.PatchJSON6902{
+			Group:   "apps",
+			Version: "v1",
+			Kind:    "DaemonSet",
+			Patch:   patchValue,
+		}
+
+		patchedConfig, err := patch.KubeYAML(manifest, nil, []config.PatchJSON6902{controlPlanePatch6902})
+		if err != nil {
+			return err
+		}
+		manifest = patchedConfig
+	}
+
+	ctx.Logger.V(5).Infof("Using the following Kindnetd config:\n%s", manifest)
+
 	// install the manifest
 	if err := node.Command(
 		"kubectl", "create", "--kubeconfig=/etc/kubernetes/admin.conf",
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/kubeadminit/init.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/kubeadminit/init.go
index 5ed3dfa12..9c596cb1f 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/kubeadminit/init.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/actions/kubeadminit/init.go
@@ -26,16 +26,19 @@ import (
 	"sigs.k8s.io/kind/pkg/cluster/nodeutils"
 
 	"sigs.k8s.io/kind/pkg/cluster/internal/create/actions"
+	"sigs.k8s.io/kind/pkg/internal/apis/config"
 )
 
 // kubeadmInitAction implements action for executing the kubeadm init
 // and a set of default post init operations like e.g. install the
 // CNI network plugin.
-type action struct{}
+type action struct {
+	skipKubeProxy bool
+}
 
 // NewAction returns a new action for kubeadm init
-func NewAction() actions.Action {
-	return &action{}
+func NewAction(cfg *config.Cluster) actions.Action {
+	return &action{skipKubeProxy: cfg.Networking.KubeProxyMode == config.NoneProxyMode}
 }
 
 // Execute runs the action
@@ -56,13 +59,18 @@ func (a *action) Execute(ctx *actions.ActionContext) error {
 		return err
 	}
 
+	// skip preflight checks, as these have undesirable side effects
+	// and don't tell us much. requires kubeadm 1.13+
+	skipPhases := "preflight"
+	if a.skipKubeProxy {
+		skipPhases += ",addon/kube-proxy"
+	}
+
 	// run kubeadm
 	cmd := node.Command(
 		// init because this is the control plane node
 		"kubeadm", "init",
-		// skip preflight checks, as these have undesirable side effects
-		// and don't tell us much. requires kubeadm 1.13+
-		"--skip-phases=preflight",
+		"--skip-phases="+skipPhases,
 		// specify our generated config file
 		"--config=/kind/kubeadm.conf",
 		"--skip-token-print",
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/create.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/create.go
index 9d96b7ebe..e89d78960 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/create.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/create/create.go
@@ -19,7 +19,6 @@ package create
 import (
 	"fmt"
 	"math/rand"
-	"regexp"
 	"time"
 
 	"github.com/alessio/shellescape"
@@ -49,12 +48,6 @@ const (
 	clusterNameMax = 50
 )
 
-// similar to valid docker container names, but since we will prefix
-// and suffix this name, we can relax it a little
-// see NewContext() for usage
-// https://godoc.org/github.com/docker/docker/daemon/names#pkg-constants
-var validNameRE = regexp.MustCompile(`^[a-z0-9_.-]+$`)
-
 // ClusterOptions holds cluster creation options
 type ClusterOptions struct {
 	Config       *config.Cluster
@@ -73,6 +66,11 @@ type ClusterOptions struct {
 
 // Cluster creates a cluster
 func Cluster(logger log.Logger, p providers.Provider, opts *ClusterOptions) error {
+	// validate provider first
+	if err := validateProvider(p); err != nil {
+		return err
+	}
+
 	// default / process options (namely config)
 	if err := fixupOptions(opts); err != nil {
 		return err
@@ -83,14 +81,6 @@ func Cluster(logger log.Logger, p providers.Provider, opts *ClusterOptions) erro
 		return err
 	}
 
-	// TODO: move to config validation
-	// validate the name
-	if !validNameRE.MatchString(opts.Config.Name) {
-		return errors.Errorf(
-			"'%s' is not a valid cluster name, cluster names must match `%s`",
-			opts.Config.Name, validNameRE.String(),
-		)
-	}
 	// warn if cluster name might typically be too long
 	if len(opts.Config.Name) > clusterNameMax {
 		logger.Warnf("cluster name %q is probably too long, this might not work properly on some systems", opts.Config.Name)
@@ -123,7 +113,7 @@ func Cluster(logger log.Logger, p providers.Provider, opts *ClusterOptions) erro
 	}
 	if !opts.StopBeforeSettingUpKubernetes {
 		actionsToRun = append(actionsToRun,
-			kubeadminit.NewAction(), // run kubeadm init
+			kubeadminit.NewAction(opts.Config), // run kubeadm init
 		)
 		// this step might be skipped, but is next after init
 		if !opts.Config.Networking.DisableDefaultCNI {
@@ -249,3 +239,19 @@ func fixupOptions(opts *ClusterOptions) error {
 
 	return nil
 }
+
+func validateProvider(p providers.Provider) error {
+	info, err := p.Info()
+	if err != nil {
+		return err
+	}
+	if info.Rootless {
+		if !info.Cgroup2 {
+			return errors.New("running kind with rootless provider requires cgroup v2, see https://kind.sigs.k8s.io/docs/user/rootless/")
+		}
+		if !info.SupportsMemoryLimit || !info.SupportsPidsLimit || !info.SupportsCPUShares {
+			return errors.New("running kind with rootless provider requires setting systemd property \"Delegate=yes\", see https://kind.sigs.k8s.io/docs/user/rootless/")
+		}
+	}
+	return nil
+}
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/kubeadm/config.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/kubeadm/config.go
index 3e311d9f9..c1241b3d9 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/kubeadm/config.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/kubeadm/config.go
@@ -25,6 +25,7 @@ import (
 
 	"k8s.io/apimachinery/pkg/util/version"
 	"sigs.k8s.io/kind/pkg/errors"
+	"sigs.k8s.io/kind/pkg/internal/apis/config"
 )
 
 // ConfigData is supplied to the kubeadm config template, with values populated
@@ -46,7 +47,7 @@ type ConfigData struct {
 
 	// ControlPlane flag specifies the node belongs to the control plane
 	ControlPlane bool
-	// The main IP address of the node
+	// The IP address or comma separated list IP addresses of of the node
 	NodeAddress string
 	// The name for the node (not the address)
 	NodeName string
@@ -67,18 +68,27 @@ type ConfigData struct {
 	// Kubernetes API Server RuntimeConfig
 	RuntimeConfig map[string]string
 
-	// IPv4 values take precedence over IPv6 by default, if true set IPv6 default values
-	IPv6 bool
+	// IPFamily of the cluster, it can be IPv4, IPv6 or DualStack
+	IPFamily config.ClusterIPFamily
+
+	// Labels are the labels, in the format "key1=val1,key2=val2", with which the respective node will be labeled
+	NodeLabels string
 
 	// DerivedConfigData is populated by Derive()
 	// These auto-generated fields are available to Config templates,
 	// but not meant to be set by hand
 	DerivedConfigData
+
+	// Provider is running with rootless mode, so kube-proxy needs to be configured
+	// not to fail on sysctl error.
+	RootlessProvider bool
 }
 
 // DerivedConfigData fields are automatically derived by
 // ConfigData.Derive if they are not specified / zero valued
 type DerivedConfigData struct {
+	// AdvertiseAddress is the first address in NodeAddress
+	AdvertiseAddress string
 	// DockerStableTag is automatically derived from KubernetesVersion
 	DockerStableTag string
 	// SortedFeatureGateKeys allows us to iterate FeatureGates deterministically
@@ -87,14 +97,24 @@ type DerivedConfigData struct {
 	FeatureGatesString string
 	// RuntimeConfigString is of the form `Foo=true,Baz=false`
 	RuntimeConfigString string
+	// KubeadmFeatureGates contains Kubeadm only feature gates
+	KubeadmFeatureGates map[string]bool
+	// IPv4 values take precedence over IPv6 by default, if true set IPv6 default values
+	IPv6 bool
 }
 
 // Derive automatically derives DockerStableTag if not specified
 func (c *ConfigData) Derive() {
+	// get the first address to use it as the API advertised address
+	c.AdvertiseAddress = strings.Split(c.NodeAddress, ",")[0]
+
 	if c.DockerStableTag == "" {
 		c.DockerStableTag = strings.Replace(c.KubernetesVersion, "+", "_", -1)
 	}
 
+	// get the IP addresses family for defaulting components
+	c.IPv6 = c.IPFamily == config.IPv6Family
+
 	// get sorted list of FeatureGate keys
 	featureGateKeys := make([]string, 0, len(c.FeatureGates))
 	for k := range c.FeatureGates {
@@ -187,7 +207,7 @@ bootstrapTokens:
 # we use a well know port for making the API server discoverable inside docker network. 
 # from the host machine such port will be accessible via a random local port instead.
 localAPIEndpoint:
-  advertiseAddress: "{{ .NodeAddress }}"
+  advertiseAddress: "{{ .AdvertiseAddress }}"
   bindPort: {{.APIBindPort}}
 nodeRegistration:
   criSocket: "/run/containerd/containerd.sock"
@@ -204,7 +224,7 @@ metadata:
 {{ if .ControlPlane -}}
 controlPlane:
   localAPIEndpoint:
-    advertiseAddress: "{{ .NodeAddress }}"
+    advertiseAddress: "{{ .AdvertiseAddress }}"
     bindPort: {{.APIBindPort}}
 {{- end }}
 nodeRegistration:
@@ -223,6 +243,11 @@ apiVersion: kubelet.config.k8s.io/v1beta1
 kind: KubeletConfiguration
 metadata:
   name: config
+# explicitly set default cgroup driver
+# unblocks https://github.com/kubernetes/kubernetes/pull/99471
+# TODO: consider switching to systemd instead
+# tracked in: https://github.com/kubernetes-sigs/kind/issues/1726
+cgroupDriver: cgroupfs
 # configure ipv6 addresses in IPv6 mode
 {{ if .IPv6 -}}
 address: "::"
@@ -240,6 +265,7 @@ evictionHard:
 {{ range $key := .SortedFeatureGateKeys }}
   "{{ $key }}": {{$.FeatureGates $key }}
 {{end}}{{end}}
+{{if ne .KubeProxyMode "None"}}
 ---
 apiVersion: kubeproxy.config.k8s.io/v1alpha1
 kind: KubeProxyConfiguration
@@ -252,6 +278,11 @@ mode: "{{ .KubeProxyMode }}"
 {{end}}{{end}}
 iptables:
   minSyncPeriod: 1s
+conntrack:
+# Skip setting sysctl value "net.netfilter.nf_conntrack_max"
+# It is a global variable that affects other namespaces
+  maxPerCore: 0
+{{end}}
 `
 
 // ConfigTemplateBetaV2 is the kubeadm config template for API version v1beta2
@@ -262,6 +293,10 @@ metadata:
   name: config
 kubernetesVersion: {{.KubernetesVersion}}
 clusterName: "{{.ClusterName}}"
+{{ if .KubeadmFeatureGates}}featureGates:
+{{ range $key, $value := .KubeadmFeatureGates }}
+  "{{ $key }}": {{ $value }}
+{{end}}{{end}}
 controlPlaneEndpoint: "{{ .ControlPlaneEndpoint }}"
 # on docker for mac we have to expose the api server via port forward,
 # so we need to ensure the cert is valid for localhost so we can talk
@@ -307,7 +342,7 @@ bootstrapTokens:
 # we use a well know port for making the API server discoverable inside docker network. 
 # from the host machine such port will be accessible via a random local port instead.
 localAPIEndpoint:
-  advertiseAddress: "{{ .NodeAddress }}"
+  advertiseAddress: "{{ .AdvertiseAddress }}"
   bindPort: {{.APIBindPort}}
 nodeRegistration:
   criSocket: "unix:///run/containerd/containerd.sock"
@@ -315,6 +350,7 @@ nodeRegistration:
     fail-swap-on: "false"
     node-ip: "{{ .NodeAddress }}"
     provider-id: "kind://{{.NodeProvider}}/{{.ClusterName}}/{{.NodeName}}"
+    node-labels: "{{ .NodeLabels }}"
 ---
 # no-op entry that exists solely so it can be patched
 apiVersion: kubeadm.k8s.io/v1beta2
@@ -324,7 +360,7 @@ metadata:
 {{ if .ControlPlane -}}
 controlPlane:
   localAPIEndpoint:
-    advertiseAddress: "{{ .NodeAddress }}"
+    advertiseAddress: "{{ .AdvertiseAddress }}"
     bindPort: {{.APIBindPort}}
 {{- end }}
 nodeRegistration:
@@ -333,6 +369,7 @@ nodeRegistration:
     fail-swap-on: "false"
     node-ip: "{{ .NodeAddress }}"
     provider-id: "kind://{{.NodeProvider}}/{{.ClusterName}}/{{.NodeName}}"
+    node-labels: "{{ .NodeLabels }}"
 discovery:
   bootstrapToken:
     apiServerEndpoint: "{{ .ControlPlaneEndpoint }}"
@@ -343,6 +380,11 @@ apiVersion: kubelet.config.k8s.io/v1beta1
 kind: KubeletConfiguration
 metadata:
   name: config
+# explicitly set default cgroup driver
+# unblocks https://github.com/kubernetes/kubernetes/pull/99471
+# TODO: consider switching to systemd instead
+# tracked in: https://github.com/kubernetes-sigs/kind/issues/1726
+cgroupDriver: cgroupfs
 # configure ipv6 addresses in IPv6 mode
 {{ if .IPv6 -}}
 address: "::"
@@ -360,6 +402,7 @@ evictionHard:
 {{ range $key := .SortedFeatureGateKeys }}
   "{{ $key }}": {{ index $.FeatureGates $key }}
 {{end}}{{end}}
+{{if ne .KubeProxyMode "None"}}
 ---
 apiVersion: kubeproxy.config.k8s.io/v1alpha1
 kind: KubeProxyConfiguration
@@ -372,6 +415,16 @@ mode: "{{ .KubeProxyMode }}"
 {{end}}{{end}}
 iptables:
   minSyncPeriod: 1s
+conntrack:
+# Skip setting sysctl value "net.netfilter.nf_conntrack_max"
+# It is a global variable that affects other namespaces
+  maxPerCore: 0
+{{if .RootlessProvider}}
+# Skip setting "net.netfilter.nf_conntrack_tcp_timeout_established"
+  tcpEstablishedTimeout: 0s
+# Skip setting "net.netfilter.nf_conntrack_tcp_timeout_close"
+  tcpCloseWaitTimeout: 0s
+{{end}}{{end}}
 `
 
 // Config returns a kubeadm config generated from config data, in particular
@@ -390,6 +443,9 @@ func Config(data ConfigData) (config string, err error) {
 	// assume the latest API version, then fallback if the k8s version is too low
 	templateSource := ConfigTemplateBetaV2
 	if ver.LessThan(version.MustParseSemantic("v1.15.0")) {
+		if data.RootlessProvider {
+			return "", errors.Errorf("version %q is not compatible with rootless provider", ver)
+		}
 		templateSource = ConfigTemplateBetaV1
 	}
 
@@ -401,6 +457,17 @@ func Config(data ConfigData) (config string, err error) {
 	// derive any automatic fields if not supplied
 	data.Derive()
 
+	// Kubeadm has its own feature-gate for dual stack
+	// we need to enable it for Kubernetes version 1.20 only
+	// dual-stack is only supported in 1.20+
+	// TODO: remove this when 1.20 is EOL or we no longer support
+	// dual-stack for 1.20 in KIND
+	if ver.LessThan(version.MustParseSemantic("v1.21.0")) &&
+		ver.AtLeast(version.MustParseSemantic("v1.20.0")) {
+		data.KubeadmFeatureGates = make(map[string]bool)
+		data.KubeadmFeatureGates["IPv6DualStack"] = true
+	}
+
 	// execute the template
 	var buff bytes.Buffer
 	err = t.Execute(&buff, data)
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/patch/mergepatch.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/patch/mergepatch.go
index 7b8679f73..c52b9f55e 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/patch/mergepatch.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/patch/mergepatch.go
@@ -30,7 +30,16 @@ type mergePatch struct {
 
 func parseMergePatches(rawPatches []string) ([]mergePatch, error) {
 	patches := []mergePatch{}
+	// split document streams before trying to parse them
+	splitRawPatches := make([]string, 0, len(rawPatches))
 	for _, raw := range rawPatches {
+		splitRaw, err := splitYAMLDocuments(raw)
+		if err != nil {
+			return nil, err
+		}
+		splitRawPatches = append(splitRawPatches, splitRaw...)
+	}
+	for _, raw := range splitRawPatches {
 		matchInfo, err := parseYAMLMatchInfo(raw)
 		if err != nil {
 			return nil, errors.WithStack(err)
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/common/logs.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/common/logs.go
index 57fcb8cf1..275de5d88 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/common/logs.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/common/logs.go
@@ -5,7 +5,6 @@ import (
 	"path/filepath"
 
 	"sigs.k8s.io/kind/pkg/cluster/nodes"
-	"sigs.k8s.io/kind/pkg/cmd/kind/version"
 	"sigs.k8s.io/kind/pkg/errors"
 	"sigs.k8s.io/kind/pkg/exec"
 )
@@ -23,24 +22,9 @@ func CollectLogs(n nodes.Node, dir string) error {
 			return cmd.SetStdout(f).SetStderr(f).Run()
 		}
 	}
-	writeToPathFn := func(s string, path string) func() error {
-		return func() error {
-			f, err := FileOnHost(path)
-			if err != nil {
-				return err
-			}
-			defer f.Close()
-			_, err = f.WriteString(s)
-			return err
-		}
-	}
 
 	return errors.AggregateConcurrent([]func() error{
 		// record info about the node container
-		writeToPathFn(
-			version.DisplayVersion(),
-			filepath.Join(dir, "kind-version.txt"),
-		),
 		execToPathFn(
 			n.Command("cat", "/kind/version"),
 			"kubernetes-version.txt",
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/network.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/network.go
index 46636f289..b34041e6b 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/network.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/network.go
@@ -21,10 +21,12 @@ import (
 	"crypto/sha1"
 	"encoding/binary"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net"
 	"regexp"
 	"sort"
+	"strconv"
 	"strings"
 
 	"sigs.k8s.io/kind/pkg/errors"
@@ -61,9 +63,11 @@ func ensureNetwork(name string) error {
 
 	// Generate unique subnet per network based on the name
 	// obtained from the ULA fc00::/8 range
+	// Use the MTU configured for the docker default network
 	// Make N attempts with "probing" in case we happen to collide
 	subnet := generateULASubnetFromName(name, 0)
-	err = createNetworkNoDuplicates(name, subnet)
+	mtu := getDefaultNetworkMTU()
+	err = createNetworkNoDuplicates(name, subnet, mtu)
 	if err == nil {
 		// Success!
 		return nil
@@ -75,7 +79,7 @@ func ensureNetwork(name string) error {
 	// If it is, make more attempts below
 	if isIPv6UnavailableError(err) {
 		// only one attempt, IPAM is automatic in ipv4 only
-		return createNetworkNoDuplicates(name, "")
+		return createNetworkNoDuplicates(name, "", mtu)
 	}
 	if isPoolOverlapError(err) {
 		// pool overlap suggests perhaps another process created the network
@@ -97,7 +101,7 @@ func ensureNetwork(name string) error {
 	const maxAttempts = 5
 	for attempt := int32(1); attempt < maxAttempts; attempt++ {
 		subnet := generateULASubnetFromName(name, attempt)
-		err = createNetworkNoDuplicates(name, subnet)
+		err = createNetworkNoDuplicates(name, subnet, mtu)
 		if err == nil {
 			// success!
 			return nil
@@ -121,8 +125,8 @@ func ensureNetwork(name string) error {
 	return errors.New("exhausted attempts trying to find a non-overlapping subnet")
 }
 
-func createNetworkNoDuplicates(name, ipv6Subnet string) error {
-	if err := createNetwork(name, ipv6Subnet); err != nil && !isNetworkAlreadyExistsError(err) {
+func createNetworkNoDuplicates(name, ipv6Subnet string, mtu int) error {
+	if err := createNetwork(name, ipv6Subnet, mtu); err != nil && !isNetworkAlreadyExistsError(err) {
 		return err
 	}
 	_, err := removeDuplicateNetworks(name)
@@ -142,15 +146,33 @@ func removeDuplicateNetworks(name string) (bool, error) {
 	return len(networks) > 0, nil
 }
 
-func createNetwork(name, ipv6Subnet string) error {
-	if ipv6Subnet == "" {
-		return exec.Command("docker", "network", "create", "-d=bridge",
-			"-o", "com.docker.network.bridge.enable_ip_masquerade=true",
-			name).Run()
-	}
-	return exec.Command("docker", "network", "create", "-d=bridge",
+func createNetwork(name, ipv6Subnet string, mtu int) error {
+	args := []string{"network", "create", "-d=bridge",
 		"-o", "com.docker.network.bridge.enable_ip_masquerade=true",
-		"--ipv6", "--subnet", ipv6Subnet, name).Run()
+	}
+	if mtu > 0 {
+		args = append(args, "-o", fmt.Sprintf("com.docker.network.driver.mtu=%d", mtu))
+	}
+	if ipv6Subnet != "" {
+		args = append(args, "--ipv6", "--subnet", ipv6Subnet)
+	}
+	args = append(args, name)
+	return exec.Command("docker", args...).Run()
+}
+
+// getDefaultNetworkMTU obtains the MTU from the docker default network
+func getDefaultNetworkMTU() int {
+	cmd := exec.Command("docker", "network", "inspect", "bridge",
+		"-f", `{{ index .Options "com.docker.network.driver.mtu" }}`)
+	lines, err := exec.OutputLines(cmd)
+	if err != nil || len(lines) != 1 {
+		return 0
+	}
+	mtu, err := strconv.Atoi(lines[0])
+	if err != nil {
+		return 0
+	}
+	return mtu
 }
 
 func sortedNetworksWithName(name string) ([]string, error) {
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/provider.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/provider.go
index 0a012a265..35d5344da 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/provider.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/provider.go
@@ -17,6 +17,8 @@ limitations under the License.
 package docker
 
 import (
+	"encoding/csv"
+	"encoding/json"
 	"fmt"
 	"net"
 	"os"
@@ -49,6 +51,7 @@ func NewProvider(logger log.Logger) providers.Provider {
 // see NewProvider
 type provider struct {
 	logger log.Logger
+	info   *providers.ProviderInfo
 }
 
 // String implements fmt.Stringer
@@ -281,3 +284,62 @@ func (p *provider) CollectLogs(dir string, nodes []nodes.Node) error {
 	errs = append(errs, errors.AggregateConcurrent(fns))
 	return errors.NewAggregate(errs)
 }
+
+// Info returns the provider info.
+// The info is cached on the first time of the execution.
+func (p *provider) Info() (*providers.ProviderInfo, error) {
+	var err error
+	if p.info == nil {
+		p.info, err = info()
+	}
+	return p.info, err
+}
+
+// dockerInfo corresponds to `docker info --format '{{json .}}'`
+type dockerInfo struct {
+	CgroupDriver    string   `json:"CgroupDriver"`  // "systemd", "cgroupfs", "none"
+	CgroupVersion   string   `json:"CgroupVersion"` // e.g. "2"
+	MemoryLimit     bool     `json:"MemoryLimit"`
+	PidsLimit       bool     `json:"PidsLimit"`
+	CPUShares       bool     `json:"CPUShares"`
+	SecurityOptions []string `json:"SecurityOptions"`
+}
+
+func info() (*providers.ProviderInfo, error) {
+	cmd := exec.Command("docker", "info", "--format", "{{json .}}")
+	out, err := exec.Output(cmd)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get docker info")
+	}
+	var dInfo dockerInfo
+	if err := json.Unmarshal(out, &dInfo); err != nil {
+		return nil, err
+	}
+	info := providers.ProviderInfo{
+		Cgroup2: dInfo.CgroupVersion == "2",
+	}
+	// When CgroupDriver == "none", the MemoryLimit/PidsLimit/CPUShares
+	// values are meaningless and need to be considered false.
+	// https://github.com/moby/moby/issues/42151
+	if dInfo.CgroupDriver != "none" {
+		info.SupportsMemoryLimit = dInfo.MemoryLimit
+		info.SupportsPidsLimit = dInfo.PidsLimit
+		info.SupportsCPUShares = dInfo.CPUShares
+	}
+	for _, o := range dInfo.SecurityOptions {
+		// o is like "name=seccomp,profile=default", or "name=rootless",
+		csvReader := csv.NewReader(strings.NewReader(o))
+		sliceSlice, err := csvReader.ReadAll()
+		if err != nil {
+			return nil, err
+		}
+		for _, f := range sliceSlice {
+			for _, ff := range f {
+				if ff == "name=rootless" {
+					info.Rootless = true
+				}
+			}
+		}
+	}
+	return &info, nil
+}
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/provision.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/provision.go
index 8bd62c048..501618615 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/provision.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/provision.go
@@ -63,7 +63,8 @@ func planCreation(cfg *config.Cluster, networkName string) (createContainerFuncs
 		// For now remote docker + multi control plane is not supported
 		apiServerPort = 0              // replaced with random ports
 		apiServerAddress = "127.0.0.1" // only the LB needs to be non-local
-		if clusterIsIPv6(cfg) {
+		// only for IPv6 only clusters
+		if cfg.Networking.IPFamily == config.IPv6Family {
 			apiServerAddress = "::1" // only the LB needs to be non-local
 		}
 		// plan loadbalancer node
@@ -134,7 +135,7 @@ func createContainer(args []string) error {
 }
 
 func clusterIsIPv6(cfg *config.Cluster) bool {
-	return cfg.Networking.IPFamily == "ipv6"
+	return cfg.Networking.IPFamily == config.IPv6Family || cfg.Networking.IPFamily == config.DualStackFamily
 }
 
 func clusterHasImplicitLoadBalancer(cfg *config.Cluster) bool {
@@ -183,6 +184,9 @@ func commonArgs(cluster string, cfg *config.Cluster, networkName string, nodeNam
 		// however this _actually_ means the same thing as always
 		// so the closest thing is on-failure:1, which will retry *once*
 		"--restart=on-failure:1",
+		// this can be enabled by default in docker daemon.json, so we explicitly
+		// disable it, we want our entrypoint to be PID1, not docker-init / tini
+		"--init=false",
 	}
 
 	// enable IPv6 if necessary
@@ -251,6 +255,11 @@ func runArgsForNode(node *config.Node, clusterIPFamily config.ClusterIPFamily, n
 	}
 	args = append(args, mappingArgs...)
 
+	switch node.Role {
+	case config.ControlPlaneRole:
+		args = append(args, "-e", "KUBECONFIG=/etc/kubernetes/admin.conf")
+	}
+
 	// finally, specify the image to run
 	return append(args, node.Image), nil
 }
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/util.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/util.go
index ac2ceb6f7..82c1d34a9 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/util.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/docker/util.go
@@ -17,6 +17,7 @@ limitations under the License.
 package docker
 
 import (
+	"encoding/json"
 	"strings"
 
 	"sigs.k8s.io/kind/pkg/exec"
@@ -48,15 +49,39 @@ func usernsRemap() bool {
 }
 
 // mountDevMapper checks if the Docker storage driver is Btrfs or ZFS
+// or if the backing filesystem is Btrfs
 func mountDevMapper() bool {
 	storage := ""
+	// check the docker storage driver
 	cmd := exec.Command("docker", "info", "-f", "{{.Driver}}")
 	lines, err := exec.OutputLines(cmd)
-	if err != nil {
+	if err != nil || len(lines) != 1 {
 		return false
 	}
-	if len(lines) > 0 {
-		storage = strings.ToLower(strings.TrimSpace(lines[0]))
+
+	storage = strings.ToLower(strings.TrimSpace(lines[0]))
+	if storage == "btrfs" || storage == "zfs" || storage == "devicemapper" {
+		return true
 	}
-	return storage == "btrfs" || storage == "zfs"
+
+	// check the backing file system
+	// docker info -f '{{json .DriverStatus  }}'
+	// [["Backing Filesystem","extfs"],["Supports d_type","true"],["Native Overlay Diff","true"]]
+	cmd = exec.Command("docker", "info", "-f", "{{json .DriverStatus }}")
+	lines, err = exec.OutputLines(cmd)
+	if err != nil || len(lines) != 1 {
+		return false
+	}
+	var dat [][]string
+	if err := json.Unmarshal([]byte(lines[0]), &dat); err != nil {
+		return false
+	}
+	for _, item := range dat {
+		if item[0] == "Backing Filesystem" {
+			storage = strings.ToLower(item[1])
+			break
+		}
+	}
+
+	return storage == "btrfs" || storage == "zfs" || storage == "xfs"
 }
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/provider.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/provider.go
index f775ab52e..53cc1c5a8 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/provider.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/provider.go
@@ -53,6 +53,7 @@ func NewProvider(logger log.Logger) providers.Provider {
 // see NewProvider
 type provider struct {
 	logger log.Logger
+	info   *providers.ProviderInfo
 }
 
 // String implements fmt.Stringer
@@ -68,12 +69,6 @@ func (p *provider) Provision(status *cli.Status, cfg *config.Cluster) (err error
 		return err
 	}
 
-	// kind doesn't work with podman rootless, surface an error
-	if os.Geteuid() != 0 {
-		p.logger.Errorf("podman provider does not work properly in rootless mode")
-		os.Exit(1)
-	}
-
 	// TODO: validate cfg
 	// ensure node images are pulled before actually provisioning
 	if err := ensureNodeImages(p.logger, status, cfg); err != nil {
@@ -192,13 +187,19 @@ func (p *provider) GetAPIServerEndpoint(cluster string) (string, error) {
 	if err != nil {
 		return "", errors.Wrap(err, "failed to check podman version")
 	}
-	if v.LessThan(version.MustParseSemantic("2.2.0")) {
+	// podman inspect was broken between 2.2.0 and 3.0.0
+	// https://github.com/containers/podman/issues/8444
+	if v.AtLeast(version.MustParseSemantic("2.2.0")) &&
+		v.LessThan(version.MustParseSemantic("3.0.0")) {
+		p.logger.Warnf("WARNING: podman version %s not fully supported, please use versions 3.0.0+")
+
 		cmd := exec.Command(
 			"podman", "inspect",
 			"--format",
-			"{{ json .NetworkSettings.Ports }}",
+			"{{range .NetworkSettings.Ports }}{{range .}}{{.HostIP}}/{{.HostPort}}{{end}}{{end}}",
 			n.String(),
 		)
+
 		lines, err := exec.OutputLines(cmd)
 		if err != nil {
 			return "", errors.Wrap(err, "failed to get api server port")
@@ -206,59 +207,26 @@ func (p *provider) GetAPIServerEndpoint(cluster string) (string, error) {
 		if len(lines) != 1 {
 			return "", errors.Errorf("network details should only be one line, got %d lines", len(lines))
 		}
-
-		// portMapping19 maps to the standard CNI portmapping capability used in podman 1.9
-		// see: https://github.com/containernetworking/cni/blob/spec-v0.4.0/CONVENTIONS.md
-		type portMapping19 struct {
-			HostPort      int32  `json:"hostPort"`
-			ContainerPort int32  `json:"containerPort"`
-			Protocol      string `json:"protocol"`
-			HostIP        string `json:"hostIP"`
+		// output is in the format IP/Port
+		parts := strings.Split(strings.TrimSpace(lines[0]), "/")
+		if len(parts) != 2 {
+			return "", errors.Errorf("network details should be in the format IP/Port, received: %s", parts)
 		}
-		// portMapping20 maps to the podman 2.0 portmap type
-		// see: https://github.com/containers/podman/blob/05988fc74fc25f2ad2256d6e011dfb7ad0b9a4eb/libpod/define/container_inspect.go#L134-L143
-		type portMapping20 struct {
-			HostPort string `json:"HostPort"`
-			HostIP   string `json:"HostIp"`
+		host := parts[0]
+		port, err := strconv.Atoi(parts[1])
+		if err != nil {
+			return "", errors.Errorf("network port not an integer: %v", err)
 		}
 
-		portMappings20 := make(map[string][]portMapping20)
-		if err := json.Unmarshal([]byte(lines[0]), &portMappings20); err == nil {
-			for k, v := range portMappings20 {
-				protocol := "tcp"
-				parts := strings.Split(k, "/")
-				if len(parts) == 2 {
-					protocol = strings.ToLower(parts[1])
-				}
-				containerPort, err := strconv.Atoi(parts[0])
-				if err != nil {
-					return "", err
-				}
-				for _, pm := range v {
-					if containerPort == common.APIServerInternalPort && protocol == "tcp" {
-						return net.JoinHostPort(pm.HostIP, pm.HostPort), nil
-					}
-				}
-			}
-		}
-		var portMappings19 []portMapping19
-		if err := json.Unmarshal([]byte(lines[0]), &portMappings19); err != nil {
-			return "", errors.Errorf("invalid network details: %v", err)
-		}
-		for _, pm := range portMappings19 {
-			if pm.ContainerPort == common.APIServerInternalPort && pm.Protocol == "tcp" {
-				return net.JoinHostPort(pm.HostIP, strconv.Itoa(int(pm.HostPort))), nil
-			}
-		}
+		return net.JoinHostPort(host, strconv.Itoa(port)), nil
 	}
-	// TODO: hack until https://github.com/containers/podman/issues/8444 is resolved
+
 	cmd := exec.Command(
 		"podman", "inspect",
 		"--format",
-		"{{range .NetworkSettings.Ports }}{{range .}}{{.HostIP}}/{{.HostPort}}{{end}}{{end}}",
+		"{{ json .NetworkSettings.Ports }}",
 		n.String(),
 	)
-
 	lines, err := exec.OutputLines(cmd)
 	if err != nil {
 		return "", errors.Wrap(err, "failed to get api server port")
@@ -266,18 +234,53 @@ func (p *provider) GetAPIServerEndpoint(cluster string) (string, error) {
 	if len(lines) != 1 {
 		return "", errors.Errorf("network details should only be one line, got %d lines", len(lines))
 	}
-	// output is in the format IP/Port
-	parts := strings.Split(strings.TrimSpace(lines[0]), "/")
-	if len(parts) != 2 {
-		return "", errors.Errorf("network details should be in the format IP/Port, received: %s", parts)
+
+	// portMapping19 maps to the standard CNI portmapping capability used in podman 1.9
+	// see: https://github.com/containernetworking/cni/blob/spec-v0.4.0/CONVENTIONS.md
+	type portMapping19 struct {
+		HostPort      int32  `json:"hostPort"`
+		ContainerPort int32  `json:"containerPort"`
+		Protocol      string `json:"protocol"`
+		HostIP        string `json:"hostIP"`
 	}
-	host := parts[0]
-	port, err := strconv.Atoi(parts[1])
-	if err != nil {
-		return "", errors.Errorf("network port not an integer: %v", err)
+	// portMapping20 maps to the podman 2.0 portmap type
+	// see: https://github.com/containers/podman/blob/05988fc74fc25f2ad2256d6e011dfb7ad0b9a4eb/libpod/define/container_inspect.go#L134-L143
+	type portMapping20 struct {
+		HostPort string `json:"HostPort"`
+		HostIP   string `json:"HostIp"`
 	}
 
-	return net.JoinHostPort(host, strconv.Itoa(port)), nil
+	portMappings20 := make(map[string][]portMapping20)
+	if err := json.Unmarshal([]byte(lines[0]), &portMappings20); err == nil {
+		for k, v := range portMappings20 {
+			protocol := "tcp"
+			parts := strings.Split(k, "/")
+			if len(parts) == 2 {
+				protocol = strings.ToLower(parts[1])
+			}
+			containerPort, err := strconv.Atoi(parts[0])
+			if err != nil {
+				return "", err
+			}
+			for _, pm := range v {
+				if containerPort == common.APIServerInternalPort && protocol == "tcp" {
+					return net.JoinHostPort(pm.HostIP, pm.HostPort), nil
+				}
+			}
+		}
+	}
+
+	var portMappings19 []portMapping19
+	if err := json.Unmarshal([]byte(lines[0]), &portMappings19); err != nil {
+		return "", errors.Errorf("invalid network details: %v", err)
+	}
+	for _, pm := range portMappings19 {
+		if pm.ContainerPort == common.APIServerInternalPort && pm.Protocol == "tcp" {
+			return net.JoinHostPort(pm.HostIP, strconv.Itoa(int(pm.HostPort))), nil
+		}
+	}
+
+	return "", errors.Errorf("failed to get api server port")
 }
 
 // GetAPIServerInternalEndpoint is part of the providers.Provider interface
@@ -350,3 +353,63 @@ func (p *provider) CollectLogs(dir string, nodes []nodes.Node) error {
 	errs = append(errs, errors.AggregateConcurrent(fns))
 	return errors.NewAggregate(errs)
 }
+
+// Info returns the provider info.
+// The info is cached on the first time of the execution.
+func (p *provider) Info() (*providers.ProviderInfo, error) {
+	if p.info == nil {
+		var err error
+		p.info, err = info(p.logger)
+		if err != nil {
+			return p.info, err
+		}
+	}
+	return p.info, nil
+}
+
+// podmanInfo corresponds to `podman info --format 'json`.
+// The structure is different from `docker info --format '{{json .}}'`,
+// and lacks information about the availability of the cgroup controllers.
+type podmanInfo struct {
+	Host struct {
+		CgroupVersion string `json:"cgroupVersion,omitempty"` // "v2"
+		Security      struct {
+			Rootless bool `json:"rootless,omitempty"`
+		} `json:"security"`
+	} `json:"host"`
+}
+
+// info detects ProviderInfo by executing `podman info --format json`.
+func info(logger log.Logger) (*providers.ProviderInfo, error) {
+	const podman = "podman"
+	args := []string{"info", "--format", "json"}
+	cmd := exec.Command(podman, args...)
+	out, err := exec.Output(cmd)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to get podman info (%s %s): %q",
+			podman, strings.Join(args, " "), string(out))
+	}
+	var pInfo podmanInfo
+	if err := json.Unmarshal(out, &pInfo); err != nil {
+		return nil, err
+	}
+	info := &providers.ProviderInfo{
+		Rootless: pInfo.Host.Security.Rootless,
+		Cgroup2:  pInfo.Host.CgroupVersion == "v2",
+		// We assume all the cgroup controllers to be available.
+		//
+		// For rootless, this assumption is not always correct,
+		// so we print the warning below.
+		//
+		// TODO: We wiil be able to implement proper cgroup controller detection
+		// after the GA of Podman 3.2.x: https://github.com/containers/podman/pull/10387
+		SupportsMemoryLimit: true, // not guaranteed to be correct
+		SupportsPidsLimit:   true, // not guaranteed to be correct
+		SupportsCPUShares:   true, // not guaranteed to be correct
+	}
+	if info.Rootless {
+		logger.Warn("Cgroup controller detection is not implemented for Podman. " +
+			"If you see cgroup-related errors, you might need to set systemd property \"Delegate=yes\", see https://kind.sigs.k8s.io/docs/user/rootless/")
+	}
+	return info, nil
+}
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/provision.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/provision.go
index 47577d3e5..51dce4860 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/provision.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/provision.go
@@ -51,7 +51,8 @@ func planCreation(cfg *config.Cluster, networkName string) (createContainerFuncs
 		// For now remote podman + multi control plane is not supported
 		apiServerPort = 0              // replaced with random ports
 		apiServerAddress = "127.0.0.1" // only the LB needs to be non-local
-		if clusterIsIPv6(cfg) {
+		// only for IPv6 only clusters
+		if cfg.Networking.IPFamily == config.IPv6Family {
 			apiServerAddress = "::1" // only the LB needs to be non-local
 		}
 		// plan loadbalancer node
@@ -120,7 +121,7 @@ func createContainer(args []string) error {
 }
 
 func clusterIsIPv6(cfg *config.Cluster) bool {
-	return cfg.Networking.IPFamily == "ipv6"
+	return cfg.Networking.IPFamily == config.IPv6Family || cfg.Networking.IPFamily == config.DualStackFamily
 }
 
 func clusterHasImplicitLoadBalancer(cfg *config.Cluster) bool {
@@ -143,6 +144,8 @@ func commonArgs(cfg *config.Cluster, networkName string) ([]string, error) {
 		"--net", networkName, // attach to its own network
 		// label the node with the cluster ID
 		"--label", fmt.Sprintf("%s=%s", clusterLabelKey, cfg.Name),
+		// specify container implementation to systemd
+		"-e", "container=podman",
 	}
 
 	// enable IPv6 if necessary
@@ -151,7 +154,7 @@ func commonArgs(cfg *config.Cluster, networkName string) ([]string, error) {
 	}
 
 	// pass proxy environment variables
-	proxyEnv, err := getProxyEnv(cfg)
+	proxyEnv, err := getProxyEnv(cfg, networkName)
 	if err != nil {
 		return nil, errors.Wrap(err, "proxy setup error")
 	}
@@ -159,6 +162,12 @@ func commonArgs(cfg *config.Cluster, networkName string) ([]string, error) {
 		args = append(args, "-e", fmt.Sprintf("%s=%s", key, val))
 	}
 
+	// handle Podman on Btrfs or ZFS same as we do with Docker
+	// https://github.com/kubernetes-sigs/kind/issues/1416#issuecomment-606514724
+	if mountDevMapper() {
+		args = append(args, "--volume", "/dev/mapper:/dev/mapper")
+	}
+
 	return args, nil
 }
 
@@ -209,6 +218,11 @@ func runArgsForNode(node *config.Node, clusterIPFamily config.ClusterIPFamily, n
 	}
 	args = append(args, mappingArgs...)
 
+	switch node.Role {
+	case config.ControlPlaneRole:
+		args = append(args, "-e", "KUBECONFIG=/etc/kubernetes/admin.conf")
+	}
+
 	// finally, specify the image to run
 	_, image := sanitizeImage(node.Image)
 	return append(args, image), nil
@@ -243,12 +257,12 @@ func runArgsForLoadBalancer(cfg *config.Cluster, name string, args []string) ([]
 	return append(args, image), nil
 }
 
-func getProxyEnv(cfg *config.Cluster) (map[string]string, error) {
+func getProxyEnv(cfg *config.Cluster, networkName string) (map[string]string, error) {
 	envs := common.GetProxyEnvs(cfg)
 	// Specifically add the podman network subnets to NO_PROXY if we are using a proxy
 	if len(envs) > 0 {
-		// podman default bridge network is named "bridge" (https://docs.podman.com/network/bridge/#use-the-default-bridge-network)
-		subnets, err := getSubnets("bridge")
+		// kind default bridge is "kind"
+		subnets, err := getSubnets(networkName)
 		if err != nil {
 			return nil, err
 		}
@@ -267,7 +281,8 @@ func getProxyEnv(cfg *config.Cluster) (map[string]string, error) {
 }
 
 func getSubnets(networkName string) ([]string, error) {
-	format := `{{range (index (index . "IPAM") "Config")}}{{index . "Subnet"}} {{end}}`
+	// TODO: unmarshall json and get rid of this complex query
+	format := `{{ range (index (index (index (index . "plugins") 0 ) "ipam" ) "ranges")}}{{ index ( index . 0 ) "subnet" }} {{end}}`
 	cmd := exec.Command("podman", "network", "inspect", "-f", format, networkName)
 	lines, err := exec.OutputLines(cmd)
 	if err != nil {
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/util.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/util.go
index 853f9301a..eb274ab24 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/util.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/podman/util.go
@@ -115,3 +115,19 @@ func deleteVolumes(names []string) error {
 	cmd := exec.Command("podman", args...)
 	return cmd.Run()
 }
+
+// mountDevMapper checks if the podman storage driver is Btrfs or ZFS
+func mountDevMapper() bool {
+	storage := ""
+	cmd := exec.Command("podman", "info", "-f",
+		`{{ index .Store.GraphStatus "Backing Filesystem"}}`)
+	lines, err := exec.OutputLines(cmd)
+	if err != nil {
+		return false
+	}
+
+	if len(lines) > 0 {
+		storage = strings.ToLower(strings.TrimSpace(lines[0]))
+	}
+	return storage == "btrfs" || storage == "zfs"
+}
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/provider.go b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/provider.go
index 6e28c4dc7..d2f8e3628 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/provider.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/internal/providers/provider.go
@@ -45,4 +45,15 @@ type Provider interface {
 	GetAPIServerInternalEndpoint(cluster string) (string, error)
 	// CollectLogs will populate dir with cluster logs and other debug files
 	CollectLogs(dir string, nodes []nodes.Node) error
+	// Info returns the provider info
+	Info() (*ProviderInfo, error)
+}
+
+// ProviderInfo is the info of the provider
+type ProviderInfo struct {
+	Rootless            bool
+	Cgroup2             bool
+	SupportsMemoryLimit bool
+	SupportsPidsLimit   bool
+	SupportsCPUShares   bool
 }
diff --git a/vendor/sigs.k8s.io/kind/pkg/cluster/provider.go b/vendor/sigs.k8s.io/kind/pkg/cluster/provider.go
index 26d308ce4..7a6ae1e6b 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cluster/provider.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cluster/provider.go
@@ -17,11 +17,17 @@ limitations under the License.
 package cluster
 
 import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
 	"sort"
 
+	"sigs.k8s.io/kind/pkg/cmd/kind/version"
+
 	"sigs.k8s.io/kind/pkg/cluster/constants"
 	"sigs.k8s.io/kind/pkg/cluster/nodes"
 	"sigs.k8s.io/kind/pkg/cluster/nodeutils"
+	"sigs.k8s.io/kind/pkg/errors"
 	"sigs.k8s.io/kind/pkg/log"
 
 	internalcreate "sigs.k8s.io/kind/pkg/cluster/internal/create"
@@ -68,20 +74,54 @@ func NewProvider(options ...ProviderOption) *Provider {
 		}
 	}
 
+	// ensure a provider if none was set
+	// NOTE: depends on logger being set (see sorting above)
 	if p.provider == nil {
-		// auto-detect based on each package IsAvailable() function
-		// default to docker for backwards compatibility
-		if docker.IsAvailable() {
-			p.provider = docker.NewProvider(p.logger)
-		} else if podman.IsAvailable() {
-			p.provider = podman.NewProvider(p.logger)
-		} else {
-			p.provider = docker.NewProvider(p.logger)
+		// DetectNodeProvider does not fallback to allow callers to determine
+		// this behavior
+		// However for compatibility if the caller of NewProvider supplied no
+		// option and we autodetect internally, we default to the docker provider
+		// for fallback, to avoid a breaking change for now.
+		// This may change in the future.
+		// TODO: consider breaking this API for earlier errors.
+		providerOpt, _ := DetectNodeProvider()
+		if providerOpt == nil {
+			providerOpt = ProviderWithDocker()
 		}
+		providerOpt.apply(p)
 	}
 	return p
 }
 
+// NoNodeProviderDetectedError indicates that we could not autolocate an available
+// NodeProvider backend on the host
+var NoNodeProviderDetectedError = errors.NewWithoutStack("failed to detect any supported node provider")
+
+// DetectNodeProvider allows callers to autodetect the node provider
+// *without* fallback to the default.
+//
+// Pass the returned ProviderOption to NewProvider to pass the auto-detect Docker
+// or Podman option explicitly (in the future there will be more options)
+//
+// NOTE: The kind *cli* also checks `KIND_EXPERIMENTAL_PROVIDER` for "podman" or
+// "docker" currently and does not auto-detect / respects this if set.
+//
+// This will be replaced with some other mechanism in the future (likely when
+// podman support is GA), in the meantime though your tool may wish to match this.
+//
+// In the future when this is not considered experimental,
+// that logic will be in a public API as well.
+func DetectNodeProvider() (ProviderOption, error) {
+	// auto-detect based on each node provider's IsAvailable() function
+	if docker.IsAvailable() {
+		return ProviderWithDocker(), nil
+	}
+	if podman.IsAvailable() {
+		return ProviderWithPodman(), nil
+	}
+	return nil, errors.WithStack(NoNodeProviderDetectedError)
+}
+
 // ProviderOption is an option for configuring a provider
 type ProviderOption interface {
 	apply(p *Provider)
@@ -191,5 +231,18 @@ func (p *Provider) CollectLogs(name, dir string) error {
 	if err != nil {
 		return err
 	}
+	// ensure directory
+	if err := os.MkdirAll(dir, os.ModePerm); err != nil {
+		return errors.Wrap(err, "failed to create logs directory")
+	}
+	// write kind version
+	if err := ioutil.WriteFile(
+		filepath.Join(dir, "kind-version.txt"),
+		[]byte(version.DisplayVersion()),
+		0666, // match os.Create
+	); err != nil {
+		return errors.Wrap(err, "failed to write kind-version.txt")
+	}
+	// collect and write cluster logs
 	return p.provider.CollectLogs(dir, n)
 }
diff --git a/vendor/sigs.k8s.io/kind/pkg/cmd/kind/version/version.go b/vendor/sigs.k8s.io/kind/pkg/cmd/kind/version/version.go
index 1074c479e..48b783e36 100644
--- a/vendor/sigs.k8s.io/kind/pkg/cmd/kind/version/version.go
+++ b/vendor/sigs.k8s.io/kind/pkg/cmd/kind/version/version.go
@@ -50,7 +50,7 @@ func DisplayVersion() string {
 }
 
 // VersionCore is the core portion of the kind CLI version per Semantic Versioning 2.0.0
-const VersionCore = "0.10.0"
+const VersionCore = "0.11.1"
 
 // VersionPreRelease is the pre-release portion of the kind CLI version per
 // Semantic Versioning 2.0.0
diff --git a/vendor/sigs.k8s.io/kind/pkg/errors/errors.go b/vendor/sigs.k8s.io/kind/pkg/errors/errors.go
index 3e2d1d714..98bc47bf2 100644
--- a/vendor/sigs.k8s.io/kind/pkg/errors/errors.go
+++ b/vendor/sigs.k8s.io/kind/pkg/errors/errors.go
@@ -17,6 +17,8 @@ limitations under the License.
 package errors
 
 import (
+	stderrors "errors"
+
 	pkgerrors "github.com/pkg/errors"
 )
 
@@ -26,6 +28,12 @@ func New(message string) error {
 	return pkgerrors.New(message)
 }
 
+// NewWithoutStack is like new but does NOT wrap with a stack
+// This is useful for exported errors
+func NewWithoutStack(message string) error {
+	return stderrors.New(message)
+}
+
 // Errorf formats according to a format specifier and returns the string as a
 // value that satisfies error. Errorf also records the stack trace at the
 // point it was called.
diff --git a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/convert_v1alpha4.go b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/convert_v1alpha4.go
index 96e7aa8d4..f37fe6c16 100644
--- a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/convert_v1alpha4.go
+++ b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/convert_v1alpha4.go
@@ -51,6 +51,7 @@ func convertv1alpha4Node(in *v1alpha4.Node, out *Node) {
 	out.Role = NodeRole(in.Role)
 	out.Image = in.Image
 
+	out.Labels = in.Labels
 	out.KubeadmConfigPatches = in.KubeadmConfigPatches
 	out.ExtraMounts = make([]Mount, len(in.ExtraMounts))
 	out.ExtraPortMappings = make([]PortMapping, len(in.ExtraPortMappings))
diff --git a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/default.go b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/default.go
index b4486e929..7a93df802 100644
--- a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/default.go
+++ b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/default.go
@@ -48,14 +48,14 @@ func SetDefaultsCluster(obj *Cluster) {
 		SetDefaultsNode(a)
 	}
 	if obj.Networking.IPFamily == "" {
-		obj.Networking.IPFamily = "ipv4"
+		obj.Networking.IPFamily = IPv4Family
 	}
 
 	// default to listening on 127.0.0.1:randomPort on ipv4
 	// and [::1]:randomPort on ipv6
 	if obj.Networking.APIServerAddress == "" {
 		obj.Networking.APIServerAddress = "127.0.0.1"
-		if obj.Networking.IPFamily == "ipv6" {
+		if obj.Networking.IPFamily == IPv6Family {
 			obj.Networking.APIServerAddress = "::1"
 		}
 	}
@@ -63,11 +63,14 @@ func SetDefaultsCluster(obj *Cluster) {
 	// default the pod CIDR
 	if obj.Networking.PodSubnet == "" {
 		obj.Networking.PodSubnet = "10.244.0.0/16"
-		if obj.Networking.IPFamily == "ipv6" {
+		if obj.Networking.IPFamily == IPv6Family {
 			// node-mask cidr default is /64 so we need a larger subnet, we use /56 following best practices
 			// xref: https://www.ripe.net/publications/docs/ripe-690#4--size-of-end-user-prefix-assignment---48---56-or-something-else-
 			obj.Networking.PodSubnet = "fd00:10:244::/56"
 		}
+		if obj.Networking.IPFamily == DualStackFamily {
+			obj.Networking.PodSubnet = "10.244.0.0/16,fd00:10:244::/56"
+		}
 	}
 
 	// default the service CIDR using the kubeadm default
@@ -76,13 +79,16 @@ func SetDefaultsCluster(obj *Cluster) {
 	// we allocate a /16 subnet that allows 65535 services (current Kubernetes tested limit is O(10k) services)
 	if obj.Networking.ServiceSubnet == "" {
 		obj.Networking.ServiceSubnet = "10.96.0.0/16"
-		if obj.Networking.IPFamily == "ipv6" {
+		if obj.Networking.IPFamily == IPv6Family {
 			obj.Networking.ServiceSubnet = "fd00:10:96::/112"
 		}
+		if obj.Networking.IPFamily == DualStackFamily {
+			obj.Networking.ServiceSubnet = "10.96.0.0/16,fd00:10:96::/112"
+		}
 	}
 	// default the KubeProxyMode using iptables as it's already the default
 	if obj.Networking.KubeProxyMode == "" {
-		obj.Networking.KubeProxyMode = IPTablesMode
+		obj.Networking.KubeProxyMode = IPTablesProxyMode
 	}
 }
 
diff --git a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/types.go b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/types.go
index 49b3625cc..33bbd721d 100644
--- a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/types.go
+++ b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/types.go
@@ -85,6 +85,9 @@ type Node struct {
 	// If unset a default image will be used, see defaults.Image
 	Image string
 
+	// Labels are the labels with which the respective node will be labeled
+	Labels map[string]string
+
 	/* Advanced fields */
 
 	// ExtraMounts describes additional mount points for the node container
@@ -157,16 +160,20 @@ const (
 	IPv4Family ClusterIPFamily = "ipv4"
 	// IPv6Family sets ClusterIPFamily to ipv6
 	IPv6Family ClusterIPFamily = "ipv6"
+	// DualStackFamily sets ClusterIPFamily to dual
+	DualStackFamily ClusterIPFamily = "dual"
 )
 
 // ProxyMode defines a proxy mode for kube-proxy
 type ProxyMode string
 
 const (
-	// IPTablesMode sets ProxyMode to iptables
-	IPTablesMode ProxyMode = "iptables"
-	// IPVSMode sets ProxyMode to iptables
-	IPVSMode ProxyMode = "ipvs"
+	// IPTablesProxyMode sets ProxyMode to iptables
+	IPTablesProxyMode ProxyMode = "iptables"
+	// IPVSProxyMode sets ProxyMode to ipvs
+	IPVSProxyMode ProxyMode = "ipvs"
+	// NoneProxyMode disables kube-proxy
+	NoneProxyMode ProxyMode = "none"
 )
 
 // PatchJSON6902 represents an inline kustomize json 6902 patch
diff --git a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/validate.go b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/validate.go
index 62671621b..5152322a9 100644
--- a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/validate.go
+++ b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/validate.go
@@ -17,16 +17,31 @@ limitations under the License.
 package config
 
 import (
+	"fmt"
 	"net"
+	"regexp"
+	"strings"
 
 	"sigs.k8s.io/kind/pkg/errors"
 )
 
+// similar to valid docker container names, but since we will prefix
+// and suffix this name, we can relax it a little
+// see NewContext() for usage
+// https://godoc.org/github.com/docker/docker/daemon/names#pkg-constants
+var validNameRE = regexp.MustCompile(`^[a-z0-9.-]+$`)
+
 // Validate returns a ConfigErrors with an entry for each problem
 // with the config, or nil if there are none
 func (c *Cluster) Validate() error {
 	errs := []error{}
 
+	// validate the name
+	if !validNameRE.MatchString(c.Name) {
+		errs = append(errs, errors.Errorf("'%s' is not a valid cluster name, cluster names must match `%s`",
+			c.Name, validNameRE.String()))
+	}
+
 	// the api server port only needs checking if we aren't picking a random one
 	// at runtime
 	if c.Networking.APIServerPort != 0 {
@@ -36,17 +51,20 @@ func (c *Cluster) Validate() error {
 		}
 	}
 
+	isDualStack := c.Networking.IPFamily == DualStackFamily
 	// podSubnet should be a valid CIDR
-	if _, _, err := net.ParseCIDR(c.Networking.PodSubnet); err != nil {
-		errs = append(errs, errors.Wrapf(err, "invalid podSubnet"))
+	if err := validateSubnets(c.Networking.PodSubnet, isDualStack); err != nil {
+		errs = append(errs, errors.Errorf("invalid pod subnet %v", err))
 	}
+
 	// serviceSubnet should be a valid CIDR
-	if _, _, err := net.ParseCIDR(c.Networking.ServiceSubnet); err != nil {
-		errs = append(errs, errors.Wrapf(err, "invalid serviceSubnet"))
+	if err := validateSubnets(c.Networking.ServiceSubnet, isDualStack); err != nil {
+		errs = append(errs, errors.Errorf("invalid service subnet %v", err))
 	}
 
 	// KubeProxyMode should be iptables or ipvs
-	if c.Networking.KubeProxyMode != IPTablesMode && c.Networking.KubeProxyMode != IPVSMode {
+	if c.Networking.KubeProxyMode != IPTablesProxyMode && c.Networking.KubeProxyMode != IPVSProxyMode &&
+		c.Networking.KubeProxyMode != NoneProxyMode {
 		errs = append(errs, errors.Errorf("invalid kubeProxyMode: %s", c.Networking.KubeProxyMode))
 	}
 
@@ -121,3 +139,64 @@ func validatePort(port int32) error {
 	}
 	return nil
 }
+
+func validateSubnets(subnetStr string, dualstack bool) error {
+	allErrs := []error{}
+
+	cidrsString := strings.Split(subnetStr, ",")
+	subnets := make([]*net.IPNet, 0, len(cidrsString))
+	for _, cidrString := range cidrsString {
+		_, cidr, err := net.ParseCIDR(cidrString)
+		if err != nil {
+			return fmt.Errorf("failed to parse cidr value:%q with error: %v", cidrString, err)
+		}
+		subnets = append(subnets, cidr)
+	}
+
+	switch {
+	// if DualStack only 2 CIDRs allowed
+	case dualstack && len(subnets) > 2:
+		allErrs = append(allErrs, errors.New("expected one (IPv4 or IPv6) CIDR or two CIDRs from each family for dual-stack networking"))
+	// if DualStack and there are 2 CIDRs validate if there is at least one of each IP family
+	case dualstack && len(subnets) == 2:
+		areDualStackCIDRs, err := isDualStackCIDRs(subnets)
+		if err != nil {
+			allErrs = append(allErrs, err)
+		} else if !areDualStackCIDRs {
+			allErrs = append(allErrs, errors.New("expected one (IPv4 or IPv6) CIDR or two CIDRs from each family for dual-stack networking"))
+		}
+	// if not DualStack only one CIDR allowed
+	case !dualstack && len(subnets) > 1:
+		allErrs = append(allErrs, errors.New("only one CIDR allowed for single-stack networking"))
+	}
+
+	if len(allErrs) > 0 {
+		return errors.NewAggregate(allErrs)
+	}
+	return nil
+}
+
+// isDualStackCIDRs returns if
+// - all are valid cidrs
+// - at least one cidr from each family (v4 or v6)
+func isDualStackCIDRs(cidrs []*net.IPNet) (bool, error) {
+	v4Found := false
+	v6Found := false
+	for _, cidr := range cidrs {
+		if cidr == nil {
+			return false, fmt.Errorf("cidr %v is invalid", cidr)
+		}
+
+		if v4Found && v6Found {
+			continue
+		}
+
+		if cidr.IP != nil && cidr.IP.To4() == nil {
+			v6Found = true
+			continue
+		}
+		v4Found = true
+	}
+
+	return v4Found && v6Found, nil
+}
diff --git a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/zz_generated.deepcopy.go b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/zz_generated.deepcopy.go
index 74e0c9d5f..3595f0ae3 100644
--- a/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/zz_generated.deepcopy.go
+++ b/vendor/sigs.k8s.io/kind/pkg/internal/apis/config/zz_generated.deepcopy.go
@@ -113,6 +113,13 @@ func (in *Networking) DeepCopy() *Networking {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Node) DeepCopyInto(out *Node) {
 	*out = *in
+	if in.Labels != nil {
+		in, out := &in.Labels, &out.Labels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 	if in.ExtraMounts != nil {
 		in, out := &in.ExtraMounts, &out.ExtraMounts
 		*out = make([]Mount, len(*in))


Test patch:
diff --git a/pkg/controller/operators/catalog/client_factory_stub_test.go b/pkg/controller/operators/catalog/client_factory_stub_test.go
new file mode 100644
index 000000000..10855666b
--- /dev/null
+++ b/pkg/controller/operators/catalog/client_factory_stub_test.go
@@ -0,0 +1,33 @@
+package catalog
+
+import (
+	"k8s.io/client-go/dynamic"
+
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/clients"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
+)
+
+type stubClientFactory struct {
+	operatorClient   operatorclient.ClientInterface
+	kubernetesClient versioned.Interface
+	dynamicClient    dynamic.Interface
+}
+
+var _ clients.Factory = &stubClientFactory{}
+
+func (f *stubClientFactory) WithConfigTransformer(clients.ConfigTransformer) clients.Factory {
+	return f
+}
+
+func (f *stubClientFactory) NewOperatorClient() (operatorclient.ClientInterface, error) {
+	return f.operatorClient, nil
+}
+
+func (f *stubClientFactory) NewKubernetesClient() (versioned.Interface, error) {
+	return f.kubernetesClient, nil
+}
+
+func (f *stubClientFactory) NewDynamicClient() (dynamic.Interface, error) {
+	return f.dynamicClient, nil
+}
diff --git a/pkg/controller/operators/catalog/operator_test.go b/pkg/controller/operators/catalog/operator_test.go
index 4c5693dd9..c5df7e35a 100644
--- a/pkg/controller/operators/catalog/operator_test.go
+++ b/pkg/controller/operators/catalog/operator_test.go
@@ -1515,9 +1515,14 @@ func NewFakeOperator(ctx context.Context, namespace string, namespaces []string,
 		resolver:              config.resolver,
 		reconciler:            config.reconciler,
 		recorder:              config.recorder,
-		clientAttenuator:      scoped.NewClientAttenuator(logger, &rest.Config{}, opClientFake, clientFake, dynamicClientFake),
+		clientAttenuator:      scoped.NewClientAttenuator(logger, &rest.Config{}, opClientFake),
 		serviceAccountQuerier: scoped.NewUserDefinedServiceAccountQuerier(logger, clientFake),
 		catsrcQueueSet:        queueinformer.NewEmptyResourceQueueSet(),
+		clientFactory: &stubClientFactory{
+			operatorClient:   opClientFake,
+			kubernetesClient: clientFake,
+			dynamicClient:    dynamicClientFake,
+		},
 	}
 	op.sources = grpc.NewSourceStore(config.logger, 1*time.Second, 5*time.Second, op.syncSourceState)
 	if op.reconciler == nil {
diff --git a/pkg/controller/operators/olm/client_factory_stub_test.go b/pkg/controller/operators/olm/client_factory_stub_test.go
new file mode 100644
index 000000000..20ce510c0
--- /dev/null
+++ b/pkg/controller/operators/olm/client_factory_stub_test.go
@@ -0,0 +1,33 @@
+package olm
+
+import (
+	"k8s.io/client-go/dynamic"
+
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/clients"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
+)
+
+type stubClientFactory struct {
+	operatorClient   operatorclient.ClientInterface
+	kubernetesClient versioned.Interface
+	dynamicClient    dynamic.Interface
+}
+
+var _ clients.Factory = &stubClientFactory{}
+
+func (f *stubClientFactory) WithConfigTransformer(clients.ConfigTransformer) clients.Factory {
+	return f
+}
+
+func (f *stubClientFactory) NewOperatorClient() (operatorclient.ClientInterface, error) {
+	return f.operatorClient, nil
+}
+
+func (f *stubClientFactory) NewKubernetesClient() (versioned.Interface, error) {
+	return f.kubernetesClient, nil
+}
+
+func (f *stubClientFactory) NewDynamicClient() (dynamic.Interface, error) {
+	return f.dynamicClient, nil
+}
diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go
index 00f2a5e93..effebcfb9 100644
--- a/pkg/controller/operators/olm/operator_test.go
+++ b/pkg/controller/operators/olm/operator_test.go
@@ -318,6 +318,11 @@ func NewFakeOperator(ctx context.Context, options ...fakeOperatorOption) (*Opera
 		return nil, fmt.Errorf("failed to wait for caches to sync")
 	}
 
+	op.clientFactory = &stubClientFactory{
+		operatorClient:   config.operatorClient,
+		kubernetesClient: config.externalClient,
+	}
+
 	return op, nil
 }
 
diff --git a/test/e2e/installplan_e2e_test.go b/test/e2e/installplan_e2e_test.go
index ef2258924..ad022026b 100644
--- a/test/e2e/installplan_e2e_test.go
+++ b/test/e2e/installplan_e2e_test.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -21,6 +22,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 	authorizationv1 "k8s.io/api/authorization/v1"
 	corev1 "k8s.io/api/core/v1"
+	policyv1beta1 "k8s.io/api/policy/v1beta1"
 	rbacv1 "k8s.io/api/rbac/v1"
 	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -31,6 +33,7 @@ import (
 	k8sjson "k8s.io/apimachinery/pkg/runtime/serializer/json"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/util/retry"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -60,6 +63,140 @@ var _ = Describe("Install Plan", func() {
 		TearDown(testNamespace)
 	})
 
+	When("an InstallPlan step contains a deprecated resource version", func() {
+		var (
+			pdb      policyv1beta1.PodDisruptionBudget
+			manifest string
+			counter  float64
+		)
+
+		BeforeEach(func() {
+			dc, err := discovery.NewDiscoveryClientForConfig(ctx.Ctx().RESTConfig())
+			Expect(err).ToNot(HaveOccurred())
+
+			v, err := dc.ServerVersion()
+			Expect(err).ToNot(HaveOccurred())
+
+			if minor, err := strconv.Atoi(v.Minor); err == nil && minor < 21 {
+				// This is a tactical can-kick with
+				// the expectation that the
+				// event-emitting behavior being
+				// tested in this context will have
+				// moved by the time 1.25 arrives.
+				Skip("hack: test is dependent on 1.21+ behavior")
+			}
+		})
+
+		BeforeEach(func() {
+			counter = 0
+			for _, metric := range getMetricsFromPod(ctx.Ctx().KubeClient(), getPodWithLabel(ctx.Ctx().KubeClient(), "app=catalog-operator"), "8081") {
+				if metric.Family == "installplan_warnings_total" {
+					counter = metric.Value
+				}
+			}
+
+			csv := newCSV("test-csv", testNamespace, "", semver.Version{}, nil, nil, nil)
+			Expect(ctx.Ctx().Client().Create(context.TODO(), &csv)).To(Succeed())
+
+			pdb = policyv1beta1.PodDisruptionBudget{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "test-pdb",
+				},
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "PodDisruptionBudget",
+					APIVersion: policyv1beta1.SchemeGroupVersion.String(),
+				},
+				Spec: policyv1beta1.PodDisruptionBudgetSpec{},
+			}
+
+			scheme := runtime.NewScheme()
+			Expect(policyv1beta1.AddToScheme(scheme)).To(Succeed())
+			{
+				var b bytes.Buffer
+				Expect(k8sjson.NewSerializer(k8sjson.DefaultMetaFactory, scheme, scheme, false).Encode(&pdb, &b)).To(Succeed())
+				manifest = b.String()
+			}
+
+			plan := operatorsv1alpha1.InstallPlan{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: testNamespace,
+					Name:      "test-plan",
+				},
+				Spec: operatorsv1alpha1.InstallPlanSpec{
+					Approval:                   operatorsv1alpha1.ApprovalAutomatic,
+					Approved:                   true,
+					ClusterServiceVersionNames: []string{},
+				},
+				Status: operatorsv1alpha1.InstallPlanStatus{
+					Phase:          operatorsv1alpha1.InstallPlanPhaseInstalling,
+					CatalogSources: []string{},
+					Plan: []*operatorsv1alpha1.Step{
+						{
+							Resolving: "test-csv",
+							Status:    operatorsv1alpha1.StepStatusUnknown,
+							Resource: operatorsv1alpha1.StepResource{
+								Name:     pdb.GetName(),
+								Version:  pdb.APIVersion,
+								Kind:     pdb.Kind,
+								Manifest: manifest,
+							},
+						},
+					},
+				},
+			}
+			Expect(ctx.Ctx().Client().Create(context.Background(), &plan)).To(Succeed())
+			Expect(ctx.Ctx().Client().Status().Update(context.Background(), &plan)).To(Succeed())
+			Eventually(func() (*operatorsv1alpha1.InstallPlan, error) {
+				return &plan, ctx.Ctx().Client().Get(context.Background(), client.ObjectKeyFromObject(&plan), &plan)
+			}).Should(HavePhase(operatorsv1alpha1.InstallPlanPhaseComplete))
+
+		})
+
+		It("creates an Event surfacing the deprecation warning", func() {
+			Eventually(func() ([]corev1.Event, error) {
+				var events corev1.EventList
+				if err := ctx.Ctx().Client().List(context.Background(), &events, client.InNamespace(testNamespace)); err != nil {
+					return nil, err
+				}
+				var result []corev1.Event
+				for _, item := range events.Items {
+					result = append(result, corev1.Event{
+						InvolvedObject: corev1.ObjectReference{
+							APIVersion: item.InvolvedObject.APIVersion,
+							Kind:       item.InvolvedObject.Kind,
+							Namespace:  item.InvolvedObject.Namespace,
+							Name:       item.InvolvedObject.Name,
+							FieldPath:  item.InvolvedObject.FieldPath,
+						},
+						Reason:  item.Reason,
+						Message: item.Message,
+					})
+				}
+				return result, nil
+			}).Should(ContainElement(corev1.Event{
+				InvolvedObject: corev1.ObjectReference{
+					APIVersion: operatorsv1alpha1.InstallPlanAPIVersion,
+					Kind:       operatorsv1alpha1.InstallPlanKind,
+					Namespace:  testNamespace,
+					Name:       "test-plan",
+					FieldPath:  "status.plan[0]",
+				},
+				Reason:  "AppliedWithWarnings",
+				Message: "1 warning(s) generated during installation of operator \"test-csv\" (PodDisruptionBudget \"test-pdb\"): policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget",
+			}))
+
+		})
+
+		It("increments a metric counting the warning", func() {
+			Eventually(func() []Metric {
+				return getMetricsFromPod(ctx.Ctx().KubeClient(), getPodWithLabel(ctx.Ctx().KubeClient(), "app=catalog-operator"), "8081")
+			}).Should(ContainElement(LikeMetric(
+				WithFamily("installplan_warnings_total"),
+				WithValueGreaterThan(counter),
+			)))
+		})
+	})
+
 	When("a CustomResourceDefinition step resolved from a bundle is applied", func() {
 		var (
 			crd      apiextensionsv1.CustomResourceDefinition
diff --git a/test/e2e/scoped_client_test.go b/test/e2e/scoped_client_test.go
index a3f490a19..028b94103 100644
--- a/test/e2e/scoped_client_test.go
+++ b/test/e2e/scoped_client_test.go
@@ -2,6 +2,7 @@ package e2e
 
 import (
 	"context"
+
 	. "github.com/onsi/ginkgo"
 	"github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
@@ -10,10 +11,9 @@ import (
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/rest"
 
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/clients"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
 	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/scoped"
 	"github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx"
@@ -25,9 +25,7 @@ var _ = Describe("Scoped Client bound to a service account can be used to make A
 	var (
 		config *rest.Config
 
-		kubeclient    operatorclient.ClientInterface
-		crclient      versioned.Interface
-		dynamicclient dynamic.Interface
+		kubeclient operatorclient.ClientInterface
 
 		logger *logrus.Logger
 	)
@@ -36,8 +34,6 @@ var _ = Describe("Scoped Client bound to a service account can be used to make A
 		config = ctx.Ctx().RESTConfig()
 
 		kubeclient = newKubeClient()
-		crclient = newCRClient()
-		dynamicclient = ctx.Ctx().DynamicClient()
 
 		logger = logrus.New()
 		logger.SetOutput(GinkgoWriter)
@@ -99,7 +95,7 @@ var _ = Describe("Scoped Client bound to a service account can be used to make A
 			return v.Secrets
 		}, BeEmpty()))
 
-		strategy := scoped.NewClientAttenuator(logger, config, kubeclient, crclient, dynamicclient)
+		strategy := scoped.NewClientAttenuator(logger, config, kubeclient)
 		getter := func() (reference *corev1.ObjectReference, err error) {
 			reference = &corev1.ObjectReference{
 				Namespace: namespace,
@@ -114,23 +110,31 @@ var _ = Describe("Scoped Client bound to a service account can be used to make A
 		}
 
 		By("Get scoped client instance(s)")
-		kubeclientGot, crclientGot, dynamicClientGot, errGot := strategy.AttenuateClient(getter)
-		Expect(errGot).ToNot(HaveOccurred())
+		attenuate, err := strategy.AttenuateToServiceAccount(getter)
+		Expect(err).ToNot(HaveOccurred())
+
+		factory := clients.NewFactory(config).WithConfigTransformer(attenuate)
+		kubeclientGot, err := factory.NewOperatorClient()
+		Expect(err).ToNot(HaveOccurred())
 		Expect(kubeclientGot).ToNot(BeNil())
+		crclientGot, err := factory.NewKubernetesClient()
+		Expect(err).ToNot(HaveOccurred())
 		Expect(crclientGot).ToNot(BeNil())
+		dynamicClientGot, err := factory.NewDynamicClient()
+		Expect(err).ToNot(HaveOccurred())
 		Expect(dynamicClientGot).ToNot(BeNil())
 
-		_, errGot = kubeclientGot.KubernetesInterface().CoreV1().ConfigMaps(namespace).Get(context.TODO(), genName("does-not-exist-"), metav1.GetOptions{})
-		Expect(errGot).To(HaveOccurred())
-		tc.assertFunc(errGot)
+		_, err = kubeclientGot.KubernetesInterface().CoreV1().ConfigMaps(namespace).Get(context.TODO(), genName("does-not-exist-"), metav1.GetOptions{})
+		Expect(err).To(HaveOccurred())
+		tc.assertFunc(err)
 
-		_, errGot = crclientGot.OperatorsV1alpha1().CatalogSources(namespace).Get(context.TODO(), genName("does-not-exist-"), metav1.GetOptions{})
-		Expect(errGot).To(HaveOccurred())
-		tc.assertFunc(errGot)
+		_, err = crclientGot.OperatorsV1alpha1().CatalogSources(namespace).Get(context.TODO(), genName("does-not-exist-"), metav1.GetOptions{})
+		Expect(err).To(HaveOccurred())
+		tc.assertFunc(err)
 
 		gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "ConfigMap"}
-		_, errGot = dynamicClientGot.Resource(gvr).Namespace(namespace).Get(context.TODO(), genName("does-not-exist-"), metav1.GetOptions{})
-		Expect(errGot).To(HaveOccurred())
-		tc.assertFunc(errGot)
+		_, err = dynamicClientGot.Resource(gvr).Namespace(namespace).Get(context.TODO(), genName("does-not-exist-"), metav1.GetOptions{})
+		Expect(err).To(HaveOccurred())
+		tc.assertFunc(err)
 	}, tableEntries...)
 })