Repository: metallb/metallb
Pull request #1045 (2021-11-14 09:14:48)

Add Operator mode for e2e tests
Currently how the e2e tests work is by changing MetalLB's ConfigMap directly.
Here we add a way for the tests to use the Operator's CRDs instead of
manipulating the ConfigMap. That'll enable us to have a true e2e scenario
for the Operator, meaning that from one side (the Operator's e2e) it
validates that it configures MetalLB's ConfigMap the way it thinks it should,
and with the new tests mode we could also know that that the operator is
configuring MetalLB properly from the other side (MetalLBs e2e) = the way
MetalLB expects it to be configured.

This may also serve as a base for when(if) the Operator is integrated with
MetalLB and not as a standalone.

Signed-off-by: Ori Braunshtein <obraunsh@redhat.com>

oribon: Marked as draft, waiting for BFDProfile CRD in Operator.
fedepaol: LGTM

Patch:
diff --git a/go.mod b/go.mod
index 4eec3c4b..4071e0f0 100644
--- a/go.mod
+++ b/go.mod
@@ -16,9 +16,10 @@ require (
 	github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7
 	github.com/mdlayher/ndp v0.0.0-20200602162440-17ab9e3e5567
 	github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
+	github.com/metallb/metallb-operator v0.0.0-20211130125940-1fa9bd1b26e6
 	github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721
-	github.com/onsi/ginkgo v1.11.0
-	github.com/onsi/gomega v1.7.0
+	github.com/onsi/ginkgo v1.14.1
+	github.com/onsi/gomega v1.10.2
 	github.com/ory/dockertest/v3 v3.8.0
 	github.com/osrg/gobgp v2.0.0+incompatible
 	github.com/pkg/errors v0.9.1
@@ -30,11 +31,12 @@ require (
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
 	gopkg.in/yaml.v2 v2.4.0
-	k8s.io/api v0.20.2
-	k8s.io/apimachinery v0.20.2
-	k8s.io/client-go v0.20.2
+	k8s.io/api v0.20.4
+	k8s.io/apimachinery v0.20.4
+	k8s.io/client-go v0.20.4
 	k8s.io/klog v0.3.1
-	k8s.io/kubernetes v0.0.0-00010101000000-000000000000
+	k8s.io/kubernetes v1.21.1
+	sigs.k8s.io/controller-runtime v0.7.0
 )
 
 // pinning to specific versions is required because we are consuming
diff --git a/go.sum b/go.sum
index 9e58f575..72905b9f 100644
--- a/go.sum
+++ b/go.sum
@@ -58,6 +58,9 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
 github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20200415212048-7901bc822317/go.mod h1:DF8FZRxMHMGv/vP2lQP6h+dYzzjpuRn24VeRiYn3qjQ=
 github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
 github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
 github.com/Microsoft/go-winio v0.4.15/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
 github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU=
@@ -210,6 +213,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw=
+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/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
@@ -244,8 +248,11 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
 github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
 github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
-github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v0.3.0 h1:q4c+kbcR0d5rSurhBR8dIgieOaYpXtsdTYfx22Cu6rs=
+github.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/zapr v0.2.0 h1:v6Ji8yBW77pva6NkJKQdHLAJKrIJKRHz0RXwPqCHSR4=
+github.com/go-logr/zapr v0.2.0/go.mod h1:qhKdvif7YF5GI9NWEpyxTSSBdGmzkNguibrdCNVPunU=
 github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
 github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
 github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
@@ -374,8 +381,9 @@ github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
 github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/googleapis/gnostic v0.5.1 h1:A8Yhf6EtqTv9RMsU6MQTyrtV1TjWlR6xU9BsZIwuTCM=
+github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
 github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -412,8 +420,9 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
@@ -424,11 +433,12 @@ github.com/hashicorp/memberlist v0.1.7/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/heketi/heketi v9.0.1-0.20190917153846-c2e2a4ab7ab9+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva7RS5ytVoSoholZQON6o=
 github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -452,6 +462,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
 github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
+github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -509,6 +520,8 @@ github.com/mdlayher/raw v0.0.0-20190313224157-43dbcdd7739d/go.mod h1:r1fbeITl2xL
 github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
 github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
 github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
+github.com/metallb/metallb-operator v0.0.0-20211130125940-1fa9bd1b26e6 h1:SnlhuUWaybz321LlQrY+h3Hwfl3vdOtUniCMKttbHSk=
+github.com/metallb/metallb-operator v0.0.0-20211130125940-1fa9bd1b26e6/go.mod h1:OklSBaSeVcigNS4wtnHABJJlpSype7HZ7xg1gWDffh4=
 github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -520,6 +533,7 @@ github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCL
 github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY=
 github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -530,6 +544,7 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
 github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/moby/ipvs v1.0.1/go.mod h1:2pngiyseZbIKXNv7hsKj3O9UEz30c53MT9005gt2hxQ=
 github.com/moby/sys/mountinfo v0.1.3/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o=
 github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
@@ -555,17 +570,24 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
 github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
 github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4=
+github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs=
+github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -684,6 +706,7 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
 github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
+github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 github.com/storageos/go-api v2.2.0+incompatible/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -750,10 +773,20 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
+go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
+go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -801,6 +834,7 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu
 golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@@ -852,6 +886,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@@ -919,6 +954,7 @@ golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -946,6 +982,7 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200602100848-8d3cce7afc34/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1017,6 +1054,9 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1053,12 +1093,15 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.1.0 h1:Phva6wqu+xR//Njw6iorylFFgn/z547tw5Ne3HZPQ+k=
+gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU=
 gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
 gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
 gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
@@ -1183,7 +1226,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
@@ -1209,6 +1251,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+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=
@@ -1225,6 +1268,7 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 k8s.io/api v0.20.2 h1:y/HR22XDZY3pniu9hIFDLpUCPq2w5eQ6aV/VFQ7uJMw=
 k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8=
+k8s.io/apiextensions-apiserver v0.20.2 h1:rfrMWQ87lhd8EzQWRnbQ4gXrniL/yTRBgYH1x1+BLlo=
 k8s.io/apiextensions-apiserver v0.20.2/go.mod h1:F6TXp389Xntt+LUq3vw6HFOLttPa0V8821ogLGwb6Zs=
 k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg=
 k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
@@ -1270,6 +1314,7 @@ k8s.io/metrics v0.20.2/go.mod h1:yTck5nl5wt/lIeLcU6g0b8/AKJf2girwe0PQiaM4Mwk=
 k8s.io/mount-utils v0.20.2/go.mod h1:Jv9NRZ5L2LF87A17GaGlArD+r3JAJdZFvo4XD1cG4Kc=
 k8s.io/sample-apiserver v0.20.2/go.mod h1:Q4VuPfFr3WOSkv6XKmY8FukZESdtH5MWqO0umFDfHcM=
 k8s.io/system-validators v1.2.0/go.mod h1:bPldcLgkIUK22ALflnsXk8pvkTEndYdNuaHH6gRrl0Q=
+k8s.io/utils v0.0.0-20200912215256-4140de9c8800/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw=
 k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
@@ -1283,6 +1328,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14 h1:TihvEz9MPj2u0KWds6E2OBUXfwaL4qRJ33c7HGiJpqk=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+sigs.k8s.io/controller-runtime v0.7.0 h1:bU20IBBEPccWz5+zXpLnpVsgBYxqclaHu1pVDl/gEt8=
+sigs.k8s.io/controller-runtime v0.7.0/go.mod h1:pJ3YBrJiAqMAZKi6UVGuE98ZrroV1p+pIhoHsMm9wdU=
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
diff --git a/tasks.py b/tasks.py
index ad851967..6210e59e 100644
--- a/tasks.py
+++ b/tasks.py
@@ -654,8 +654,9 @@ def lint(ctx, env="container"):
     "skip": "the list of arguments to pass into as -ginkgo.skip",
     "ipv4_service_range": "a range of IPv4 addresses for MetalLB to use when running in layer2 mode.",
     "ipv6_service_range": "a range of IPv6 addresses for MetalLB to use when running in layer2 mode.",
+    "use_operator": "use operator to update MetalLB configuration",
 })
-def e2etest(ctx, name="kind", export=None, kubeconfig=None, system_namespaces="kube-system,metallb-system", service_pod_port=80, skip_docker=False, focus="", skip="", ipv4_service_range=None, ipv6_service_range=None):
+def e2etest(ctx, name="kind", export=None, kubeconfig=None, system_namespaces="kube-system,metallb-system", service_pod_port=80, skip_docker=False, focus="", skip="", ipv4_service_range=None, ipv6_service_range=None, use_operator=False):
     """Run E2E tests against development cluster."""
     if skip_docker:
         opt_skip_docker = "--skip-docker"
@@ -666,6 +667,10 @@ def e2etest(ctx, name="kind", export=None, kubeconfig=None, system_namespaces="k
     if skip:
         ginkgo_skip = "--ginkgo.skip="+skip
 
+    opt_use_operator = ""
+    if use_operator:
+        opt_use_operator = "--use-operator"
+
     ginkgo_focus = ""
     if focus:
         ginkgo_focus = "--ginkgo.focus="+focus
@@ -693,7 +698,7 @@ def e2etest(ctx, name="kind", export=None, kubeconfig=None, system_namespaces="k
         ipv6_service_range = get_service_range(6)
 
     testrun = run("cd `git rev-parse --show-toplevel`/e2etest &&"
-            "go test -timeout 30m {} {} --provider=local --kubeconfig={} --service-pod-port={} -ipv4-service-range={} -ipv6-service-range={} {}".format(ginkgo_focus, ginkgo_skip, kubeconfig, service_pod_port, ipv4_service_range, ipv6_service_range, opt_skip_docker), warn="True")
+            "KUBECONFIG={} go test -timeout 30m {} {} --provider=local --kubeconfig={} --service-pod-port={} -ipv4-service-range={} -ipv6-service-range={} {} {}".format(kubeconfig, ginkgo_focus, ginkgo_skip, kubeconfig, service_pod_port, ipv4_service_range, ipv6_service_range, opt_skip_docker, opt_use_operator), warn="True")
 
     if export != None:
         run("kind export logs {}".format(export))


Test patch:
diff --git a/e2etest/bgp_test.go b/e2etest/bgp_test.go
index 9a8ad915..bffc7803 100644
--- a/e2etest/bgp_test.go
+++ b/e2etest/bgp_test.go
@@ -25,6 +25,7 @@ import (
 	"strconv"
 	"time"
 
+	"go.universe.tf/metallb/e2etest/pkg/config"
 	"go.universe.tf/metallb/e2etest/pkg/executor"
 	"go.universe.tf/metallb/e2etest/pkg/metrics"
 	"go.universe.tf/metallb/e2etest/pkg/routes"
@@ -70,6 +71,7 @@ type containerConfig struct {
 }
 
 var _ = ginkgo.Describe("BGP", func() {
+	var configUpdater config.Updater
 	var cs clientset.Interface
 	var f *framework.Framework
 	ibgpContainerConfig := containerConfig{
@@ -117,7 +119,7 @@ var _ = ginkgo.Describe("BGP", func() {
 	ginkgo.AfterEach(func() {
 		ginkgo.By("Clearing the previous configuration")
 		// Clean previous configuration.
-		err := updateConfigMap(cs, configFile{})
+		err := configUpdater.Clean()
 		framework.ExpectNoError(err)
 
 		err = stopFRRContainers(frrContainers)
@@ -141,15 +143,21 @@ var _ = ginkgo.Describe("BGP", func() {
 		}
 		framework.ExpectNoError(err)
 		cs = f.ClientSet
+		configUpdater = config.UpdaterForConfigMap(cs, testNameSpace)
+		if useOperator {
+			clientconfig := f.ClientConfig()
+			configUpdater, err = config.UpdaterForOperator(clientconfig, testNameSpace)
+			framework.ExpectNoError(err)
+		}
 	})
 
 	table.DescribeTable("A service of protocol load balancer should work with", func(pairingIPFamily, setProtocoltest string, poolAddresses []string, tweak testservice.Tweak) {
 		var allNodes *corev1.NodeList
-		configData := configFile{
-			Pools: []addressPool{
+		configData := config.File{
+			Pools: []config.AddressPool{
 				{
 					Name:      "bgp-test",
-					Protocol:  BGP,
+					Protocol:  config.BGP,
 					Addresses: poolAddresses,
 				},
 			},
@@ -159,7 +167,7 @@ var _ = ginkgo.Describe("BGP", func() {
 			pairExternalFRRWithNodes(cs, c, pairingIPFamily)
 		}
 
-		err := updateConfigMap(cs, configData)
+		err := configUpdater.Update(configData)
 		framework.ExpectNoError(err)
 
 		for _, c := range frrContainers {
@@ -255,11 +263,11 @@ var _ = ginkgo.Describe("BGP", func() {
 				peerAddrs = append(peerAddrs, address+fmt.Sprintf(":%d", c.RouterConfig.BGPPort))
 			}
 
-			configData := configFile{
-				Pools: []addressPool{
+			configData := config.File{
+				Pools: []config.AddressPool{
 					{
 						Name:      poolName,
-						Protocol:  BGP,
+						Protocol:  config.BGP,
 						Addresses: []string{poolAddress},
 					},
 				},
@@ -269,7 +277,7 @@ var _ = ginkgo.Describe("BGP", func() {
 				pairExternalFRRWithNodes(cs, c, ipFamily)
 			}
 
-			err := updateConfigMap(cs, configData)
+			err := configUpdater.Update(configData)
 			framework.ExpectNoError(err)
 
 			for _, c := range frrContainers {
@@ -375,12 +383,12 @@ var _ = ginkgo.Describe("BGP", func() {
 	ginkgo.Context("validate different AddressPools for type=Loadbalancer", func() {
 		ginkgo.AfterEach(func() {
 			// Clean previous configuration.
-			err := updateConfigMap(cs, configFile{})
+			err := configUpdater.Clean()
 			framework.ExpectNoError(err)
 		})
 
-		table.DescribeTable("set different AddressPools ranges modes", func(addressPools []addressPool, pairingFamily string, tweak testservice.Tweak) {
-			configData := configFile{
+		table.DescribeTable("set different AddressPools ranges modes", func(addressPools []config.AddressPool, pairingFamily string, tweak testservice.Tweak) {
+			configData := config.File{
 				Peers: peersForContainers(frrContainers, pairingFamily),
 				Pools: addressPools,
 			}
@@ -388,7 +396,7 @@ var _ = ginkgo.Describe("BGP", func() {
 				pairExternalFRRWithNodes(cs, c, pairingFamily)
 			}
 
-			err := updateConfigMap(cs, configData)
+			err := configUpdater.Update(configData)
 			framework.ExpectNoError(err)
 
 			for _, c := range frrContainers {
@@ -416,56 +424,56 @@ var _ = ginkgo.Describe("BGP", func() {
 				validateService(cs, svc, allNodes.Items, c)
 			}
 		},
-			table.Entry("IPV4 - test AddressPool defined by address range", []addressPool{
+			table.Entry("IPV4 - test AddressPool defined by address range", []config.AddressPool{
 				{
 					Name:     "bgp-test",
-					Protocol: BGP,
+					Protocol: config.BGP,
 					Addresses: []string{
 						"192.168.10.0-192.168.10.18",
 					},
 				}}, "ipv4", testservice.TrafficPolicyCluster,
 			),
-			table.Entry("IPV4 - test AddressPool defined by network prefix", []addressPool{
+			table.Entry("IPV4 - test AddressPool defined by network prefix", []config.AddressPool{
 				{
 					Name:     "bgp-test",
-					Protocol: BGP,
+					Protocol: config.BGP,
 					Addresses: []string{
 						"192.168.10.0/24",
 					},
 				}}, "ipv4", testservice.TrafficPolicyCluster,
 			),
-			table.Entry("IPV6 - test AddressPool defined by address range", []addressPool{
+			table.Entry("IPV6 - test AddressPool defined by address range", []config.AddressPool{
 				{
 					Name:     "bgp-test",
-					Protocol: BGP,
+					Protocol: config.BGP,
 					Addresses: []string{
 						"fc00:f853:0ccd:e799::0-fc00:f853:0ccd:e799::18",
 					},
 				}}, "ipv6", testservice.TrafficPolicyCluster,
 			),
-			table.Entry("IPV6 - test AddressPool defined by network prefix", []addressPool{
+			table.Entry("IPV6 - test AddressPool defined by network prefix", []config.AddressPool{
 				{
 					Name:     "bgp-test",
-					Protocol: BGP,
+					Protocol: config.BGP,
 					Addresses: []string{
 						"fc00:f853:0ccd:e799::/124",
 					},
 				}}, "ipv6", testservice.TrafficPolicyCluster,
 			),
-			table.Entry("DUALSTACK - test AddressPool defined by address range", []addressPool{
+			table.Entry("DUALSTACK - test AddressPool defined by address range", []config.AddressPool{
 				{
 					Name:     "bgp-test",
-					Protocol: BGP,
+					Protocol: config.BGP,
 					Addresses: []string{
 						"192.168.10.0-192.168.10.18",
 						"fc00:f853:0ccd:e799::0-fc00:f853:0ccd:e799::18",
 					},
 				}}, "dual", testservice.TrafficPolicyCluster,
 			),
-			table.Entry("DUALSTACK - test AddressPool defined by network prefix", []addressPool{
+			table.Entry("DUALSTACK - test AddressPool defined by network prefix", []config.AddressPool{
 				{
 					Name:     "bgp-test",
-					Protocol: BGP,
+					Protocol: config.BGP,
 					Addresses: []string{
 						"192.168.10.0/24",
 						"fc00:f853:0ccd:e799::/124",
@@ -476,19 +484,19 @@ var _ = ginkgo.Describe("BGP", func() {
 	})
 
 	ginkgo.Context("BFD", func() {
-		table.DescribeTable("should work with the given bfd profile", func(bfd bfdProfile, pairingFamily string, poolAddresses []string, tweak testservice.Tweak) {
-			configData := configFile{
-				Pools: []addressPool{
+		table.DescribeTable("should work with the given bfd profile", func(bfd config.BfdProfile, pairingFamily string, poolAddresses []string, tweak testservice.Tweak) {
+			configData := config.File{
+				Pools: []config.AddressPool{
 					{
 						Name:      "bfd-test",
-						Protocol:  BGP,
+						Protocol:  config.BGP,
 						Addresses: poolAddresses,
 					},
 				},
 				Peers:       withBFD(peersForContainers(frrContainers, pairingFamily), bfd.Name),
-				BFDProfiles: []bfdProfile{bfd},
+				BFDProfiles: []config.BfdProfile{bfd},
 			}
-			err := updateConfigMap(cs, configData)
+			err := configUpdater.Update(configData)
 			framework.ExpectNoError(err)
 
 			for _, c := range frrContainers {
@@ -519,7 +527,7 @@ var _ = ginkgo.Describe("BGP", func() {
 					if err != nil {
 						return err
 					}
-					toCompare := BFDProfileWithDefaults(bfd)
+					toCompare := config.BFDProfileWithDefaults(bfd)
 					err = frr.BFDPeersMatchNodes(allNodes.Items, bfdPeers, pairingFamily)
 					if err != nil {
 						return err
@@ -537,11 +545,11 @@ var _ = ginkgo.Describe("BGP", func() {
 
 		},
 			table.Entry("IPV4 - default",
-				bfdProfile{
+				config.BfdProfile{
 					Name: "bar",
 				}, "ipv4", []string{v4PoolAddresses}, testservice.TrafficPolicyCluster),
 			table.Entry("IPV4 - full params",
-				bfdProfile{
+				config.BfdProfile{
 					Name:             "full1",
 					ReceiveInterval:  uint32Ptr(60),
 					TransmitInterval: uint32Ptr(61),
@@ -551,7 +559,7 @@ var _ = ginkgo.Describe("BGP", func() {
 					MinimumTTL:       uint32Ptr(254),
 				}, "ipv4", []string{v4PoolAddresses}, testservice.TrafficPolicyCluster),
 			table.Entry("IPV4 - echo mode enabled",
-				bfdProfile{
+				config.BfdProfile{
 					Name:             "echo",
 					ReceiveInterval:  uint32Ptr(80),
 					TransmitInterval: uint32Ptr(81),
@@ -561,11 +569,11 @@ var _ = ginkgo.Describe("BGP", func() {
 					MinimumTTL:       uint32Ptr(254),
 				}, "ipv4", []string{v4PoolAddresses}, testservice.TrafficPolicyCluster),
 			table.Entry("IPV6 - default",
-				bfdProfile{
+				config.BfdProfile{
 					Name: "bar",
 				}, "ipv6", []string{v6PoolAddresses}, testservice.TrafficPolicyCluster),
 			table.Entry("IPV6 - full params",
-				bfdProfile{
+				config.BfdProfile{
 					Name:             "full1",
 					ReceiveInterval:  uint32Ptr(60),
 					TransmitInterval: uint32Ptr(61),
@@ -575,7 +583,7 @@ var _ = ginkgo.Describe("BGP", func() {
 					MinimumTTL:       uint32Ptr(254),
 				}, "ipv6", []string{v6PoolAddresses}, testservice.TrafficPolicyCluster),
 			table.Entry("IPV6 - echo mode enabled",
-				bfdProfile{
+				config.BfdProfile{
 					Name:             "echo",
 					ReceiveInterval:  uint32Ptr(80),
 					TransmitInterval: uint32Ptr(81),
@@ -585,7 +593,7 @@ var _ = ginkgo.Describe("BGP", func() {
 					MinimumTTL:       uint32Ptr(254),
 				}, "ipv6", []string{v6PoolAddresses}, testservice.TrafficPolicyCluster),
 			table.Entry("DUALSTACK - full params",
-				bfdProfile{
+				config.BfdProfile{
 					Name:             "full1",
 					ReceiveInterval:  uint32Ptr(60),
 					TransmitInterval: uint32Ptr(61),
@@ -599,19 +607,19 @@ var _ = ginkgo.Describe("BGP", func() {
 				}),
 		)
 
-		table.DescribeTable("metrics", func(bfd bfdProfile, pairingFamily string, poolAddresses []string) {
-			configData := configFile{
-				Pools: []addressPool{
+		table.DescribeTable("metrics", func(bfd config.BfdProfile, pairingFamily string, poolAddresses []string) {
+			configData := config.File{
+				Pools: []config.AddressPool{
 					{
 						Name:      "bfd-test",
-						Protocol:  BGP,
+						Protocol:  config.BGP,
 						Addresses: poolAddresses,
 					},
 				},
 				Peers:       withBFD(peersForContainers(frrContainers, pairingFamily), bfd.Name),
-				BFDProfiles: []bfdProfile{bfd},
+				BFDProfiles: []config.BfdProfile{bfd},
 			}
-			err := updateConfigMap(cs, configData)
+			err := configUpdater.Update(configData)
 			framework.ExpectNoError(err)
 
 			for _, c := range frrContainers {
@@ -731,11 +739,11 @@ var _ = ginkgo.Describe("BGP", func() {
 			}
 		},
 			table.Entry("IPV4 - default",
-				bfdProfile{
+				config.BfdProfile{
 					Name: "bar",
 				}, "ipv4", []string{v4PoolAddresses}),
 			table.Entry("IPV4 - echo mode enabled",
-				bfdProfile{
+				config.BfdProfile{
 					Name:             "echo",
 					ReceiveInterval:  uint32Ptr(80),
 					TransmitInterval: uint32Ptr(81),
@@ -745,11 +753,11 @@ var _ = ginkgo.Describe("BGP", func() {
 					MinimumTTL:       uint32Ptr(254),
 				}, "ipv4", []string{v4PoolAddresses}),
 			table.Entry("IPV6 - default",
-				bfdProfile{
+				config.BfdProfile{
 					Name: "bar",
 				}, "ipv6", []string{v6PoolAddresses}),
 			table.Entry("IPV6 - echo mode enabled",
-				bfdProfile{
+				config.BfdProfile{
 					Name:             "echo",
 					ReceiveInterval:  uint32Ptr(80),
 					TransmitInterval: uint32Ptr(81),
@@ -759,7 +767,7 @@ var _ = ginkgo.Describe("BGP", func() {
 					MinimumTTL:       uint32Ptr(254),
 				}, "ipv6", []string{v6PoolAddresses}),
 			table.Entry("DUALSTACK - full params",
-				bfdProfile{
+				config.BfdProfile{
 					Name:             "full1",
 					ReceiveInterval:  uint32Ptr(60),
 					TransmitInterval: uint32Ptr(61),
@@ -775,7 +783,7 @@ var _ = ginkgo.Describe("BGP", func() {
 		table.DescribeTable("should work after subsequent configuration updates", func(addressRange string, ipFamily string) {
 			var services []*corev1.Service
 			var servicesIngressIP []string
-			var pools []addressPool
+			var pools []config.AddressPool
 
 			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
 			framework.ExpectNoError(err)
@@ -787,16 +795,16 @@ var _ = ginkgo.Describe("BGP", func() {
 				lastIP, err := getIPFromRangeByIndex(addressRange, i*10+10)
 				framework.ExpectNoError(err)
 				addressesRange := fmt.Sprintf("%s-%s", firstIP, lastIP)
-				pool := addressPool{
+				pool := config.AddressPool{
 					Name:     fmt.Sprintf("test-addresspool%d", i+1),
-					Protocol: BGP,
+					Protocol: config.BGP,
 					Addresses: []string{
 						addressesRange,
 					},
 				}
 				pools = append(pools, pool)
 
-				configData := configFile{
+				configData := config.File{
 					Pools: pools,
 					Peers: peersForContainers(frrContainers, ipFamily),
 				}
@@ -805,7 +813,7 @@ var _ = ginkgo.Describe("BGP", func() {
 					pairExternalFRRWithNodes(cs, c, ipFamily)
 				}
 
-				err = updateConfigMap(cs, configData)
+				err = configUpdater.Update(configData)
 				framework.ExpectNoError(err)
 
 				for _, c := range frrContainers {
@@ -825,7 +833,7 @@ var _ = ginkgo.Describe("BGP", func() {
 				ginkgo.By("validate LoadBalancer IP is in the AddressPool range")
 				ingressIP := e2eservice.GetIngressPoint(
 					&svc.Status.LoadBalancer.Ingress[0])
-				err = validateIPInRange([]addressPool{pool}, ingressIP)
+				err = validateIPInRange([]config.AddressPool{pool}, ingressIP)
 				framework.ExpectNoError(err)
 
 				services = append(services, svc)
@@ -850,10 +858,10 @@ var _ = ginkgo.Describe("BGP", func() {
 			for i, c := range frrContainers {
 				ginkgo.By("configure peer")
 
-				configData := configFile{
+				configData := config.File{
 					Peers: peersForContainers([]*frrcontainer.FRR{c}, ipFamily),
 				}
-				err := updateConfigMap(cs, configData)
+				err := configUpdater.Update(configData)
 				framework.ExpectNoError(err)
 
 				pairExternalFRRWithNodes(cs, c, ipFamily)
@@ -867,8 +875,8 @@ var _ = ginkgo.Describe("BGP", func() {
 			table.Entry("IPV6", "ipv6"))
 
 		table.DescribeTable("configure bgp community and verify it gets propagated",
-			func(addressPools []addressPool, ipFamily string) {
-				configData := configFile{
+			func(addressPools []config.AddressPool, ipFamily string) {
+				configData := config.File{
 					Peers: peersForContainers(frrContainers, ipFamily),
 					Pools: addressPools,
 				}
@@ -876,7 +884,7 @@ var _ = ginkgo.Describe("BGP", func() {
 					pairExternalFRRWithNodes(cs, c, ipFamily)
 				}
 
-				err := updateConfigMap(cs, configData)
+				err := configUpdater.Update(configData)
 				framework.ExpectNoError(err)
 
 				for _, c := range frrContainers {
@@ -907,14 +915,14 @@ var _ = ginkgo.Describe("BGP", func() {
 					}, 4*time.Minute, 1*time.Second).Should(BeNil())
 				}
 			},
-			table.Entry("IPV4", []addressPool{
+			table.Entry("IPV4", []config.AddressPool{
 				{
 					Name:     "bgp-test",
-					Protocol: BGP,
+					Protocol: config.BGP,
 					Addresses: []string{
 						"192.168.10.0/24",
 					},
-					BGPAdvertisements: []bgpAdvertisement{
+					BGPAdvertisements: []config.BgpAdvertisement{
 						{
 							Communities: []string{
 								CommunityNoAdv,
@@ -922,14 +930,14 @@ var _ = ginkgo.Describe("BGP", func() {
 						},
 					},
 				}}, "ipv4"),
-			table.Entry("IPV6", []addressPool{
+			table.Entry("IPV6", []config.AddressPool{
 				{
 					Name:     "bgp-test",
-					Protocol: BGP,
+					Protocol: config.BGP,
 					Addresses: []string{
 						"fc00:f853:0ccd:e799::0-fc00:f853:0ccd:e799::18",
 					},
-					BGPAdvertisements: []bgpAdvertisement{
+					BGPAdvertisements: []config.BgpAdvertisement{
 						{
 							Communities: []string{
 								CommunityNoAdv,
@@ -1131,8 +1139,8 @@ func stopFRRContainers(containers []*frrcontainer.FRR) error {
 	return g.Wait()
 }
 
-func peersForContainers(containers []*frrcontainer.FRR, ipFamily string) []peer {
-	var peers []peer
+func peersForContainers(containers []*frrcontainer.FRR, ipFamily string) []config.Peer {
+	var peers []config.Peer
 
 	for i, c := range frrContainers {
 		addresses := c.AddressesForFamily(ipFamily)
@@ -1141,7 +1149,7 @@ func peersForContainers(containers []*frrcontainer.FRR, ipFamily string) []peer
 			holdTime = fmt.Sprintf("%ds", i*180)
 		}
 		for _, address := range addresses {
-			peers = append(peers, peer{
+			peers = append(peers, config.Peer{
 				Addr:     address,
 				ASN:      c.RouterConfig.ASN,
 				MyASN:    c.NeighborConfig.ASN,
@@ -1155,14 +1163,14 @@ func peersForContainers(containers []*frrcontainer.FRR, ipFamily string) []peer
 	return peers
 }
 
-func withBFD(peers []peer, bfdProfile string) []peer {
+func withBFD(peers []config.Peer, bfdProfile string) []config.Peer {
 	for i := range peers {
 		peers[i].BFDProfile = bfdProfile
 	}
 	return peers
 }
 
-func checkBFDConfigPropagated(nodeConfig bfdProfile, peerConfig bgpfrr.BFDPeer) error {
+func checkBFDConfigPropagated(nodeConfig config.BfdProfile, peerConfig bgpfrr.BFDPeer) error {
 	if peerConfig.Status != "up" {
 		return fmt.Errorf("Peer status not up")
 	}
diff --git a/e2etest/e2etest_suite_test.go b/e2etest/e2etest_suite_test.go
index 634cbb66..62de0527 100644
--- a/e2etest/e2etest_suite_test.go
+++ b/e2etest/e2etest_suite_test.go
@@ -34,6 +34,7 @@ import (
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/klog"
 	"k8s.io/kubernetes/test/e2e/framework"
 	e2econfig "k8s.io/kubernetes/test/e2e/framework/config"
@@ -56,17 +57,27 @@ var (
 	containersNetwork = defaultContainersNetwork
 	hostIPv4          string
 	hostIPv6          string
+	useOperator       bool
 )
 
 // handleFlags sets up all flags and parses the command line.
 func handleFlags() {
 	e2econfig.CopyFlags(e2econfig.Flags, flag.CommandLine)
 	framework.RegisterCommonFlags(flag.CommandLine)
-	framework.RegisterClusterFlags(flag.CommandLine)
+	/*
+		Using framework.RegisterClusterFlags(flag.CommandLine) results in a panic:
+		"flag redefined: kubeconfig".
+		This happens because controller-runtime registers the kubeconfig flag as well.
+		To solve this we set the framework's kubeconfig directly via the KUBECONFIG env var
+		instead of letting it call the flag. Since we also use the provider flag it is handled manually.
+	*/
+	flag.StringVar(&framework.TestContext.Provider, "provider", "", "The name of the Kubernetes provider (gce, gke, local, skeleton (the fallback if not set), etc.)")
+	framework.TestContext.KubeConfig = os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
 	flag.IntVar(&servicePodPort, "service-pod-port", 80, "port number that pod opens, default: 80")
 	flag.BoolVar(&skipDockerCmd, "skip-docker", false, "set this to true if the BGP daemon is running on the host instead of in a container")
 	flag.StringVar(&ipv4ServiceRange, "ipv4-service-range", "0", "a range of IPv4 addresses for MetalLB to use when running in layer2 mode")
 	flag.StringVar(&ipv6ServiceRange, "ipv6-service-range", "0", "a range of IPv6 addresses for MetalLB to use when running in layer2 mode")
+	flag.BoolVar(&useOperator, "use-operator", false, "set this to true to run the tests using operator custom resources")
 	flag.Parse()
 }
 
@@ -101,6 +112,9 @@ func TestE2E(t *testing.T) {
 }
 
 var _ = ginkgo.BeforeSuite(func() {
+	// Make sure the framework's kubeconfig is set.
+	framework.ExpectNotEqual(framework.TestContext.KubeConfig, "", fmt.Sprintf("%s env var not set", clientcmd.RecommendedConfigPathEnvVar))
+
 	if ns := os.Getenv("OO_INSTALL_NAMESPACE"); len(ns) != 0 {
 		testNameSpace = ns
 	}
diff --git a/e2etest/layer2_test.go b/e2etest/layer2_test.go
index 3e160ae0..f84ee34a 100644
--- a/e2etest/layer2_test.go
+++ b/e2etest/layer2_test.go
@@ -32,6 +32,7 @@ import (
 	"github.com/onsi/ginkgo/extensions/table"
 	"github.com/onsi/gomega"
 	pkgerrors "github.com/pkg/errors"
+	"go.universe.tf/metallb/e2etest/pkg/config"
 	"go.universe.tf/metallb/e2etest/pkg/executor"
 	"go.universe.tf/metallb/e2etest/pkg/mac"
 	"go.universe.tf/metallb/e2etest/pkg/metrics"
@@ -48,16 +49,24 @@ import (
 var _ = ginkgo.Describe("L2", func() {
 	f := framework.NewDefaultFramework("l2")
 	var loadBalancerCreateTimeout time.Duration
+	var configUpdater config.Updater
 	var cs clientset.Interface
+	var err error
 
 	ginkgo.BeforeEach(func() {
 		cs = f.ClientSet
 		loadBalancerCreateTimeout = e2eservice.GetServiceLoadBalancerCreationTimeout(cs)
+		configUpdater = config.UpdaterForConfigMap(cs, testNameSpace)
+		if useOperator {
+			clientconfig := f.ClientConfig()
+			configUpdater, err = config.UpdaterForOperator(clientconfig, testNameSpace)
+			framework.ExpectNoError(err)
+		}
 	})
 
 	ginkgo.AfterEach(func() {
 		// Clean previous configuration.
-		err := updateConfigMap(cs, configFile{})
+		err := configUpdater.Clean()
 		framework.ExpectNoError(err)
 
 		if ginkgo.CurrentGinkgoTestDescription().Failed {
@@ -67,11 +76,11 @@ var _ = ginkgo.Describe("L2", func() {
 
 	ginkgo.Context("type=Loadbalancer", func() {
 		ginkgo.BeforeEach(func() {
-			configData := configFile{
-				Pools: []addressPool{
+			configData := config.File{
+				Pools: []config.AddressPool{
 					{
 						Name:     "l2-test",
-						Protocol: Layer2,
+						Protocol: config.Layer2,
 						Addresses: []string{
 							ipv4ServiceRange,
 							ipv6ServiceRange,
@@ -79,7 +88,7 @@ var _ = ginkgo.Describe("L2", func() {
 					},
 				},
 			}
-			err := updateConfigMap(cs, configData)
+			err := configUpdater.Update(configData)
 			framework.ExpectNoError(err)
 		})
 
@@ -145,11 +154,11 @@ var _ = ginkgo.Describe("L2", func() {
 
 	ginkgo.Context("validate different AddressPools for type=Loadbalancer", func() {
 
-		table.DescribeTable("set different AddressPools ranges modes", func(getAddressPools func() []addressPool) {
-			configData := configFile{
+		table.DescribeTable("set different AddressPools ranges modes", func(getAddressPools func() []config.AddressPool) {
+			configData := config.File{
 				Pools: getAddressPools(),
 			}
-			err := updateConfigMap(cs, configData)
+			err := configUpdater.Update(configData)
 			framework.ExpectNoError(err)
 
 			svc, _ := createServiceWithBackend(cs, f.Namespace.Name, "external-local-lb", testservice.TrafficPolicyCluster)
@@ -174,11 +183,11 @@ var _ = ginkgo.Describe("L2", func() {
 			err = wgetRetry(address, executor.Host)
 			framework.ExpectNoError(err)
 		},
-			table.Entry("AddressPool defined by address range", func() []addressPool {
-				return []addressPool{
+			table.Entry("AddressPool defined by address range", func() []config.AddressPool {
+				return []config.AddressPool{
 					{
 						Name:     "l2-test",
-						Protocol: Layer2,
+						Protocol: config.Layer2,
 						Addresses: []string{
 							ipv4ServiceRange,
 							ipv6ServiceRange,
@@ -186,7 +195,7 @@ var _ = ginkgo.Describe("L2", func() {
 					},
 				}
 			}),
-			table.Entry("AddressPool defined by network prefix", func() []addressPool {
+			table.Entry("AddressPool defined by network prefix", func() []config.AddressPool {
 				var ipv4AddressesByCIDR []string
 				var ipv6AddressesByCIDR []string
 
@@ -202,10 +211,10 @@ var _ = ginkgo.Describe("L2", func() {
 					ipv6AddressesByCIDR = append(ipv6AddressesByCIDR, cidr.String())
 				}
 
-				return []addressPool{
+				return []config.AddressPool{
 					{
 						Name:      "l2-test",
-						Protocol:  Layer2,
+						Protocol:  config.Layer2,
 						Addresses: append(ipv4AddressesByCIDR, ipv6AddressesByCIDR...),
 					},
 				}
@@ -214,11 +223,11 @@ var _ = ginkgo.Describe("L2", func() {
 	})
 
 	table.DescribeTable("different services sharing the same ip should advertise from the same node", func(ipRange *string) {
-		configData := configFile{
-			Pools: []addressPool{
+		configData := config.File{
+			Pools: []config.AddressPool{
 				{
 					Name:     "l2-services-same-ip-test",
-					Protocol: Layer2,
+					Protocol: config.Layer2,
 					Addresses: []string{
 						ipv4ServiceRange,
 						ipv6ServiceRange,
@@ -226,7 +235,7 @@ var _ = ginkgo.Describe("L2", func() {
 				},
 			},
 		}
-		err := updateConfigMap(cs, configData)
+		err := configUpdater.Update(configData)
 		framework.ExpectNoError(err)
 		namespace := f.Namespace.Name
 
@@ -334,11 +343,11 @@ var _ = ginkgo.Describe("L2", func() {
 		table.DescribeTable("should be exposed by the controller", func(ipFamily string) {
 			poolName := "l2-metrics-test"
 
-			configData := configFile{
-				Pools: []addressPool{
+			configData := config.File{
+				Pools: []config.AddressPool{
 					{
 						Name:     poolName,
-						Protocol: Layer2,
+						Protocol: config.Layer2,
 						Addresses: []string{
 							ipv4ServiceRange,
 							ipv6ServiceRange,
@@ -349,7 +358,7 @@ var _ = ginkgo.Describe("L2", func() {
 			poolCount, err := poolCount(configData.Pools[0])
 			framework.ExpectNoError(err)
 
-			err = updateConfigMap(cs, configData)
+			err = configUpdater.Update(configData)
 			framework.ExpectNoError(err)
 
 			ginkgo.By("checking the metrics when no service is added")
@@ -457,7 +466,7 @@ var _ = ginkgo.Describe("L2", func() {
 	table.DescribeTable("validate requesting a specific address pool for Loadbalancer service", func(ipRange *string) {
 		var services []*corev1.Service
 		var servicesIngressIP []string
-		var pools []addressPool
+		var pools []config.AddressPool
 
 		namspace := f.Namespace.Name
 
@@ -466,19 +475,19 @@ var _ = ginkgo.Describe("L2", func() {
 			ip, err := getIPFromRangeByIndex(*ipRange, i)
 			framework.ExpectNoError(err)
 			addressesRange := fmt.Sprintf("%s-%s", ip, ip)
-			pool := addressPool{
+			pool := config.AddressPool{
 				Name:     fmt.Sprintf("test-addresspool%d", i+1),
-				Protocol: Layer2,
+				Protocol: config.Layer2,
 				Addresses: []string{
 					addressesRange,
 				},
 			}
 			pools = append(pools, pool)
 
-			configData := configFile{
+			configData := config.File{
 				Pools: pools,
 			}
-			err = updateConfigMap(cs, configData)
+			err = configUpdater.Update(configData)
 			framework.ExpectNoError(err)
 
 			ginkgo.By(fmt.Sprintf("configure service number %d", i+1))
@@ -503,7 +512,7 @@ var _ = ginkgo.Describe("L2", func() {
 
 			ginkgo.By("validate LoadBalancer IP is in the AddressPool range")
 			ingressIP := e2eservice.GetIngressPoint(&svc.Status.LoadBalancer.Ingress[0])
-			err = validateIPInRange([]addressPool{pool}, ingressIP)
+			err = validateIPInRange([]config.AddressPool{pool}, ingressIP)
 			framework.ExpectNoError(err)
 
 			services = append(services, svc)
@@ -582,7 +591,7 @@ func advertisingNodeFromMAC(nodes []corev1.Node, ip string, exc executor.Executo
 
 // taken from internal: poolCount returns the number of addresses in the pool.
 // TODO: find a better place for this func.
-func poolCount(p addressPool) (int64, error) {
+func poolCount(p config.AddressPool) (int64, error) {
 	var total int64
 	for _, r := range p.Addresses {
 		cidrs, err := internalconfig.ParseCIDR(r)
diff --git a/e2etest/parse_config.go b/e2etest/pkg/config/parse.go
similarity index 65%
rename from e2etest/parse_config.go
rename to e2etest/pkg/config/parse.go
index 77985445..ff27af8b 100644
--- a/e2etest/parse_config.go
+++ b/e2etest/pkg/config/parse.go
@@ -1,16 +1,6 @@
 // SPDX-License-Identifier:Apache-2.0
 
-package e2e
-
-import (
-	"context"
-
-	"github.com/pkg/errors"
-	"gopkg.in/yaml.v2"
-	v1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	clientset "k8s.io/client-go/kubernetes"
-)
+package config
 
 // Proto holds the protocol we are speaking.
 type Proto string
@@ -23,14 +13,14 @@ const (
 
 // configFile is the configuration as parsed out of the ConfigMap,
 // without validation or useful high level types.
-type configFile struct {
-	Peers          []peer            `yaml:"peers,omitempty"`
+type File struct {
+	Peers          []Peer            `yaml:"peers,omitempty"`
 	BGPCommunities map[string]string `yaml:"bgp-communities,omitempty"`
-	Pools          []addressPool     `yaml:"address-pools,omitempty"`
-	BFDProfiles    []bfdProfile      `yaml:"bfd-profiles"`
+	Pools          []AddressPool     `yaml:"address-pools,omitempty"`
+	BFDProfiles    []BfdProfile      `yaml:"bfd-profiles"`
 }
 
-type peer struct {
+type Peer struct {
 	MyASN         uint32         `yaml:"my-asn,omitempty"`
 	ASN           uint32         `yaml:"peer-asn,omitempty"`
 	Addr          string         `yaml:"peer-address,omitempty"`
@@ -38,38 +28,38 @@ type peer struct {
 	Port          uint16         `yaml:"peer-port,omitempty"`
 	HoldTime      string         `yaml:"hold-time,omitempty"`
 	RouterID      string         `yaml:"router-id,omitempty"`
-	NodeSelectors []nodeSelector `yaml:"node-selectors,omitempty"`
+	NodeSelectors []NodeSelector `yaml:"node-selectors,omitempty"`
 	Password      string         `yaml:"password,omitempty"`
 	BFDProfile    string         `yaml:"bfd-profile,omitempty"`
 }
 
-type nodeSelector struct {
+type NodeSelector struct {
 	MatchLabels      map[string]string      `yaml:"match-labels,omitempty"`
-	MatchExpressions []selectorRequirements `yaml:"match-expressions,omitempty"`
+	MatchExpressions []SelectorRequirements `yaml:"match-expressions,omitempty"`
 }
 
-type selectorRequirements struct {
+type SelectorRequirements struct {
 	Key      string   `yaml:"key,omitempty"`
 	Operator string   `yaml:"operator,omitempty"`
 	Values   []string `yaml:"values,omitempty"`
 }
 
-type addressPool struct {
+type AddressPool struct {
 	Protocol          Proto              `yaml:"protocol,omitempty"`
 	Name              string             `yaml:"name,omitempty"`
 	Addresses         []string           `yaml:"addresses,omitempty"`
 	AvoidBuggyIPs     bool               `yaml:"avoid-buggy-ips,omitempty"`
 	AutoAssign        *bool              `yaml:"auto-assign,omitempty"`
-	BGPAdvertisements []bgpAdvertisement `yaml:"bgp-advertisements,omitempty"`
+	BGPAdvertisements []BgpAdvertisement `yaml:"bgp-advertisements,omitempty"`
 }
 
-type bgpAdvertisement struct {
+type BgpAdvertisement struct {
 	AggregationLength *int     `yaml:"aggregation-length,omitempty"`
 	LocalPref         *uint32  `yaml:"localpref,omitempty"`
 	Communities       []string `yaml:"communities,omitempty"`
 }
 
-type bfdProfile struct {
+type BfdProfile struct {
 	Name             string  `yaml:"name"`
 	ReceiveInterval  *uint32 `yaml:"receive-interval,omitempty"`
 	TransmitInterval *uint32 `yaml:"transmit-interval,omitempty"`
@@ -80,28 +70,8 @@ type bfdProfile struct {
 	MinimumTTL       *uint32 `yaml:"minimum-ttl,omitempty"`
 }
 
-func updateConfigMap(cs clientset.Interface, data configFile) error {
-	resData, err := yaml.Marshal(data)
-	if err != nil {
-		return errors.Wrapf(err, "Failed to marshal MetalLB ConfigMap data")
-	}
-
-	_, err = cs.CoreV1().ConfigMaps(testNameSpace).Update(context.TODO(), &v1.ConfigMap{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      configMapName,
-			Namespace: testNameSpace,
-		},
-		Data: map[string]string{"config": string(resData)},
-	}, metav1.UpdateOptions{})
-	if err != nil {
-		return errors.Wrapf(err, "Failed to update MetalLB ConfigMap")
-	}
-
-	return nil
-}
-
-func BFDProfileWithDefaults(profile bfdProfile) bfdProfile {
-	res := bfdProfile{}
+func BFDProfileWithDefaults(profile BfdProfile) BfdProfile {
+	res := BfdProfile{}
 	res.Name = profile.Name
 	res.ReceiveInterval = valueWithDefault(profile.ReceiveInterval, 300)
 	res.TransmitInterval = valueWithDefault(profile.TransmitInterval, 300)
diff --git a/e2etest/pkg/config/update.go b/e2etest/pkg/config/update.go
new file mode 100644
index 00000000..fb2a63b4
--- /dev/null
+++ b/e2etest/pkg/config/update.go
@@ -0,0 +1,287 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package config
+
+import (
+	"context"
+	"time"
+
+	operatorv1beta1 "github.com/metallb/metallb-operator/api/v1beta1"
+	"github.com/pkg/errors"
+	"gopkg.in/yaml.v2"
+	v1 "k8s.io/api/core/v1"
+	apierr "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	clientset "k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type Updater interface {
+	Update(f File) error
+	Clean() error
+}
+
+type configmapUpdater struct {
+	clientset.Interface
+	name      string
+	namespace string
+}
+
+func UpdaterForConfigMap(c clientset.Interface, ns string) *configmapUpdater {
+	return &configmapUpdater{
+		Interface: c,
+		name:      "config",
+		namespace: ns,
+	}
+}
+
+func (c configmapUpdater) Update(f File) error {
+	resData, err := yaml.Marshal(f)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to marshal MetalLB ConfigMap data")
+	}
+
+	_, err = c.CoreV1().ConfigMaps(c.namespace).Update(context.TODO(), &v1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      c.name,
+			Namespace: c.namespace,
+		},
+		Data: map[string]string{"config": string(resData)},
+	}, metav1.UpdateOptions{})
+
+	if err != nil {
+		return errors.Wrapf(err, "Failed to update MetalLB ConfigMap")
+	}
+
+	return nil
+}
+
+func (c configmapUpdater) Clean() error {
+	return c.Update(File{})
+}
+
+type operatorUpdater struct {
+	client.Client
+	namespace string
+}
+
+func UpdaterForOperator(r *rest.Config, ns string) (*operatorUpdater, error) {
+	myScheme := runtime.NewScheme()
+
+	if err := operatorv1beta1.AddToScheme(myScheme); err != nil {
+		return nil, err
+	}
+
+	cl, err := client.New(r, client.Options{
+		Scheme: myScheme,
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &operatorUpdater{
+		Client:    cl,
+		namespace: ns,
+	}, nil
+}
+
+func (o operatorUpdater) Update(f File) error {
+	for _, a := range f.Pools {
+		err := o.createPool(a)
+		if err != nil {
+			return err
+		}
+	}
+
+	for _, bp := range f.BFDProfiles {
+		err := o.createBFDProfile(bp)
+		if err != nil {
+			return err
+		}
+	}
+
+	/*
+		Since a peer doesn't have a name we need to clean them before creating.
+		Without this the webhook fails the request because we are trying to create
+		a BGPPeer with the same configuration under a different (generated) name.
+		TODO: is there a better way to handle this?
+	*/
+	err := o.DeleteAllOf(context.Background(), &operatorv1beta1.BGPPeer{}, client.InNamespace(o.namespace))
+	if err != nil {
+		return err
+	}
+
+	for _, p := range f.Peers {
+		err := o.createPeer(p)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (o operatorUpdater) Clean() error {
+	err := o.DeleteAllOf(context.Background(), &operatorv1beta1.AddressPool{}, client.InNamespace(o.namespace))
+	if err != nil {
+		return err
+	}
+
+	err = o.DeleteAllOf(context.Background(), &operatorv1beta1.BFDProfile{}, client.InNamespace(o.namespace))
+	if err != nil {
+		return err
+	}
+
+	err = o.DeleteAllOf(context.Background(), &operatorv1beta1.BGPPeer{}, client.InNamespace(o.namespace))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (o operatorUpdater) createPool(a AddressPool) error {
+	pool := o.poolToOperator(a)
+	existing := &operatorv1beta1.AddressPool{}
+	err := o.Client.Get(context.TODO(), types.NamespacedName{Name: pool.Name, Namespace: pool.Namespace}, existing)
+
+	if err != nil {
+		if apierr.IsNotFound(err) {
+			return o.Client.Create(context.TODO(), pool)
+		}
+		return err
+	}
+
+	pool.ResourceVersion = existing.ResourceVersion
+	return o.Client.Update(context.TODO(), pool)
+}
+
+func (o operatorUpdater) poolToOperator(a AddressPool) *operatorv1beta1.AddressPool {
+	addrs := make([]string, len(a.Addresses))
+	copy(addrs, a.Addresses)
+
+	bgppadvs := make([]operatorv1beta1.BgpAdvertisement, len(a.BGPAdvertisements))
+	for _, b := range a.BGPAdvertisements {
+		adv := operatorv1beta1.BgpAdvertisement{}
+		if b.AggregationLength != nil {
+			al := int32(*b.AggregationLength)
+			adv.AggregationLength = &al
+		}
+		if b.LocalPref != nil {
+			adv.LocalPref = *b.LocalPref
+		}
+		copy(adv.Communities, b.Communities)
+		bgppadvs = append(bgppadvs, adv)
+	}
+
+	return &operatorv1beta1.AddressPool{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      a.Name,
+			Namespace: o.namespace,
+		},
+		Spec: operatorv1beta1.AddressPoolSpec{
+			Protocol:  string(a.Protocol),
+			Addresses: addrs,
+			// AvoidBuggyIPs missing from operator
+			AutoAssign:        a.AutoAssign,
+			BGPAdvertisements: bgppadvs,
+		},
+	}
+}
+
+func (o operatorUpdater) createPeer(p Peer) error {
+	peer, err := o.peerToOperator(p)
+	if err != nil {
+		return err
+	}
+
+	// peer's name is autogenerated, will not hit a name conflict here.
+	return o.Client.Create(context.TODO(), peer)
+}
+
+func (o operatorUpdater) peerToOperator(p Peer) (*operatorv1beta1.BGPPeer, error) {
+	var holdtime time.Duration
+	var err error
+	if p.HoldTime != "" {
+		holdtime, err = time.ParseDuration(p.HoldTime)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	nodeselectors := make([]operatorv1beta1.NodeSelector, len(p.NodeSelectors))
+	for _, ns := range p.NodeSelectors {
+		n := operatorv1beta1.NodeSelector{
+			MatchLabels: ns.MatchLabels,
+		}
+		for _, e := range ns.MatchExpressions {
+			vals := make([]string, len(e.Values))
+			copy(vals, e.Values)
+			expr := operatorv1beta1.MatchExpression{
+				Key:      e.Key,
+				Operator: e.Operator,
+				Values:   vals,
+			}
+			n.MatchExpressions = append(n.MatchExpressions, expr)
+		}
+		nodeselectors = append(nodeselectors, n)
+	}
+
+	return &operatorv1beta1.BGPPeer{
+		ObjectMeta: metav1.ObjectMeta{
+			GenerateName: "testpeer-",
+			Namespace:    o.namespace,
+		},
+		Spec: operatorv1beta1.BGPPeerSpec{
+			MyASN:         p.MyASN,
+			ASN:           p.ASN,
+			Address:       p.Addr,
+			SrcAddress:    p.SrcAddr,
+			Port:          p.Port,
+			HoldTime:      holdtime,
+			RouterID:      p.RouterID,
+			NodeSelectors: nodeselectors,
+			Password:      p.Password,
+			BFDProfile:    p.BFDProfile,
+		},
+	}, nil
+
+}
+
+func (o operatorUpdater) createBFDProfile(bp BfdProfile) error {
+	bfdprofile := o.bfdProfileToOperator(bp)
+	existing := &operatorv1beta1.BFDProfile{}
+	err := o.Client.Get(context.TODO(), types.NamespacedName{Name: bfdprofile.Name, Namespace: bfdprofile.Namespace}, existing)
+
+	if err != nil {
+		if apierr.IsNotFound(err) {
+			return o.Client.Create(context.TODO(), bfdprofile)
+		}
+		return err
+	}
+
+	bfdprofile.ResourceVersion = existing.ResourceVersion
+	return o.Client.Update(context.TODO(), bfdprofile)
+}
+
+func (o operatorUpdater) bfdProfileToOperator(bp BfdProfile) *operatorv1beta1.BFDProfile {
+	return &operatorv1beta1.BFDProfile{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      bp.Name,
+			Namespace: o.namespace,
+		},
+		Spec: operatorv1beta1.BFDProfileSpec{
+			ReceiveInterval:  bp.ReceiveInterval,
+			TransmitInterval: bp.TransmitInterval,
+			DetectMultiplier: bp.DetectMultiplier,
+			EchoInterval:     bp.EchoInterval,
+			EchoMode:         bp.EchoMode,
+			PassiveMode:      bp.PassiveMode,
+			MinimumTTL:       bp.MinimumTTL,
+		},
+	}
+}
diff --git a/e2etest/utils.go b/e2etest/utils.go
index 69151e5a..b24f6cac 100644
--- a/e2etest/utils.go
+++ b/e2etest/utils.go
@@ -7,6 +7,7 @@ import (
 	"net"
 	"os/exec"
 
+	"go.universe.tf/metallb/e2etest/pkg/config"
 	"go.universe.tf/metallb/e2etest/pkg/executor"
 	internalconfig "go.universe.tf/metallb/internal/config"
 	v1 "k8s.io/api/core/v1"
@@ -65,7 +66,7 @@ func tweakRCPort(rc *v1.ReplicationController) {
 	}
 }
 
-func validateIPInRange(addressPools []addressPool, ip string) error {
+func validateIPInRange(addressPools []config.AddressPool, ip string) error {
 	input := net.ParseIP(ip)
 	for _, addressPool := range addressPools {
 		for _, address := range addressPool.Addresses {