Repository: metallb/metallb
Pull request #701 (2020-08-19 18:20:37)

website: Update contacts for code of conduct
Closes: #675 

rata: @russellb good point.

@daxmc99 I'm not sure your full name. I'll be away for holidays starting tomorrow, so feel free to create a new PR and close this one if I don't act. My full name is Rodrigo Campos :)
daxmc99: My Full name is Dax McDonald 😃
johananl: Force-pushed a change to add the full names (I know this is your branch @rata - doing this because of your limited availability).
johananl: Also - fixed a typo.

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 52742cc0..37c211ad 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -3,57 +3,47 @@
 version: 2
 jobs:
   test-1.13:
-    working_directory: /go/src/go.universe.tf/metallb
     docker:
-      - image: circleci/golang:1.13
+      - image: cimg/go:1.13
     steps:
       - checkout
-      - run: sudo apt-get install python-pip
-      - run: sudo pip install invoke semver pyyaml
-      - run: echo "export GO111MODULE=on" >>$BASH_ENV
       - run: go test -short ./...
       - run: go test -short -race ./...
       - run: cp manifests/metallb.yaml manifests/metallb.yaml.prev
   lint-1.13:
-    working_directory: /go/src/go.universe.tf/metallb
     docker:
-      - image: circleci/golang:1.13
+      - image: cimg/go:1.13
     steps:
       - checkout
-      - run: echo "export GO111MODULE=on" >>$BASH_ENV
       - run: curl -L https://git.io/vp6lP | sh # gometalinter
       - run: PATH=./bin:$PATH gometalinter --deadline=5m --disable-all --enable=gofmt --enable=vet --vendor ./...
-  deploy-controller:
-    working_directory: /go/src/go.universe.tf/metallb
+  publish-images:
     docker:
-      - image: circleci/golang:1.13
+      - image: cimg/go:1.13
     steps:
       - checkout
       - setup_remote_docker
-      - run: sudo apt-get install python-pip
-      - run: sudo pip install invoke semver pyyaml
+      - run: sudo apt-get update && sudo apt-get install -y python-pip
+      - run: pip install invoke semver pyyaml
       - run: docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
       - run: mkdir -p ./bin
-      - run: wget -O ./bin/manifest-tool https://github.com/estesp/manifest-tool/releases/download/v1.0.0/manifest-tool-linux-amd64 && chmod +x ./bin/manifest-tool
-      - run: echo "export GO111MODULE=on" >>$BASH_ENV
-      - run: PATH=./bin:$PATH inv push-multiarch --binaries=controller --tag=${CIRCLE_BRANCH:-${CIRCLE_TAG}} --docker-user=metallb
-  deploy-speaker:
-    working_directory: /go/src/go.universe.tf/metallb
+      - run: wget -O ./bin/manifest-tool https://github.com/estesp/manifest-tool/releases/download/v1.0.3/manifest-tool-linux-amd64 && chmod +x ./bin/manifest-tool
+      - run: PATH=./bin:$PATH inv push-multiarch --binaries=controller --binaries=speaker --registry=docker.io --repo=metallb --tag=${CIRCLE_BRANCH:-${CIRCLE_TAG}}
+  publish-images-quay:
     docker:
-      - image: circleci/golang:1.13
+      - image: cimg/go:1.13
     steps:
       - checkout
       - setup_remote_docker
-      - run: sudo apt-get install python-pip
-      - run: sudo pip install invoke semver pyyaml
-      - run: docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
+      - run: sudo apt-get update && sudo apt-get install -y python-pip
+      - run: pip install invoke semver pyyaml
+      - run: docker login quay.io -u $QUAY_USER -p $QUAY_PASSWORD
       - run: mkdir -p ./bin
-      - run: wget -O ./bin/manifest-tool https://github.com/estesp/manifest-tool/releases/download/v1.0.0/manifest-tool-linux-amd64 && chmod +x ./bin/manifest-tool
-      - run: echo "export GO111MODULE=on" >>$BASH_ENV
-      - run: PATH=./bin:$PATH inv push-multiarch --binaries=speaker --tag=${CIRCLE_BRANCH:-${CIRCLE_TAG}} --docker-user=metallb
+      - run: wget -O ./bin/manifest-tool https://github.com/estesp/manifest-tool/releases/download/v1.0.3/manifest-tool-linux-amd64 && chmod +x ./bin/manifest-tool
+      - run: PATH=./bin:$PATH inv push-multiarch --binaries=controller --binaries=speaker --registry=quay.io --repo=metallb --tag=${CIRCLE_BRANCH:-${CIRCLE_TAG}}
 workflows:
   version: 2
-  test-and-deploy:
+  test-and-publish:
     jobs:
       - test-1.13:
           filters:
@@ -63,7 +53,7 @@ workflows:
           filters:
             tags:
               only: /.*/
-      - deploy-controller:
+      - publish-images:
           filters:
             branches:
               only:
@@ -74,7 +64,7 @@ workflows:
           requires:
             - test-1.13
             - lint-1.13
-      - deploy-speaker:
+      - publish-images-quay:
           filters:
             branches:
               only:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index a50bf2c7..4dfbbb5e 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -2,7 +2,7 @@ Thanks for sending a pull request! A few things before we get started:
 
 1. If this is your first time, please read the [contributing guide](https://metallb.universe.tf/community/#contributing)
 2. For non-trivial pull requests, please [file an
-   issue](https://github.com/google/metallb/issues/new) first, and get
+   issue](https://github.com/metallb/metallb/issues/new) first, and get
    agreement that the change is a good idea, and a general guideline
    for how it should be implemented, before sending code. Large PRs
    that weren't first discussed and agreed upon in an issue won't be
diff --git a/CODEOWNERS b/CODEOWNERS
index 3a291852..21b27583 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -2,7 +2,7 @@
 # https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
 
 # The MetalLB maintainers team
-*   @daxmc99 @johananl @rata
+*   @daxmc99 @johananl @rata @russellb
 
 # Emeritus Maintainers
 #
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c765abcd..71836ea4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,3 +2,10 @@
 
 See the [contributing section of the
 website](https://metallb.universe.tf/community) for information.
+
+## Certificate of Origin
+
+By contributing to this project you agree to the Developer Certificate of
+Origin (DCO). This document was created by the Linux Kernel community and is a
+simple statement that you, as a contributor, have the legal right to make the
+contribution. See the [DCO](DCO) file for details.
diff --git a/DCO b/DCO
new file mode 100644
index 00000000..8201f992
--- /dev/null
+++ b/DCO
@@ -0,0 +1,37 @@
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+    have the right to submit it under the open source license
+    indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+    of my knowledge, is covered under an appropriate open source
+    license and I have the right under that license to submit that
+    work with modifications, whether created in whole or in part
+    by me, under the same open source license (unless I am
+    permitted to submit under a different license), as indicated
+    in the file; or
+
+(c) The contribution was provided directly to me by some other
+    person who certified (a), (b) or (c) and I have not modified
+    it.
+
+(d) I understand and agree that this project and the contribution
+    are public and that a record of the contribution (including all
+    personal information I submit with it, including my sign-off) is
+    maintained indefinitely and may be redistributed consistent with
+    this project or the open source license(s) involved.
diff --git a/dev-env/README.md b/dev-env/README.md
new file mode 100644
index 00000000..d9185698
--- /dev/null
+++ b/dev-env/README.md
@@ -0,0 +1,17 @@
+# dev-env
+
+This directory contains supporting files for the included MetalLB development
+environment. The environment is run using the following command:
+
+```
+inv dev-env
+```
+
+There is also support for configuring MetalLB to peer with a BGP router running
+in a container:
+
+```
+inv dev-env --protocol bgp
+```
+
+For more information, see [the BGP dev-env README](bgp/README.md).
diff --git a/dev-env/bgp/.gitignore b/dev-env/bgp/.gitignore
new file mode 100644
index 00000000..d96f1de9
--- /dev/null
+++ b/dev-env/bgp/.gitignore
@@ -0,0 +1,2 @@
+config.yaml
+frr-volume
diff --git a/dev-env/bgp/README.md b/dev-env/bgp/README.md
new file mode 100644
index 00000000..2207a6a3
--- /dev/null
+++ b/dev-env/bgp/README.md
@@ -0,0 +1,95 @@
+# BGP test env
+
+This assumes a 3-node `kind` based test cluster as set up by running `inv
+dev-env -p bgp`, which sets up the development environment with some
+configuration and a BGP router for development and testing purposes.
+
+Along with the cluster, there will be a container named `frr` which is the BGP
+router. Note that this only works in an IPv4 configuration for now.
+
+The configuration used for MetalLB can be found in `config.yaml`. The FRR
+configuration is in the `frr/` directory.
+
+Observe that the BGP speakers have peered with our router:
+
+```
+$ docker exec frr vtysh -c "show ip bgp summary"
+
+IPv4 Unicast Summary:
+BGP router identifier 172.18.0.5, local AS number 64512 vrf-id 0
+BGP table version 1
+RIB entries 1, using 192 bytes of memory
+Peers 3, using 43 KiB of memory
+
+Neighbor        V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt
+172.18.0.2      4      64512        22        21        0    0    0 00:09:47            1        0
+172.18.0.3      4      64512        22        21        0    0    0 00:09:47            1        0
+172.18.0.4      4      64512        22        21        0    0    0 00:09:47            1        0
+
+Total number of neighbors 3
+```
+
+Create an `nginx` Deployment and a corresponding Service of type LoadBalancer:
+
+```
+kubectl apply -f dev-env/testsvc.yaml
+```
+
+Observe the IP address allocated to the Service by MetalLB:
+
+```
+$ kubectl get svc nginx
+NAME    TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)        AGE
+nginx   LoadBalancer   10.106.88.254   192.168.10.0   80:30076/TCP   53m
+```
+
+Ensure a route to this Service has been published by the MetalLB BGP speaker:
+
+```
+$ docker exec frr vtysh -c "show ip bgp detail"
+BGP table version is 1, local router ID is 172.18.0.5, vrf id 0
+Default local pref 100, local AS 64512
+Status codes:  s suppressed, d damped, h history, * valid, > best, = multipath,
+               i internal, r RIB-failure, S Stale, R Removed
+Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
+Origin codes:  i - IGP, e - EGP, ? - incomplete
+
+   Network          Next Hop            Metric LocPrf Weight Path
+*>i192.168.10.0/32  172.18.0.2                      0      0 ?
+*=i                 172.18.0.3                      0      0 ?
+*=i                 172.18.0.4                      0      0 ?
+
+Displayed  1 routes and 3 total paths
+```
+
+Now validate that you can connect to the Service from the router:
+
+```
+$ docker exec -it frr bash
+bash-5.0# echo "GET /" | nc 192.168.10.0 80
+<!DOCTYPE html>
+<html>
+<head>
+<title>Welcome to nginx!</title>
+<style>
+    body {
+        width: 35em;
+        margin: 0 auto;
+        font-family: Tahoma, Verdana, Arial, sans-serif;
+    }
+</style>
+</head>
+<body>
+<h1>Welcome to nginx!</h1>
+<p>If you see this page, the nginx web server is successfully installed and
+working. Further configuration is required.</p>
+
+<p>For online documentation and support please refer to
+<a href="http://nginx.org/">nginx.org</a>.<br/>
+Commercial support is available at
+<a href="http://nginx.com/">nginx.com</a>.</p>
+
+<p><em>Thank you for using nginx.</em></p>
+</body>
+</html>
+```
diff --git a/dev-env/bgp/config.yaml.tmpl b/dev-env/bgp/config.yaml.tmpl
new file mode 100644
index 00000000..9445f3a7
--- /dev/null
+++ b/dev-env/bgp/config.yaml.tmpl
@@ -0,0 +1,18 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  namespace: metallb-system
+  name: config
+data:
+  config: |
+    peers:
+      # NOTE: PEER_ADDRESS will be substituted with the IP address
+      # assigned to the frr container.
+    - peer-address: PEER_ADDRESS
+      peer-asn: 64512
+      my-asn: 64512
+    address-pools:
+    - name: default
+      protocol: bgp
+      addresses:
+      - 192.168.10.0/24
diff --git a/dev-env/bgp/frr/bgpd.conf.tmpl b/dev-env/bgp/frr/bgpd.conf.tmpl
new file mode 100644
index 00000000..0a58b60f
--- /dev/null
+++ b/dev-env/bgp/frr/bgpd.conf.tmpl
@@ -0,0 +1,43 @@
+! -*- bgp -*-
+!
+! BGPd sample configuration file
+!
+! $Id: bgpd.conf.sample,v 1.1 2002/12/13 20:15:29 paul Exp $
+!
+hostname bgpd
+password zebra
+!enable password please-set-at-here
+!
+!
+! NOTE: These NODE_IPs will be substituted with the IP addresses
+! assigned to the kind cluster Nodes if they are different.
+!
+router bgp 64512
+ neighbor NODE0_IP remote-as 64512
+ neighbor NODE0_IP next-hop-self
+ neighbor NODE1_IP remote-as 64512
+ neighbor NODE1_IP next-hop-self
+ neighbor NODE2_IP remote-as 64512
+ neighbor NODE2_IP next-hop-self
+
+!
+!router bgp 7675
+! bgp router-id 10.0.0.1
+! network 10.0.0.0/8
+! neighbor 10.0.0.2 remote-as 7675
+! neighbor 10.0.0.2 ebgp-multihop
+!
+! address-family ipv4 unicast
+!  neighbor 10.0.0.2 route-map set-nexthop out
+!  neighbor 10.0.0.2 next-hop-self
+! exit-address-family
+!
+! access-list all permit any
+!
+!route-map set-nexthop permit 10
+! match ip address all
+! set ip next-hop 10.0.0.1
+!
+!log file /var/log/bgpd.log
+!
+log stdout debugging
diff --git a/dev-env/bgp/frr/daemons b/dev-env/bgp/frr/daemons
new file mode 100644
index 00000000..5e1f5bdc
--- /dev/null
+++ b/dev-env/bgp/frr/daemons
@@ -0,0 +1,80 @@
+# This file tells the frr package which daemons to start.
+#
+# Sample configurations for these daemons can be found in
+# /usr/share/doc/frr/examples/.
+#
+# ATTENTION:
+#
+# When activating a daemon for the first time, a config file, even if it is
+# empty, has to be present *and* be owned by the user and group "frr", else
+# the daemon will not be started by /etc/init.d/frr. The permissions should
+# be u=rw,g=r,o=.
+# When using "vtysh" such a config file is also needed. It should be owned by
+# group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too.
+#
+# The watchfrr and zebra daemons are always started.
+#
+bgpd=yes
+ospfd=no
+ospf6d=no
+ripd=no
+ripngd=no
+isisd=no
+pimd=no
+ldpd=no
+nhrpd=no
+eigrpd=no
+babeld=no
+sharpd=no
+pbrd=no
+bfdd=no
+fabricd=no
+vrrpd=no
+
+#
+# If this option is set the /etc/init.d/frr script automatically loads
+# the config via "vtysh -b" when the servers are started.
+# Check /etc/pam.d/frr if you intend to use "vtysh"!
+#
+vtysh_enable=yes
+zebra_options="  -A 127.0.0.1 -s 90000000"
+bgpd_options="   -A 127.0.0.1"
+ospfd_options="  -A 127.0.0.1"
+ospf6d_options=" -A ::1"
+ripd_options="   -A 127.0.0.1"
+ripngd_options=" -A ::1"
+isisd_options="  -A 127.0.0.1"
+pimd_options="   -A 127.0.0.1"
+ldpd_options="   -A 127.0.0.1"
+nhrpd_options="  -A 127.0.0.1"
+eigrpd_options=" -A 127.0.0.1"
+babeld_options=" -A 127.0.0.1"
+sharpd_options=" -A 127.0.0.1"
+pbrd_options="   -A 127.0.0.1"
+staticd_options="-A 127.0.0.1"
+bfdd_options="   -A 127.0.0.1"
+fabricd_options="-A 127.0.0.1"
+vrrpd_options="  -A 127.0.0.1"
+
+# configuration profile
+#
+#frr_profile="traditional"
+#frr_profile="datacenter"
+
+#
+# This is the maximum number of FD's that will be available.
+# Upon startup this is read by the control files and ulimit
+# is called. Uncomment and use a reasonable value for your
+# setup if you are expecting a large number of peers in
+# say BGP.
+#MAX_FDS=1024
+
+# The list of daemons to watch is automatically generated by the init script.
+#watchfrr_options=""
+
+# for debugging purposes, you can specify a "wrap" command to start instead
+# of starting the daemon directly, e.g. to use valgrind on ospfd:
+#   ospfd_wrap="/usr/bin/valgrind"
+# or you can use "all_wrap" for all daemons, e.g. to use perf record:
+#   all_wrap="/usr/bin/perf record --call-graph -"
+# the normal daemon command is added to this at the end.
diff --git a/dev-env/bgp/frr/zebra.conf b/dev-env/bgp/frr/zebra.conf
new file mode 100644
index 00000000..49c6a01e
--- /dev/null
+++ b/dev-env/bgp/frr/zebra.conf
@@ -0,0 +1,24 @@
+! -*- zebra -*-
+!
+! zebra sample configuration file
+!
+hostname Router
+password zebra
+enable password zebra
+!
+! Interface's description.
+!
+!interface lo
+! description test of desc.
+!
+!interface sit0
+! multicast
+
+!
+! Static default route sample.
+!
+!ip route 0.0.0.0/0 203.181.89.241
+!
+
+!log file zebra.log
+
diff --git a/go.mod b/go.mod
index 0d9c0d66..39bc4852 100644
--- a/go.mod
+++ b/go.mod
@@ -8,13 +8,9 @@ require (
 	github.com/eapache/channels v1.1.0 // indirect
 	github.com/eapache/queue v1.1.0 // indirect
 	github.com/go-kit/kit v0.9.0
-	github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
-	github.com/golang/protobuf v1.3.1
-	github.com/google/go-cmp v0.3.0
-	github.com/googleapis/gnostic v0.2.0 // indirect
+	github.com/golang/protobuf v1.4.3
+	github.com/google/go-cmp v0.5.2
 	github.com/hashicorp/memberlist v0.1.7
-	github.com/imdario/mergo v0.3.6 // indirect
-	github.com/kr/pretty v0.1.0 // indirect
 	github.com/mdlayher/arp v0.0.0-20190313224443-98a83c8a2717
 	github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7
 	github.com/mdlayher/ndp v0.0.0-20190419144644-012988d57f9a
@@ -22,19 +18,13 @@ require (
 	github.com/osrg/gobgp v2.0.0+incompatible
 	github.com/prometheus/client_golang v1.0.0
 	github.com/satori/go.uuid v1.2.0 // indirect
-	github.com/spf13/cobra v0.0.5
-	github.com/spf13/pflag v1.0.3
+	github.com/spf13/viper v1.7.0 // indirect
 	github.com/vishvananda/netlink v1.0.0 // indirect
 	github.com/vishvananda/netns v0.0.0-20190625233234-7109fa855b0f // indirect
-	go.universe.tf/virtuakube v0.0.0-20190708182722-512c11153571
-	golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe
-	golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
-	google.golang.org/grpc v1.22.0 // indirect
-	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
-	gopkg.in/inf.v0 v0.9.1 // indirect
-	gopkg.in/yaml.v2 v2.2.2
-	k8s.io/api v0.0.0-20190620084959-7cf5895f2711
-	k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
-	k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
+	golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
+	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/klog v0.3.1
 )
diff --git a/go.sum b/go.sum
index c4071d8e..1862983d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,113 +1,241 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
+github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
+github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
+github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
+github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
 github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k=
 github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0=
 github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
-github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
-github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+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-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
-github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8=
-github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
-github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
-github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
-github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
-github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
-github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
-github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
-github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
+github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
+github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 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/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=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/memberlist v0.1.7 h1:5WLszGdi8FjebYTCKuX9zaGNl7pD31uv6Bj6657HxqQ=
 github.com/hashicorp/memberlist v0.1.7/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
-github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE=
-github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+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/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
-github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mdlayher/arp v0.0.0-20190313224443-98a83c8a2717 h1:lAsTIRVOYxE/LwrMmSAVfV/HDndBEvHaAEkjbwhqsJ4=
@@ -120,173 +248,396 @@ github.com/mdlayher/ndp v0.0.0-20190419144644-012988d57f9a/go.mod h1:KcNh3mzU4B9
 github.com/mdlayher/raw v0.0.0-20190313224157-43dbcdd7739d/go.mod h1:r1fbeITl2xL/zLbVnNHFyOzQJTgr/3fpf1lJX/cjzR8=
 github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18 h1:zwOa3e/13D6veNIz6zzuqrd3eZEMF0dzD0AQWKcYSs4=
 github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
 github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
-github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+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/gomega v0.0.0-20190113212917-5533ce8a0da3 h1:EooPXg51Tn+xmWPXJUGCnJhJSpeuMlBmfJVcqIRmmv8=
-github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+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/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+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/osrg/gobgp v2.0.0+incompatible h1:91ARQbE1AtO0U4TIxHPJ7wYVZIqduyBwS1+FjlHlmrY=
 github.com/osrg/gobgp v2.0.0+incompatible/go.mod h1:vGVJPLW6JFDD7WA1vJsjB8OKmbbC2TKwHtr90CZS/u4=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
-github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
 github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
-github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
-github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
+github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 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=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/vishvananda/netlink v1.0.0 h1:bqNY2lgheFIu1meHUFSH3d7vG93AFyqg3oGbJCOJgSM=
 github.com/vishvananda/netlink v1.0.0/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
 github.com/vishvananda/netns v0.0.0-20190625233234-7109fa855b0f h1:nBX3nTcmxEtHSERBJaIo1Qa26VwRaopnZmfDQUXsF4I=
 github.com/vishvananda/netns v0.0.0-20190625233234-7109fa855b0f/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
-github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe h1:5kUPFAF52umOUPH12MuNUmyVTseJRNBftDl/KfsvX3I=
 gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe/go.mod h1:P9LSM1KVzrIstFgUaveuwiAm8PK5VTB3yJEU8kqlbrU=
-go.universe.tf/virtuakube v0.0.0-20190708182722-512c11153571 h1:SNPfXM3jBvs3gbtK3Wu9r6rSxLkd7ipGkBDGbiHk0RY=
-go.universe.tf/virtuakube v0.0.0-20190708182722-512c11153571/go.mod h1:+cR3Ho+tlNjpSEukR6sjpfz8hWHdt5JGh0hkFGgTf8A=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+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/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=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
-golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
-golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4 h1:3i7qG/aA9NUAzdnJHfhgxSKSmxbAebomYR5IZgFbC5Y=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+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-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU=
-golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
+golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 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-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=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 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=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw=
-google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+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/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
-gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
+gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-k8s.io/api v0.0.0-20190620084959-7cf5895f2711 h1:BblVYz/wE5WtBsD/Gvu54KyBUTJMflolzc5I2DTvh50=
-k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
-k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719 h1:uV4S5IB5g4Nvi+TBVNf3e9L4wrirlwYJ6w88jUQxTUw=
-k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
-k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g=
-k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/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/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg=
+k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/client-go v0.20.2 h1:uuf+iIAbfnCSw8IGAv/Rg0giM+2bOzHLOsbbrwrdhNQ=
+k8s.io/client-go v0.20.2/go.mod h1:kH5brqWqp7HDxUFKoEgiI4v8G1xzbe9giaCenUWJzgE=
+k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
 k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
-k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30 h1:TRb4wNWoBVrH9plmkp2q86FIDppkbrEXdXlxU3a3BMI=
-k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
-k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4=
-k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
-sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
+k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
+k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw=
+k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+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/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=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/internal/config/config.go b/internal/config/config.go
index f41c4266..866f8229 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -18,6 +18,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"reflect"
 	"strconv"
 	"strings"
 	"time"
@@ -200,6 +201,14 @@ func Parse(bs []byte) (*Config, error) {
 		if err != nil {
 			return nil, fmt.Errorf("parsing peer #%d: %s", i+1, err)
 		}
+		for _, ep := range cfg.Peers {
+			// TODO: Be smarter regarding conflicting peers. For example, two
+			// peers could have a different hold time but they'd still result
+			// in two BGP sessions between the speaker and the remote host.
+			if reflect.DeepEqual(peer, ep) {
+				return nil, fmt.Errorf("peer #%d already exists", i+1)
+			}
+		}
 		cfg.Peers = append(cfg.Peers, peer)
 	}
 
diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go
index 58b186c1..5241593a 100644
--- a/internal/k8s/k8s.go
+++ b/internal/k8s/k8s.go
@@ -1,6 +1,7 @@
 package k8s // import "go.universe.tf/metallb/internal/k8s"
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -259,9 +260,9 @@ func New(cfg *Config) (*Client, error) {
 	return c, nil
 }
 
-// GetPodsIPs get the IPs from all the pods matched by the labels string
-func (c *Client) GetPodsIPs(namespace, labels string) ([]string, error) {
-	pl, err := c.client.CoreV1().Pods(namespace).List(metav1.ListOptions{LabelSelector: labels})
+// PodIPs returns the IPs of all the pods matched by the labels string.
+func (c *Client) PodIPs(namespace, labels string) ([]string, error) {
+	pl, err := c.client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labels})
 	if err != nil {
 		return nil, err
 	}
@@ -334,13 +335,13 @@ func (c *Client) ForceSync() {
 // the updated Service is returned. Note that changes to svc.Status
 // are not propagated, for that you need to call UpdateStatus.
 func (c *Client) Update(svc *v1.Service) (*v1.Service, error) {
-	return c.client.CoreV1().Services(svc.Namespace).Update(svc)
+	return c.client.CoreV1().Services(svc.Namespace).Update(context.TODO(), svc, metav1.UpdateOptions{})
 }
 
 // UpdateStatus writes the protected "status" field of svc back into
 // the Kubernetes cluster.
 func (c *Client) UpdateStatus(svc *v1.Service) error {
-	_, err := c.client.CoreV1().Services(svc.Namespace).UpdateStatus(svc)
+	_, err := c.client.CoreV1().Services(svc.Namespace).UpdateStatus(context.TODO(), svc, metav1.UpdateOptions{})
 	return err
 }
 
diff --git a/internal/layer2/announcer.go b/internal/layer2/announcer.go
index 1460490e..df9e91ec 100644
--- a/internal/layer2/announcer.go
+++ b/internal/layer2/announcer.go
@@ -20,6 +20,10 @@ type Announce struct {
 	ndps     map[int]*ndpResponder
 	ips      map[string]net.IP // svcName -> IP
 	ipRefcnt map[string]int    // ip.String() -> number of uses
+
+	// This channel can block - do not write to it while holding the mutex
+	// to avoid deadlocking.
+	spamCh chan net.IP
 }
 
 // New returns an initialized Announce.
@@ -30,8 +34,10 @@ func New(l log.Logger) (*Announce, error) {
 		ndps:     map[int]*ndpResponder{},
 		ips:      map[string]net.IP{},
 		ipRefcnt: map[string]int{},
+		spamCh:   make(chan net.IP, 1024),
 	}
 	go ret.interfaceScan()
+	go ret.spamLoop()
 
 	return ret, nil
 }
@@ -131,24 +137,52 @@ func (a *Announce) updateInterfaces() {
 	return
 }
 
-func (a *Announce) spam(name string) {
-	// TODO: should abort if we lose control of the IP mid-spam.
-	start := time.Now()
-	for time.Since(start) < 5*time.Second {
-		if err := a.gratuitous(name); err != nil {
-			a.logger.Log("op", "gratuitousAnnounce", "error", err, "service", name, "msg", "failed to make gratuitous IP announcement")
+func (a *Announce) spamLoop() {
+	// Map IP to spam stop time.
+	m := map[string]time.Time{}
+	// See https://github.com/metallb/metallb/issues/172 for the 1100 choice.
+	ticker := time.NewTicker(1100 * time.Millisecond)
+	for {
+		select {
+		case ip := <-a.spamCh:
+			ipStr := ip.String()
+			_, ok := m[ipStr]
+			// Set spam stop time to 5 seconds from now.
+			m[ipStr] = time.Now().Add(5 * time.Second)
+			if !ok {
+				// Spam right away to avoid waiting up to 1100 milliseconds even if
+				// it means we call spam() twice in a row in a short amount of time.
+				a.spam(ip)
+			}
+		case now := <-ticker.C:
+			for ipStr, until := range m {
+				if now.After(until) {
+					// We have spammed enough - remove the IP from the map.
+					delete(m, ipStr)
+				} else {
+					a.spam(net.ParseIP(ipStr))
+				}
+			}
 		}
-		time.Sleep(1100 * time.Millisecond)
 	}
 }
 
-func (a *Announce) gratuitous(name string) error {
-	a.Lock()
-	defer a.Unlock()
+func (a *Announce) doSpam(ip net.IP) {
+	a.spamCh <- ip
+}
 
-	ip, ok := a.ips[name]
-	if !ok {
-		// No IP means we've lost control of the IP, someone else is
+func (a *Announce) spam(ip net.IP) {
+	if err := a.gratuitous(ip); err != nil {
+		a.logger.Log("op", "gratuitousAnnounce", "error", err, "ip", ip, "msg", "failed to make gratuitous IP announcement")
+	}
+}
+
+func (a *Announce) gratuitous(ip net.IP) error {
+	a.RLock()
+	defer a.RUnlock()
+
+	if a.ipRefcnt[ip.String()] <= 0 {
+		// We've lost control of the IP, someone else is
 		// doing announcements.
 		return nil
 	}
@@ -181,6 +215,8 @@ func (a *Announce) shouldAnnounce(ip net.IP) dropReason {
 
 // SetBalancer adds ip to the set of announced addresses.
 func (a *Announce) SetBalancer(name string, ip net.IP) {
+	// Call doSpam at the end of the function without holding the lock
+	defer a.doSpam(ip)
 	a.Lock()
 	defer a.Unlock()
 
@@ -203,9 +239,6 @@ func (a *Announce) SetBalancer(name string, ip net.IP) {
 			a.logger.Log("op", "watchMulticastGroup", "error", err, "ip", ip, "msg", "failed to watch NDP multicast group for IP, NDP responder will not respond to requests for this address")
 		}
 	}
-
-	go a.spam(name)
-
 }
 
 // DeleteBalancer deletes an address from the set of addresses we should announce.
diff --git a/internal/speakerlist/speakerlist.go b/internal/speakerlist/speakerlist.go
new file mode 100644
index 00000000..7d60ad01
--- /dev/null
+++ b/internal/speakerlist/speakerlist.go
@@ -0,0 +1,319 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package speakerlist
+
+import (
+	"crypto/sha256"
+	golog "log"
+	"strconv"
+	"sync"
+	"time"
+
+	"go.universe.tf/metallb/internal/k8s"
+
+	gokitlog "github.com/go-kit/kit/log"
+	"github.com/hashicorp/memberlist"
+)
+
+// SpeakerList represents a list of healthy speakers.
+type SpeakerList struct {
+	l         gokitlog.Logger
+	client    *k8s.Client
+	stopCh    chan struct{}
+	namespace string
+	labels    string
+
+	// The following fields are nil when memberlist is disabled.
+	mlEventCh chan memberlist.NodeEvent
+	ml        *memberlist.Memberlist
+	mlJoinCh  chan struct{}
+
+	mlMux        sync.Mutex // Mutex for mlSpeakerIPs.
+	mlSpeakerIPs []string   // Speaker pod IPs.
+}
+
+// New creates a new SpeakerList and returns a pointer to it.
+func New(logger gokitlog.Logger, nodeName, bindAddr, bindPort, secret, namespace, labels string, stopCh chan struct{}) (*SpeakerList, error) {
+	sl := SpeakerList{
+		l:         logger,
+		stopCh:    stopCh,
+		namespace: namespace,
+		labels:    labels,
+	}
+
+	if namespace == "" || labels == "" || bindAddr == "" {
+		logger.Log("op", "startup", "msg", "not starting fast dead node detection (memberlist), need ml-bindaddr / ml-labels / ml-namespace config")
+		return &sl, nil
+	}
+
+	mconfig := memberlist.DefaultLANConfig()
+
+	// mconfig.Name MUST be equal to the spec.nodeName field of the speaker pod as we match it
+	// against the nodeName field of Endpoint objects inside usableNodes().
+	mconfig.Name = nodeName
+	mconfig.BindAddr = bindAddr
+	if bindPort != "" {
+		mlport, err := strconv.Atoi(bindPort)
+		if err != nil {
+			logger.Log("op", "startup", "error", "unable to parse ml-bindport", "msg", err)
+			return nil, err
+		}
+		mconfig.BindPort = mlport
+		mconfig.AdvertisePort = mlport
+	}
+	loggerout := gokitlog.NewStdlibAdapter(gokitlog.With(sl.l, "component", "Memberlist"))
+	mconfig.Logger = golog.New(loggerout, "", golog.Lshortfile)
+	if secret == "" {
+		logger.Log("op", "startup", "warning", "no ml-secret-key set, memberlist traffic will not be encrypted")
+	} else {
+		sha := sha256.New()
+		mconfig.SecretKey = sha.Sum([]byte(secret))[:16]
+	}
+
+	// This channel is used by the Rejoin() method which runs on k8s node
+	// changes. A buffered channel is used to avoid blocking the main goroutine
+	// while waiting for the join operation to complete.
+	sl.mlJoinCh = make(chan struct{}, 1)
+
+	// ChannelEventDelegate hints that it should not block, so make mlEventCh
+	// 'big'.
+	// TODO: See https://github.com/metallb/metallb/issues/716
+	sl.mlEventCh = make(chan memberlist.NodeEvent, 1024)
+	mconfig.Events = &memberlist.ChannelEventDelegate{Ch: sl.mlEventCh}
+
+	ml, err := memberlist.Create(mconfig)
+	if err != nil {
+		logger.Log("op", "startup", "error", err, "msg", "failed to create memberlist")
+		return nil, err
+	}
+
+	sl.ml = ml
+
+	return &sl, nil
+}
+
+// Start initializes the SpeakerList. This functions must be called before using
+// other SpeakerList methods.
+func (sl *SpeakerList) Start(client *k8s.Client) {
+	// TODO: The k8s client parameter should ideally be a parameter to
+	// New(). However, that is not possible today because:
+	//
+	// 1. The newController() function takes the sList as param[1]
+	// 2. The newController() function uses the sList param to create the layer 2 controller[2]
+	// 3. The new Kubernetes client uses the created controller to set the callbacks[3]
+	//
+	// Then, we have a dependency cycle if we add the Kubernetes client to speakerlist.New():
+	//
+	// 1. To create a Kubernetes client we need a controller
+	// 2. The newController() function uses the sList param to create the layer 2 controller[2]
+	// 3. The new Kubernetes client uses the created controller to set the callbacks[3]
+	//
+	// We should probably move the speakerlist code to be an implementation
+	// detail of the Layer 2 controller and, when doing so, try to fix this
+	// problem.
+	//
+	// [1]: https://github.com/metallb/metallb/pull/662/files#diff-60053ad6fecb5a3cfabb6f3d9e720899R109
+	// [2]: https://github.com/metallb/metallb/pull/662/files#diff-60053ad6fecb5a3cfabb6f3d9e720899L232-L251
+	// [3]: https://github.com/metallb/metallb/pull/662/files#diff-60053ad6fecb5a3cfabb6f3d9e720899L160-L162
+
+	sl.client = client
+
+	if sl.ml == nil {
+		return
+	}
+
+	// Initialize sl.mlSpeakerIPs.
+	iplist, err := sl.mlSpeakers()
+	if err != nil {
+		sl.l.Log("op", "memberDiscovery", "error", err, "msg", "failed to get pod IPs")
+		iplist = nil
+	}
+
+	sl.mlMux.Lock()
+	sl.mlSpeakerIPs = iplist
+	sl.mlMux.Unlock()
+
+	// Update mlSpeakerIPs in the background.
+	go sl.updateSpeakerIPs()
+
+	go sl.memberlistWatchEvents()
+	go sl.joinMembers()
+}
+
+// updateSpeakerIPs runs forever updating the sl.mlSpeakerIPs slice with the
+// IPs of all the speaker pods. As the function queries the API server, it waits at
+// least 5 minutes between queries to avoid self-induced API server DoS (should
+// be treated with care).
+func (sl *SpeakerList) updateSpeakerIPs() {
+	for {
+		// This either stops the loop or makes sure to wait at least 5
+		// minutes before making another call to sl.mlSpeakers(), which
+		// queries the API server.
+		select {
+		case <-sl.stopCh:
+			return
+		case <-time.After(5 * time.Minute):
+			// This blocks until the API server responds.
+			iplist, err := sl.mlSpeakers()
+			if err != nil {
+				sl.l.Log("op", "memberDiscovery", "error", err, "msg", "failed to get pod IPs")
+				continue
+			}
+
+			sl.mlMux.Lock()
+			sl.mlSpeakerIPs = iplist
+			sl.mlMux.Unlock()
+		}
+	}
+}
+
+func (sl *SpeakerList) mlSpeakers() ([]string, error) {
+	// This call blocks until we get a response from the API server.
+	// In the client-go version we are using, there is no way to use a
+	// context. Newer versions of client-go support using a context
+	// in the call sl.client.PodIPs() is using.
+	//
+	// TODO: When updating client-go, this code can be simplified by using a
+	// context in the following way:
+	// If we use a context, we can specify a timeout and then there is no need to
+	// run mlUpdateSpeaker() in the background. We could then call
+	// this function from mlJoin() with a context (timeout) and use
+	// sl.mlSpeakerIPs on failure. On success, we could update
+	// sl.mlSpeakerIPs with the response and continue to do the join.
+	iplist, err := sl.client.PodIPs(sl.namespace, sl.labels)
+	if err != nil {
+		return nil, err
+	}
+
+	return iplist, nil
+}
+
+func (sl *SpeakerList) joinMembers() {
+	// Every one minute, try to rejoin.
+	// This joins nodes that leave the cluster for just a few seconds.
+	// Discovering new IPs (updated by sl.updateSpeakerIPs()) might take a while
+	// longer.
+	ticker := time.NewTicker(1 * time.Minute)
+
+	for {
+		select {
+		case <-sl.stopCh:
+			return
+		case <-ticker.C:
+			sl.mlJoin()
+		case <-sl.mlJoinCh:
+			sl.mlJoin()
+		}
+	}
+}
+
+func (sl *SpeakerList) members() map[string]struct{} {
+	members := make(map[string]struct{})
+
+	for _, node := range sl.ml.Members() {
+		ip := node.Addr.String()
+		members[ip] = struct{}{}
+	}
+
+	return members
+}
+
+// mlJoin joins speaker pods that are not members of this cluster
+// to the cluster. It performs a memberlist.Join() with the IPs in
+// mlSpeakerIPs that are not members of the cluster.
+func (sl *SpeakerList) mlJoin() {
+	// IPs to be joined to the memberlist cluster.
+	var joinIPs []string
+
+	members := sl.members()
+
+	sl.mlMux.Lock()
+	defer sl.mlMux.Unlock()
+
+	for _, ip := range sl.mlSpeakerIPs {
+		// If an IP is not a member of the cluster, add it to joinIPs.
+		if _, isMember := members[ip]; !isMember {
+			joinIPs = append(joinIPs, ip)
+		}
+	}
+
+	if len(joinIPs) == 0 {
+		// Not logging here to avoid spamming the logs.
+		return
+	}
+
+	nr, err := sl.ml.Join(joinIPs)
+	if err != nil || nr != len(joinIPs) {
+		sl.l.Log("op", "memberDiscovery", "msg", "partial join", "joined", nr, "expected", len(joinIPs), "error", err)
+	} else {
+		sl.l.Log("op", "Member detection", "msg", "memberlist join succesfully", "number of other nodes", nr)
+	}
+}
+
+// Rejoin initiates a discovery and joins all the speakers to the memberlist
+// cluster.
+func (sl *SpeakerList) Rejoin() {
+	if sl.ml == nil {
+		return
+	}
+
+	select {
+	case sl.mlJoinCh <- struct{}{}:
+		sl.l.Log("op", "memberDiscovery", "msg", "triggering discovery")
+	default:
+		sl.l.Log("op", "memberDiscovery", "msg", "previous discovery in progress - doing nothing")
+	}
+
+	return
+}
+
+// UsableSpeakers returns a map of usable speaker nodes.
+func (sl *SpeakerList) UsableSpeakers() map[string]bool {
+	if sl.ml == nil {
+		return nil
+	}
+	activeNodes := map[string]bool{}
+	for _, n := range sl.ml.Members() {
+		activeNodes[n.Name] = true
+	}
+	return activeNodes
+}
+
+// Stop stops the SpeakerList.
+func (sl *SpeakerList) Stop() {
+	if sl.ml == nil {
+		return
+	}
+
+	sl.l.Log("op", "shutdown", "msg", "leaving memberlist cluster")
+	err := sl.ml.Leave(time.Second)
+	sl.l.Log("op", "shutdown", "msg", "left memberlist cluster", "error", err)
+	err = sl.ml.Shutdown()
+	sl.l.Log("op", "shutdown", "msg", "memberlist shutdown", "error", err)
+}
+
+func event2String(e memberlist.NodeEventType) string {
+	return []string{"NodeJoin", "NodeLeave", "NodeUpdate"}[e]
+}
+
+func (sl *SpeakerList) memberlistWatchEvents() {
+	for {
+		select {
+		case e := <-sl.mlEventCh:
+			sl.l.Log("msg", "node event - forcing sync", "node addr", e.Node.Addr, "node name", e.Node.Name, "node event", event2String(e.Event))
+			sl.client.ForceSync()
+		case <-sl.stopCh:
+			return
+		}
+	}
+}
diff --git a/manifests/metallb.yaml b/manifests/metallb.yaml
index 7742e8c6..0186e498 100644
--- a/manifests/metallb.yaml
+++ b/manifests/metallb.yaml
@@ -50,9 +50,7 @@ metadata:
 spec:
   allowPrivilegeEscalation: false
   allowedCapabilities:
-  - NET_ADMIN
   - NET_RAW
-  - SYS_ADMIN
   allowedHostPaths: []
   defaultAddCapabilities: []
   defaultAllowPrivilegeEscalation: false
@@ -64,6 +62,8 @@ spec:
   hostPorts:
   - max: 7472
     min: 7472
+  - max: 7946
+    min: 7946
   privileged: true
   readOnlyRootFilesystem: true
   requiredDropCapabilities:
@@ -300,6 +300,8 @@ spec:
             fieldRef:
               fieldPath: status.podIP
         # needed when another software is also using memberlist / port 7946
+        # when changing this default you also need to update the container ports definition
+        # and the PodSecurityPolicy hostPorts definition
         #- name: METALLB_ML_BIND_PORT
         #  value: "7946"
         - name: METALLB_ML_LABELS
@@ -313,12 +315,16 @@ spec:
             secretKeyRef:
               name: memberlist
               key: secretkey
-        image: metallb/speaker:main
-        imagePullPolicy: Always
+        image: quay.io/metallb/speaker:main
         name: speaker
         ports:
         - containerPort: 7472
           name: monitoring
+        - containerPort: 7946
+          name: memberlist-tcp
+        - containerPort: 7946
+          name: memberlist-udp
+          protocol: UDP
         resources:
           limits:
             cpu: 100m
@@ -327,20 +333,19 @@ spec:
           allowPrivilegeEscalation: false
           capabilities:
             add:
-            - NET_ADMIN
             - NET_RAW
-            - SYS_ADMIN
             drop:
             - ALL
           readOnlyRootFilesystem: true
       hostNetwork: true
       nodeSelector:
-        beta.kubernetes.io/os: linux
+        kubernetes.io/os: linux
       serviceAccountName: speaker
       terminationGracePeriodSeconds: 2
       tolerations:
       - effect: NoSchedule
         key: node-role.kubernetes.io/master
+        operator: Exists
 ---
 apiVersion: apps/v1
 kind: Deployment
@@ -369,8 +374,7 @@ spec:
       - args:
         - --port=7472
         - --config=config
-        image: metallb/controller:main
-        imagePullPolicy: Always
+        image: quay.io/metallb/controller:main
         name: controller
         ports:
         - containerPort: 7472
@@ -386,7 +390,7 @@ spec:
             - all
           readOnlyRootFilesystem: true
       nodeSelector:
-        beta.kubernetes.io/os: linux
+        kubernetes.io/os: linux
       securityContext:
         runAsNonRoot: true
         runAsUser: 65534
diff --git a/speaker/layer2_controller.go b/speaker/layer2_controller.go
index 2209b563..83e3e101 100644
--- a/speaker/layer2_controller.go
+++ b/speaker/layer2_controller.go
@@ -21,7 +21,6 @@ import (
 	"sort"
 
 	"github.com/go-kit/kit/log"
-	"github.com/hashicorp/memberlist"
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/layer2"
 	"k8s.io/api/core/v1"
@@ -30,7 +29,7 @@ import (
 type layer2Controller struct {
 	announcer *layer2.Announce
 	myNode    string
-	mList     *memberlist.Memberlist
+	sList     SpeakerList
 }
 
 func (c *layer2Controller) SetConfig(log.Logger, *config.Config) error {
@@ -39,23 +38,18 @@ func (c *layer2Controller) SetConfig(log.Logger, *config.Config) error {
 
 // usableNodes returns all nodes that have at least one fully ready
 // endpoint on them.
-func usableNodes(eps *v1.Endpoints, mList *memberlist.Memberlist) []string {
-	var activeNodes map[string]bool
-	if mList != nil {
-		activeNodes = map[string]bool{}
-		for _, n := range mList.Members() {
-			activeNodes[n.Name] = true
-		}
-	}
-
+// The speakers parameter is a map with the node name as key and the readiness
+// status as value (true means ready, false means not ready).
+// If the speakers map is nil, it is ignored.
+func usableNodes(eps *v1.Endpoints, speakers map[string]bool) []string {
 	usable := map[string]bool{}
 	for _, subset := range eps.Subsets {
 		for _, ep := range subset.Addresses {
 			if ep.NodeName == nil {
 				continue
 			}
-			if activeNodes != nil {
-				if _, ok := activeNodes[*ep.NodeName]; !ok {
+			if speakers != nil {
+				if ready, ok := speakers[*ep.NodeName]; !ok || !ready {
 					continue
 				}
 			}
@@ -76,7 +70,7 @@ func usableNodes(eps *v1.Endpoints, mList *memberlist.Memberlist) []string {
 }
 
 func (c *layer2Controller) ShouldAnnounce(l log.Logger, name string, svc *v1.Service, eps *v1.Endpoints) string {
-	nodes := usableNodes(eps, c.mList)
+	nodes := usableNodes(eps, c.sList.UsableSpeakers())
 	// Sort the slice by the hash of node + service name. This
 	// produces an ordering of ready nodes that is unique to this
 	// service.
@@ -110,5 +104,6 @@ func (c *layer2Controller) DeleteBalancer(l log.Logger, name, reason string) err
 }
 
 func (c *layer2Controller) SetNode(log.Logger, *v1.Node) error {
+	c.sList.Rejoin()
 	return nil
 }
diff --git a/speaker/main.go b/speaker/main.go
index 4f1089aa..549e275e 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -15,27 +15,23 @@
 package main
 
 import (
-	"crypto/sha256"
 	"flag"
 	"fmt"
-	golog "log"
 	"net"
 	"os"
 	"os/signal"
-	"strconv"
 	"syscall"
-	"time"
 
 	"go.universe.tf/metallb/internal/bgp"
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/k8s"
 	"go.universe.tf/metallb/internal/layer2"
 	"go.universe.tf/metallb/internal/logging"
+	"go.universe.tf/metallb/internal/speakerlist"
 	"go.universe.tf/metallb/internal/version"
 	v1 "k8s.io/api/core/v1"
 
 	gokitlog "github.com/go-kit/kit/log"
-	"github.com/hashicorp/memberlist"
 	"github.com/prometheus/client_golang/prometheus"
 )
 
@@ -101,44 +97,16 @@ func main() {
 	}()
 	defer logger.Log("op", "shutdown", "msg", "done")
 
-	var mlist *memberlist.Memberlist
-	var eventCh chan memberlist.NodeEvent
-	if *mlNamespace == "" || *mlLabels == "" || *mlBindAddr == "" {
-		logger.Log("op", "startup", "msg", "Not starting fast dead node detection (MemberList), need ml-bindaddr / ml-labels / ml-namespace config")
-	} else {
-		mconfig := memberlist.DefaultLANConfig()
-		// mconfig.Name MUST be spec.nodeName, as we will match it against Enpoints nodeName in usableNodes()
-		mconfig.Name = *myNode
-		mconfig.BindAddr = *mlBindAddr
-		if *mlBindPort != "" {
-			mlport, err := strconv.Atoi(*mlBindPort)
-			if err != nil {
-				logger.Log("op", "startup", "error", "unable to parse ml-bindport", "msg", err)
-				os.Exit(1)
-			}
-			mconfig.BindPort = mlport
-			mconfig.AdvertisePort = mlport
-		}
-		loggerout := gokitlog.NewStdlibAdapter(gokitlog.With(logger, "component", "MemberList"))
-		mconfig.Logger = golog.New(loggerout, "", golog.Lshortfile)
-		if *mlSecret != "" {
-			sha := sha256.New()
-			mconfig.SecretKey = sha.Sum([]byte(*mlSecret))[:16]
-		}
-		eventCh = make(chan memberlist.NodeEvent, 16)
-		mconfig.Events = &memberlist.ChannelEventDelegate{Ch: eventCh}
-		mlist, err = memberlist.Create(mconfig)
-		if err != nil {
-			logger.Log("op", "startup", "error", err, "msg", "failed to create memberlist")
-			os.Exit(1)
-		}
+	sList, err := speakerlist.New(logger, *myNode, *mlBindAddr, *mlBindPort, *mlSecret, *mlNamespace, *mlLabels, stopCh)
+	if err != nil {
+		os.Exit(1)
 	}
 
 	// Setup all clients and speakers, config decides what is being done runtime.
 	ctrl, err := newController(controllerConfig{
 		MyNode: *myNode,
 		Logger: logger,
-		MList:  mlist,
+		SList:  sList,
 	})
 	if err != nil {
 		logger.Log("op", "startup", "error", err, "msg", "failed to create MetalLB controller")
@@ -167,47 +135,14 @@ func main() {
 	}
 	ctrl.client = client
 
-	if mlist != nil {
-		go watchMemberListEvents(logger, eventCh, stopCh, client)
-
-		iplist, err := client.GetPodsIPs(*mlNamespace, *mlLabels)
-		if err != nil {
-			logger.Log("op", "startup", "error", err, "msg", "failed to get PodsIPs")
-			os.Exit(1)
-		}
-		n, err := mlist.Join(iplist)
-		logger.Log("op", "startup", "msg", "Memberlist join", "nb joigned", n, "error ?", err)
-		defer func() {
-			logger.Log("op", "shutdown", "msg", "leaving MemberList cluster")
-			err = mlist.Leave(time.Second)
-			logger.Log("op", "shutdown", "msg", "left MemberList cluster", "error ?", err)
-			mlist.Shutdown()
-			logger.Log("op", "shutdown", "msg", "MemberList shutdown", "error ?", err)
-		}()
-	}
+	sList.Start(client)
+	defer sList.Stop()
 
 	if err := client.Run(stopCh); err != nil {
 		logger.Log("op", "startup", "error", err, "msg", "failed to run k8s client")
 	}
 }
 
-func event2String(e memberlist.NodeEventType) string {
-	return [...]string{"NodeJoin", "NodeLeave", "NodeUpdate"}[e]
-}
-
-func watchMemberListEvents(logger gokitlog.Logger, eventCh chan memberlist.NodeEvent, stopCh chan struct{}, client *k8s.Client) {
-	for {
-		select {
-		case e := <-eventCh:
-			logger.Log("msg", "Node event", "node addr", e.Node.Addr, "node name", e.Node.Name, "node event", event2String(e.Event))
-			logger.Log("msg", "Call Force Sync")
-			client.ForceSync()
-		case <-stopCh:
-			return
-		}
-	}
-}
-
 type controller struct {
 	myNode string
 
@@ -222,10 +157,10 @@ type controller struct {
 type controllerConfig struct {
 	MyNode string
 	Logger gokitlog.Logger
-	MList  *memberlist.Memberlist
+	SList  SpeakerList
 
 	// For testing only, and will be removed in a future release.
-	// See: https://github.com/google/metallb/issues/152.
+	// See: https://github.com/metallb/metallb/issues/152.
 	DisableLayer2 bool
 }
 
@@ -246,7 +181,7 @@ func newController(cfg controllerConfig) (*controller, error) {
 		protocols[config.Layer2] = &layer2Controller{
 			announcer: a,
 			myNode:    cfg.MyNode,
-			mList:     cfg.MList,
+			sList:     cfg.SList,
 		}
 	}
 
@@ -429,3 +364,9 @@ type Protocol interface {
 	DeleteBalancer(gokitlog.Logger, string, string) error
 	SetNode(gokitlog.Logger, *v1.Node) error
 }
+
+// Speakerlist represents a list of healthy speakers.
+type SpeakerList interface {
+	UsableSpeakers() map[string]bool
+	Rejoin()
+}
diff --git a/tasks.py b/tasks.py
index 002255af..9f92c857 100644
--- a/tasks.py
+++ b/tasks.py
@@ -1,5 +1,6 @@
 import os
 import semver
+import re
 import shutil
 import sys
 import yaml
@@ -63,15 +64,16 @@ def _make_build_dirs():
       help={
           "binaries": "binaries to build. One or more of {}, or 'all'".format(", ".join(sorted(all_binaries))),
           "architectures": "architectures to build. One or more of {}, or 'all'".format(", ".join(sorted(all_architectures))),
-          "tag": "docker image tag prefix to use. Actual tag will be <tag>-<arch>. Default 'dev'.",
-          "docker-user": "docker user under which to tag the images. Default 'metallb'.",
+          "registry": "Docker registry under which to tag the images. Default 'quay.io'.",
+          "repo": "Docker repository under which to tag the images. Default 'metallb'.",
+          "tag": "Docker image tag prefix to use. Actual tag will be <tag>-<arch>. Default 'dev'.",
       })
-def build(ctx, binaries, architectures, tag="dev", docker_user="metallb"):
+def build(ctx, binaries, architectures, registry="quay.io", repo="metallb", tag="dev"):
     """Build MetalLB docker images."""
     binaries = _check_binaries(binaries)
     architectures = _check_architectures(architectures)
     _make_build_dirs()
-    
+
     commit = run("git describe --dirty --always", hide=True).stdout.strip()
     branch = run("git rev-parse --abbrev-ref HEAD", hide=True).stdout.strip()
 
@@ -95,9 +97,10 @@ def build(ctx, binaries, architectures, tag="dev", docker_user="metallb"):
                 env=env,
                 echo=True)
             run("docker build "
-                "-t {user}/{bin}:{tag}-{arch} "
+                "-t {registry}/{repo}/{bin}:{tag}-{arch} "
                 "-f {bin}/Dockerfile build/{arch}/{bin}".format(
-                    user=docker_user,
+                    registry=registry,
+                    repo=repo,
                     bin=bin,
                     tag=tag,
                     arch=arch),
@@ -107,19 +110,21 @@ def build(ctx, binaries, architectures, tag="dev", docker_user="metallb"):
       help={
           "binaries": "binaries to build. One or more of {}, or 'all'".format(", ".join(sorted(all_binaries))),
           "architectures": "architectures to build. One or more of {}, or 'all'".format(", ".join(sorted(all_architectures))),
-          "tag": "docker image tag prefix to use. Actual tag will be <tag>-<arch>. Default 'dev'.",
-          "docker-user": "docker user under which to tag the images. Default 'metallb'.",
+          "registry": "Docker registry under which to tag the images. Default 'quay.io'.",
+          "repo": "Docker repository under which to tag the images. Default 'metallb'.",
+          "tag": "Docker image tag prefix to use. Actual tag will be <tag>-<arch>. Default 'dev'.",
       })
-def push(ctx, binaries, architectures, tag="dev", docker_user="metallb"):
+def push(ctx, binaries, architectures, registry="quay.io", repo="metallb", tag="dev"):
     """Build and push docker images to registry."""
     binaries = _check_binaries(binaries)
     architectures = _check_architectures(architectures)
 
     for arch in architectures:
         for bin in binaries:
-            build(ctx, binaries=[bin], architectures=[arch], tag=tag, docker_user=docker_user)
-            run("docker push {user}/{bin}:{tag}-{arch}".format(
-                user=docker_user,
+            build(ctx, binaries=[bin], architectures=[arch], registry=registry, repo=repo, tag=tag)
+            run("docker push {registry}/{repo}/{bin}:{tag}-{arch}".format(
+                registry=registry,
+                repo=repo,
                 bin=bin,
                 arch=arch,
                 tag=tag),
@@ -128,32 +133,56 @@ def push(ctx, binaries, architectures, tag="dev", docker_user="metallb"):
 @task(iterable=["binaries"],
       help={
           "binaries": "binaries to build. One or more of {}, or 'all'".format(", ".join(sorted(all_binaries))),
-          "tag": "docker image tag prefix to use. Actual tag will be <tag>-<arch>. Default 'dev'.",
-          "docker-user": "docker user under which to tag the images. Default 'metallb'.",
+          "registry": "Docker registry under which to tag the images. Default 'quay.io'.",
+          "repo": "Docker repository under which to tag the images. Default 'metallb'.",
+          "tag": "Docker image tag prefix to use. Actual tag will be <tag>-<arch>. Default 'dev'.",
       })
-def push_multiarch(ctx, binaries, tag="dev", docker_user="metallb"):
+def push_multiarch(ctx, binaries, registry="quay.io", repo="metallb", tag="dev"):
     """Build and push multi-architecture docker images to registry."""
     binaries = _check_binaries(binaries)
     architectures = _check_architectures(["all"])
-    push(ctx, binaries=binaries, architectures=architectures, tag=tag, docker_user=docker_user)
-    
+    push(ctx, binaries=binaries, architectures=architectures, registry=registry, repo=repo, tag=tag)
+
     platforms = ",".join("linux/{}".format(arch) for arch in architectures)
     for bin in binaries:
         run("manifest-tool push from-args "
             "--platforms {platforms} "
-            "--template {user}/{bin}:{tag}-ARCH "
-            "--target {user}/{bin}:{tag}".format(
+            "--template {registry}/{repo}/{bin}:{tag}-ARCH "
+            "--target {registry}/{repo}/{bin}:{tag}".format(
                 platforms=platforms,
-                user=docker_user,
+                registry=registry,
+                repo=repo,
                 bin=bin,
                 tag=tag),
             echo=True)
 
+
+def validate_kind_version():
+    """Validate minimum required version of kind."""
+    # If kind is not installed, this first command will raise an UnexpectedExit
+    # exception, and inv will exit at this point making it clear running "kind"
+    # failed.
+    min_version = "0.9.0"
+
+    try:
+        raw = run("kind version", echo=True)
+    except Exception as e:
+        raise Exit(message="Could not determine kind version (is kind installed?)")
+
+    actual_version = re.search("v(\d*\.\d*\.\d*)", raw.stdout).group(1)
+    delta = semver.compare(actual_version, min_version)
+
+    if delta < 0:
+        raise Exit(message="kind version >= {} required".format(min_version))
+
+
 @task(help={
     "architecture": "CPU architecture of the local machine. Default 'amd64'.",
     "name": "name of the kind cluster to use.",
+    "protocol": "Pre-configure MetalLB with the specified protocol. "
+                "Unconfigured by default. Supported: 'bgp'",
 })
-def dev_env(ctx, architecture="amd64", name="kind", cni=None):
+def dev_env(ctx, architecture="amd64", name="kind", cni=None, protocol=None):
     """Build and run MetalLB in a local Kind cluster.
 
     If the cluster specified by --name (default "kind") doesn't exist,
@@ -161,11 +190,14 @@ def dev_env(ctx, architecture="amd64", name="kind", cni=None):
     checkout, push them into kind, and deploy manifests/metallb.yaml
     to run those images.
     """
+
+    validate_kind_version()
+
     clusters = run("kind get clusters", hide=True).stdout.strip().splitlines()
     mk_cluster = name not in clusters
     if mk_cluster:
         config = {
-            "apiVersion": "kind.sigs.k8s.io/v1alpha3",
+            "apiVersion": "kind.x-k8s.io/v1alpha4",
             "kind": "Cluster",
             "nodes": [{"role": "control-plane"},
                       {"role": "worker"},
@@ -186,9 +218,9 @@ def dev_env(ctx, architecture="amd64", name="kind", cni=None):
         run("kubectl apply -f e2etest/manifests/{}.yaml".format(cni), echo=True)
 
     build(ctx, binaries=["controller", "speaker", "mirror-server"], architectures=[architecture])
-    run("kind load docker-image --name={} metallb/controller:dev-{}".format(name, architecture), echo=True)
-    run("kind load docker-image --name={} metallb/speaker:dev-{}".format(name, architecture), echo=True)
-    run("kind load docker-image --name={} metallb/mirror-server:dev-{}".format(name, architecture), echo=True)
+    run("kind load docker-image --name={} quay.io/metallb/controller:dev-{}".format(name, architecture), echo=True)
+    run("kind load docker-image --name={} quay.io/metallb/speaker:dev-{}".format(name, architecture), echo=True)
+    run("kind load docker-image --name={} quay.io/metallb/mirror-server:dev-{}".format(name, architecture), echo=True)
 
     run("kubectl delete po -nmetallb-system --all", echo=True)
 
@@ -200,7 +232,6 @@ def dev_env(ctx, architecture="amd64", name="kind", cni=None):
         with open(manifests_dir + "/metallb.yaml") as f:
             manifest = f.read()
         manifest = manifest.replace(":main", ":dev-{}".format(architecture))
-        manifest = manifest.replace("imagePullPolicy: Always", "imagePullPolicy: Never")
         with open(tmpdir + "/metallb.yaml", "w") as f:
             f.write(manifest)
             f.flush()
@@ -231,6 +262,95 @@ stringData:
         tmp.flush()
         run("kubectl apply -f {}".format(tmp.name), echo=True)
 
+    if protocol == "bgp":
+        print("Configuring MetalLB with a BGP test environment")
+        bgp_dev_env()
+    else:
+        print("Leaving MetalLB unconfigured")
+
+
+# Configure MetalLB in the dev-env for BGP testing. Start an frr based BGP
+# router in a container and configure MetalLB to peer with it.
+# See dev-env/bgp/README.md for some more information.
+def bgp_dev_env():
+    dev_env_dir = os.getcwd() + "/dev-env/bgp"
+    frr_volume_dir = dev_env_dir + "/frr-volume"
+
+    # TODO -- The IP address handling will need updates to add support for IPv6
+
+    # We need the IPs for each Node in the cluster to place them in the BGP
+    # router configuration file (bgpd.conf). Each Node will peer with this
+    # router.
+    node_ips = run("kubectl get nodes -o jsonpath='{.items[*].status.addresses"
+            "[?(@.type==\"InternalIP\")].address}{\"\\n\"}'", echo=True)
+    node_ips = node_ips.stdout.strip().split()
+    if len(node_ips) != 3:
+        raise Exit(message='Expected 3 nodes, got %d' % len(node_ips))
+
+    # Create a new directory that will be used as the config volume for frr.
+    try:
+        # sudo because past docker runs will have changed ownership of this dir
+        run('sudo rm -rf "%s"' % frr_volume_dir)
+        os.mkdir(frr_volume_dir)
+    except FileExistsError:
+        pass
+    except Exception as e:
+        raise Exit(message='Failed to create frr-volume directory: %s'
+                   % str(e))
+
+    # These two config files are static, so we just copy them straight in.
+    shutil.copyfile("%s/frr/zebra.conf" % dev_env_dir,
+                    "%s/zebra.conf" % frr_volume_dir)
+    shutil.copyfile("%s/frr/daemons" % dev_env_dir,
+                    "%s/daemons" % frr_volume_dir)
+
+    # bgpd.conf is created from a template so that we can include the current
+    # Node IPs.
+    with open("%s/frr/bgpd.conf.tmpl" % dev_env_dir, 'r') as f:
+        bgpd_config = "! THIS FILE IS AUTOGENERATED\n" + f.read()
+    for n in range(0, len(node_ips)):
+        bgpd_config = bgpd_config.replace("NODE%d_IP" % n, node_ips[n])
+    with open("%s/bgpd.conf" % frr_volume_dir, 'w') as f:
+        f.write(bgpd_config)
+
+    # Run a BGP router in a container for all of the speakers to peer with.
+    run('for frr in $(docker ps -a -f name=frr --format {{.Names}}) ; do '
+        '    docker rm -f $frr ; '
+        'done', echo=True)
+    run("docker run -d --privileged --network kind --rm --name frr --volume %s:/etc/frr "
+            "frrouting/frr:latest" % frr_volume_dir, echo=True)
+
+    peer_address = run('docker inspect -f "{{ '
+            'range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" frr', echo=True)
+    with open("%s/config.yaml.tmpl" % dev_env_dir, 'r') as f:
+        mlb_config = "# THIS FILE IS AUTOGENERATED\n" + f.read()
+    mlb_config = mlb_config.replace("PEER_ADDRESS", peer_address.stdout.strip())
+    with open("%s/config.yaml" % dev_env_dir, 'w') as f:
+        f.write(mlb_config)
+    # Apply the MetalLB ConfigMap
+    run("kubectl apply -f %s/config.yaml" % dev_env_dir)
+
+
+@task
+def dev_env_cleanup(ctx):
+    """Remove traces of the dev env."""
+    try:
+        run("kind delete cluster")
+    except Exception:
+        # This will fail if there's no cluster.
+        pass
+
+    run('for frr in $(docker ps -a -f name=frr --format {{.Names}}) ; do '
+        '    docker rm -f $frr ; '
+        'done', echo=True)
+
+    dev_env_dir = os.getcwd() + "/dev-env/bgp"
+    frr_volume_dir = dev_env_dir + "/frr-volume"
+
+    # sudo because past docker runs will have changed ownership of this dir
+    run('sudo rm -rf "%s"' % frr_volume_dir)
+    run('rm -f "%s"/config.yaml' % dev_env_dir)
+
 @task
 def test_cni_manifests(ctx):
     """Update CNI manifests for e2e tests."""
@@ -266,7 +386,7 @@ def release(ctx, version, skip_release_notes=False):
     status = run("git status --porcelain", hide=True).stdout.strip()
     if status != "":
         raise Exit(message="git checkout not clean, cannot release")
-    
+
     version = semver.parse_version_info(version)
     is_patch_release = version.patch != 0
 
@@ -299,9 +419,9 @@ def release(ctx, version, skip_release_notes=False):
         newpat = pattern.format("v{}").format(version)
         run("perl -pi -e 's#{}#{}#g' website/content/*.md website/content/*/*.md".format(oldpat, newpat),
             echo=True)
-    _replace("/google/metallb/{}")
-    _replace("/google/metallb/tree/{}")
-    _replace("/google/metallb/blob/{}")
+    _replace("/metallb/metallb/{}")
+    _replace("/metallb/metallb/tree/{}")
+    _replace("/metallb/metallb/blob/{}")
 
     # Update the version listed on the website sidebar
     run("perl -pi -e 's/MetalLB .*/MetalLB v{}/g' website/content/_header.md".format(version), echo=True)
diff --git a/website/config.toml b/website/config.toml
index 6f8a352e..7c6a94fb 100644
--- a/website/config.toml
+++ b/website/config.toml
@@ -7,7 +7,7 @@ theme = "docdock"
 
 [params]
 	# General information
-    editURL = "https://github.com/google/metallb/edit/main/website/content/"
+    editURL = "https://github.com/metallb/metallb/edit/main/website/content/"
 	description = "MetalLB, bare metal load-balancer for Kubernetes"
     disableNavChevron = true
 
@@ -17,7 +17,7 @@ home = ["HTML", "RSS", "JSON"]
 [[menu.shortcuts]]
 pre = "<hr>"
 name = "<i class='fa fa-github'></i> Github repo"
-url = "https://github.com/google/metallb"
+url = "https://github.com/metallb/metallb"
 weight = 10
 
 [[menu.shortcuts]]
@@ -32,5 +32,5 @@ weight = 30
 
 [[menu.shortcuts]]
 name = "<i class='fa fa-copyright'></i> License"
-url = "https://github.com/google/metallb/blob/main/LICENSE"
+url = "https://github.com/metallb/metallb/blob/main/LICENSE"
 weight = 40
diff --git a/website/content/_index.md b/website/content/_index.md
index 31a99f97..2c07bcc8 100644
--- a/website/content/_index.md
+++ b/website/content/_index.md
@@ -46,10 +46,10 @@ MetalLB requires the following to function:
   [cluster network configuration]({{% relref "installation/network-addons.md" %}}) that
   can coexist with MetalLB.
 - Some IPv4 addresses for MetalLB to hand out.
-- Depending on the operating mode, you may need one or more routers
-  capable of
-  speaking
-  [BGP](https://en.wikipedia.org/wiki/Border_Gateway_Protocol).
+- When using the BGP operating mode, you will need one or more routers
+  capable of speaking [BGP](https://en.wikipedia.org/wiki/Border_Gateway_Protocol).
+- Traffic on port 7946 (TCP & UDP) must be allowed between nodes, as required by
+  [memberlist](https://github.com/hashicorp/memberlist).
 
 ## Usage
 
@@ -67,5 +67,5 @@ information.
 
 One lightweight way you can contribute is
 to
-[tell us that you're using MetalLB](https://github.com/google/metallb/issues/5),
+[tell us that you're using MetalLB](https://github.com/metallb/metallb/issues/5),
 which will give us warm fuzzy feelings :).
diff --git a/website/content/community/_index.md b/website/content/community/_index.md
index 0bf1a6c7..758b0e4d 100644
--- a/website/content/community/_index.md
+++ b/website/content/community/_index.md
@@ -33,7 +33,7 @@ if you don't already have an IRC client.
 ## Issue Tracker
 
 Use the [GitHub issue
-tracker](https://github.com/google/metallb/issues) to file bugs and
+tracker](https://github.com/metallb/metallb/issues) to file bugs and
 features request. If you need support, please send your questions to
 the metallb-users mailing list rather than filing a GitHub issue.
 
@@ -59,6 +59,14 @@ purpose. Consult [GitHub
 Help](https://help.github.com/articles/about-pull-requests/) for more
 information on using pull requests.
 
+## Certificate of Origin
+
+By contributing to this project you agree to the Developer Certificate of
+Origin (DCO). This document was created by the Linux Kernel community and is a
+simple statement that you, as a contributor, have the legal right to make the
+contribution. See the [DCO](https://github.com/metallb/metallb/blob/main/DCO)
+file for details.
+
 ## Code organization
 
 MetalLB's code is divided between a number of binaries, and some
@@ -112,14 +120,14 @@ To develop MetalLB, you'll need a couple of pieces of software:
 - [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/), the Kubernetes commandline interface
 - [Invoke](https://www.pyinvoke.org) to drive the build system
 
->NOTE: The development environment was tested with **kind `v0.7.0`**. Older
+>NOTE: The development environment was tested with **kind `v0.9.0`**. Older
 >versions may not work since there have been breaking changes between minor
 >versions.
 
 ## Building and running the code
 
 Start by fetching the MetalLB repository, with `git clone
-https://github.com/danderson/metallb`.
+https://github.com/metallb/metallb`.
 
 From there, you can use Invoke to build Docker images, push them to
 registries, and so forth. `inv -l` lists the available tasks.
@@ -141,7 +149,7 @@ For example:
  metallb$ go run ./speaker/main.go ./speaker/*controller.go -config-ns metallb-system -kubeconfig $KUBECONFIG -node-name node0
 
 For development, fork
-the [github repository](https://github.com/google/metallb), and add
+the [github repository](https://github.com/metallb/metallb), and add
 your fork as a remote in `$GOPATH/src/go.universe.tf/metallb`, with
 `git remote add fork git@github.com:<your-github-user>/metallb.git`.
 
diff --git a/website/content/community/code-of-conduct.md b/website/content/community/code-of-conduct.md
index 44110471..887952c0 100644
--- a/website/content/community/code-of-conduct.md
+++ b/website/content/community/code-of-conduct.md
@@ -58,11 +58,12 @@ 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 dave@natulte.net. 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.
+reported by contacting Rodrigo Campos at rodrigocc@gmail.com and Dax McDonald
+at daxmc99@gmail.com (please mail both addresses). 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
diff --git a/website/content/community/release-process.md b/website/content/community/release-process.md
index a23fab85..41241f19 100644
--- a/website/content/community/release-process.md
+++ b/website/content/community/release-process.md
@@ -81,7 +81,7 @@ tag. You need to wait for these images to be pushed live before
 continuing, because the manifests for the new release point to image
 tags that don't exist until CircleCI makes them exist.
 
-Check on Docker Hub for a `vX.Y.Z` tag on each image, or check on
+Check on Quay for a `vX.Y.Z` tag on each image, or check on
 CircleCI that the deploy has completed.
 
 ### Repoint the live website
diff --git a/website/content/concepts/layer2.md b/website/content/concepts/layer2.md
index 6c8c3f21..0dc678c3 100644
--- a/website/content/concepts/layer2.md
+++ b/website/content/concepts/layer2.md
@@ -63,7 +63,7 @@ During an unplanned failover, the service IPs will be unreachable until the
 buggy clients refresh their cache entries.
 
 If you encounter a situation where layer 2 mode failover is slow (more than
-about 10s), please [file a bug](https://github.com/google/metallb/issues/new)!
+about 10s), please [file a bug](https://github.com/metallb/metallb/issues/new)!
 We can help you investigate and determine if the issue is with the client, or a
 bug in MetalLB.
 
diff --git a/website/content/configuration/_index.md b/website/content/configuration/_index.md
index da8ed389..26a43a45 100644
--- a/website/content/configuration/_index.md
+++ b/website/content/configuration/_index.md
@@ -8,7 +8,7 @@ creating and deploying a configmap into the same namespace
 (metallb-system) as the deployment.
 
 There is an example configmap in
-[`manifests/example-config.yaml`](https://raw.githubusercontent.com/google/metallb/main/manifests/example-config.yaml),
+[`manifests/example-config.yaml`](https://raw.githubusercontent.com/metallb/metallb/main/manifests/example-config.yaml),
 annotated with explanatory comments.
 
 If you've named the configmap `config.yaml`, you can deploy the manifest with `kubectl apply -f config.yaml`.
@@ -266,8 +266,8 @@ pool with the methods described in
 the [usage](/usage/#requesting-specific-ips) section.
 
 {{% notice note %}}
-Please note that if you have a single address pool, you must use `/32`
-CIDR notation (e.g. `42.176.25.64/32`).
+To specify a single IP address in a pool, use `/32` in the CIDR notation
+(e.g. `42.176.25.64/32`).
 {{% /notice %}}
 
 ### Handling buggy networks
diff --git a/website/content/configuration/calico.md b/website/content/configuration/calico.md
index fcde6acc..bf96744f 100644
--- a/website/content/configuration/calico.md
+++ b/website/content/configuration/calico.md
@@ -206,7 +206,7 @@ graph BT
 While this should theoretically work, it hasn't been demonstrated, and
 setting it up varies wildly based on which routing software/hardware
 you are interfacing with. If you get this working,
-please [let us know](https://github.com/google/metallb/issues/new),
+please [let us know](https://github.com/metallb/metallb/issues/new),
 especially if you have tips on how to make this work best!
 
 ## Ideas wanted!
@@ -217,4 +217,4 @@ acrobatics to integrate the two.
 
 If you have an idea for another workaround that would enable Calico
 and MetalLB to coexist nicely,
-please [tell us](https://github.com/google/metallb/issues/new) !
+please [tell us](https://github.com/metallb/metallb/issues/new) !
diff --git a/website/content/configuration/k3s.md b/website/content/configuration/k3s.md
new file mode 100644
index 00000000..ca0aae00
--- /dev/null
+++ b/website/content/configuration/k3s.md
@@ -0,0 +1,9 @@
+---
+title: Issues with K3s
+weight: 20
+---
+
+K3S come with its own service load balancer named Klipper. You need to disable it in order to run MetalLB.
+To disable Klipper, run the server with the `--disable servicelb` option, as described in [K3s documentation](https://rancher.com/docs/k3s/latest/en/networking/
+)
+
diff --git a/website/content/faq/_index.md b/website/content/faq/_index.md
index 6d421cd1..a5a48a43 100644
--- a/website/content/faq/_index.md
+++ b/website/content/faq/_index.md
@@ -26,4 +26,4 @@ Yes but by default, OpenStack has anti-spoofing protection enabled which prevent
 
 ## Can I update one pool at a time?
 
-Not yet.  The whole configuration for MetalLB is stored in a ConfigMap called `config` in the metallb-system namespace. We anticipate converting to Custom Resources at some point as described in [Issue 196](https://github.com/google/metallb/issues/196).  Contributions Welcome! 
\ No newline at end of file
+Not yet.  The whole configuration for MetalLB is stored in a ConfigMap called `config` in the metallb-system namespace. We anticipate converting to Custom Resources at some point as described in [Issue 196](https://github.com/metallb/metallb/issues/196).  Contributions Welcome! 
\ No newline at end of file
diff --git a/website/content/installation/clouds.md b/website/content/installation/clouds.md
index ec99fd15..2605ca94 100644
--- a/website/content/installation/clouds.md
+++ b/website/content/installation/clouds.md
@@ -18,12 +18,12 @@ Cloud platform | Supported
 AWS            | No, use EKS
 Azure          | No, use AKS
 DigitalOcean   | No, use DigitalOcean Kubernetes
+Equinix Metal  | Yes, see [Equinix Metal notes]
 Google Cloud   | No, use GKE
 Hetzner        | Yes, see [Hetzner notes](https://community.hetzner.com/tutorials/install-kubernetes-cluster)
 OVH            | Yes, when using a vRack
 OpenShift OCP  | Yes, see [OpenShift notes]
 OpenStack      | Yes, see [OpenStack notes]
-Packet         | Yes, see [Packet notes]
 Proxmox        | Yes
 VMware         | Yes
 Vultr          | Yes
@@ -31,7 +31,7 @@ Vultr          | Yes
 [use alternatives]: #alternatives
 [OpenShift notes]: #metallb-on-openshift-ocp
 [OpenStack notes]: #metallb-on-openstack
-[Packet notes]: #metallb-on-packet
+[Equinix Metal notes]: #metallb-on-equinix-metal
 
 ## Why doesn't MetalLB work on (most) cloud platforms?
 
@@ -97,14 +97,14 @@ doesn't know about. There's currently no way to make OpenStack
 cooperate with MetalLB here, so we have to turn off the spoofing
 protection entirely.
 
-### MetalLB on Packet
+### MetalLB on Equinix Metal
 
-[Packet](https://www.packet.com) is an unusually "bare metal" cloud
-platform, and supports using BGP to advertise and route floating IPs
-to machines. As such, MetalLB's BGP mode works great on Packet! There
-is even a [tutorial](https://github.com/packet-labs/kubernetes-bgp)
-written by the folks at Packet, that use MetalLB to integrate
-Kubernetes load-balancers with their BGP infrastructure.
+[Equinix Metal](https://metal.equinix.com) is an unusually "bare metal" cloud
+platform, and supports using BGP to advertise and route floating IPs to
+machines. As such, MetalLB's BGP mode works great on Equinix Metal! There is
+even a [tutorial](https://github.com/packet-labs/kubernetes-bgp) written by the
+folks at Equinix Metal, that use MetalLB to integrate Kubernetes load-balancers
+with their BGP infrastructure.
 
 ## Alternatives
 
@@ -153,5 +153,5 @@ willing to pay for the resources (servers, IPs, ...) required to test
 the integration, then we could *potentially* add support to MetalLB.
 
 If you think you can help with getting resources for testing, [file a
-bug](https://github.com/danderson/metallb/issues/new) and we can talk
+bug](https://github.com/metallb/metallb/issues/new) and we can talk
 about it!
diff --git a/website/content/installation/network-addons.md b/website/content/installation/network-addons.md
index 8ee866fb..178d2e94 100644
--- a/website/content/installation/network-addons.md
+++ b/website/content/installation/network-addons.md
@@ -32,4 +32,4 @@ efficient "IPVS mode", in addition to the default "iptables mode."
 MetalLB **might** work with IPVS mode in kube-proxy, in Kubernetes
 1.13 or later. However, it is not explicitly tested yet, so it's at
 your own risk. See our [tracking
-bug](https://github.com/google/metallb/issues/153) for details.
+bug](https://github.com/metallb/metallb/issues/153) for details.
diff --git a/website/content/release-notes/_index.md b/website/content/release-notes/_index.md
index 0159ef6b..2f7e5639 100644
--- a/website/content/release-notes/_index.md
+++ b/website/content/release-notes/_index.md
@@ -3,14 +3,59 @@ title: Release Notes
 weight: 8
 ---
 
-## Version 0.9.3
+## Version 0.9.6
 [Documentation for this release](https://metallb.universe.tf)
 
 Bugfixes:
 
+- Fix nodeAssigned event on k8s >= 1.20 ([#812](https://github.com/metallb/metallb/pull/812)).
+
+This release includes contributions from Lars Ekman, Rodrigo Campos, Russell
+Bryant and Stefan Lasiewski. Thanks for making MetalLB better!
+
+## Version 0.9.5
+[Documentation for this release](https://v0-9-5--metallb.netlify.com)
+
+New features:
+
+- Update manifests/metallb.yaml for kubernetes v1.19 ([#744](https://github.com/metallb/metallb/pull/744)).
+
+Bugfixes:
+
+- Update repository URLs ([#688](https://github.com/metallb/metallb/pull/688)).
+
+This release includes contributions from Adit Sachde and Jan Krcmar. Thanks for
+making MetalLB better!
+
+## Version 0.9.4
+[Documentation for this release](https://v0-9-4--metallb.netlify.com)
+
+New features:
+
+- Make Memberlist bind port configurable ([#582](https://github.com/metallb/metallb/pull/582)).
+
+Bugfixes:
+
+- Improve speaker log output ([#587](https://github.com/metallb/metallb/pull/587)).
+- Add "other" exec permission to binaries in Docker images ([#644](https://github.com/metallb/metallb/pull/644)).
+- Fix wrong behavior of the addresses_in_use_total metric under certain conditions ([#627](https://github.com/metallb/metallb/pull/627)).
+- Layer 2: Fix Memberlist convergence following a network partition ([#662](https://github.com/metallb/metallb/pull/662)).
+- Layer 2: Send gratuitous ARP / unsolicited NDP neighbor advertisements following a network partition ([#736](https://github.com/metallb/metallb/pull/736)).
+
+This release includes contributions from Andrew Grosser, Chance Carey, Damien
+TOURDE, Etienne Champetier, Johannes Liebermann, Jumpy Squirrel, Lars Ekman,
+Rodrigo Campos, Russell Bryant, Sebastien Dionne, Steven Follis, sumarsono
+Thorsten Schifferdecker, toby cabot and Yuan Liu. Thanks to all of them for
+making MetalLB better!
+
+## Version 0.9.3
+[Documentation for this release](https://v0-9-3--metallb.netlify.com)
+
+Bugfixes:
+
 - Fix manifests to use container image version `v0.9.3` instead of `main`. Users
   of `v0.9.2` are encouraged to upgrade, as [manifests included in that
-  release](https://raw.githubusercontent.com/google/metallb/v0.9.2/manifests/metallb.yaml)
+  release](https://raw.githubusercontent.com/metallb/metallb/v0.9.2/manifests/metallb.yaml)
   use an incorrect container image version. Those two images happen to match
   now but, as development continues on `main` branch, they will differ.
 
@@ -84,7 +129,7 @@ Action required:
 
 Bugfixes:
 
-- Fix layer2 node selection when healthy and unhealthy replicas are colocated on a single node. ([#474](https://github.com/danderson/metallb/issues/474))
+- Fix layer2 node selection when healthy and unhealthy replicas are colocated on a single node. ([#474](https://github.com/metallb/metallb/issues/474))
 
 This release includes contributions from David Anderson and Gary Richards.
 
@@ -94,10 +139,10 @@ This release includes contributions from David Anderson and Gary Richards.
 
 Bugfixes:
 
-- Fix the apiGroup for PodSecurityPolicy, for compatibility with Kubernetes 1.16. ([#458](https://github.com/google/metallb/issues/458)).
-- Fix speaker posting events with an empty string as the announcing node name. ([#456](https://github.com/google/metallb/issues/456)).
+- Fix the apiGroup for PodSecurityPolicy, for compatibility with Kubernetes 1.16. ([#458](https://github.com/metallb/metallb/issues/458)).
+- Fix speaker posting events with an empty string as the announcing node name. ([#456](https://github.com/metallb/metallb/issues/456)).
 - Fix RBAC permissions on speaker, to allow it to post events to all
-  namespaces. ([#455](https://github.com/google/metallb/issues/455)).
+  namespaces. ([#455](https://github.com/metallb/metallb/issues/455)).
 
 This release includes contributions from David Anderson.
 
@@ -136,7 +181,7 @@ New features:
 - MetalLB withdraws BGP announcements entirely for services with no
   healthy pods. This enables anycast geo-redundancy by advertising the
   same IP from multiple Kubernetes
-  clusters. ([#312](https://github.com/google/metallb/issues/312))
+  clusters. ([#312](https://github.com/metallb/metallb/issues/312))
 - The speaker only exposes its Prometheus metrics port on the node IP
   registered with Kubernetes, rather than on all interfaces. This
   should reduce the risk of exposure for clusters where nodes have
@@ -150,7 +195,7 @@ New features:
 - MetalLB now publishes a Kubernetes event to a service, indicating
   which nodes are announcing that service. This makes it much easier
   to determine how traffic is
-  flowing. ([#430](https://github.com/google/metallb/issues/430))
+  flowing. ([#430](https://github.com/metallb/metallb/issues/430))
 - The manifest and Helm chart now use the `apps/v1` version of
   `Deployment` and `DaemonSet`, rather than the obsolete
   `extensions/v1beta1`.
@@ -160,25 +205,25 @@ Bugfixes:
 - Fix address allocation in cases where no addresses were available at
   service creation, but the deletion of another service subsequently
   makes one
-  available. ([#413](https://github.com/google/metallb/issues/413))
+  available. ([#413](https://github.com/metallb/metallb/issues/413))
 - Fix allocation not updating when the address pool annotation
-  changes. ([#448](https://github.com/google/metallb/issues/448)).
+  changes. ([#448](https://github.com/metallb/metallb/issues/448)).
 - Fix periodic crashes due to `glog` trying to write to disk despite
   explicit instructions to the
-  contrary. ([#427](https://github.com/google/metallb/issues/427))
+  contrary. ([#427](https://github.com/metallb/metallb/issues/427))
 - Fix `spec.loadBalancerIP` validation on IPv6 clusters.
-  ([#301](https://github.com/google/metallb/issues/301))
+  ([#301](https://github.com/metallb/metallb/issues/301))
 - Fix BGP Router ID selection on v6 BGP sessions.
 - Fix handling of IPv6 addresses in the BGP connection establishment
   logic.
 - Generate deterministically pseudorandom BGP router IDs in IPv6-only
   clusters.
 - Fix incorrect ARP/NDP responses on bonded interfaces.
-  ([#349](https://github.com/google/metallb/issues/349))
+  ([#349](https://github.com/metallb/metallb/issues/349))
 - Fix ARP/NDP responses sent on interfaces with the NOARP flag.
-  ([#351](https://github.com/google/metallb/issues/351))
+  ([#351](https://github.com/metallb/metallb/issues/351))
 - Update MetalLB logs on the website to the new structured
-  format. ([#275](https://github.com/google/metallb/issues/301))
+  format. ([#275](https://github.com/metallb/metallb/issues/301))
 
 This release includes contributions from Alex Lovell-Troy, Antonio
 Ojea, aojeagarcia, Ashley Dumaine, Brian, Brian Topping, David
@@ -195,7 +240,7 @@ stokbaek and till. Thanks to all of them for making MetalLB better!
 Bugfixes:
 
 - Fix BGP announcement refcounting when using shared
-  IPs. ([#295](https://github.com/google/metallb/issues/295))
+  IPs. ([#295](https://github.com/metallb/metallb/issues/295))
 
 ## Version 0.7.2
 
@@ -204,9 +249,9 @@ Bugfixes:
 Bugfixes:
 
 - Fix gratuitous ARP and NDP announcements on IP
-  failover. ([#291](https://github.com/google/metallb/issues/291))
+  failover. ([#291](https://github.com/metallb/metallb/issues/291))
 - Fix BGP dialing on Arm64, by using `x/sys/unix` instead of the
-  `syscall` package. ([#289](https://github.com/google/metallb/issues/289))
+  `syscall` package. ([#289](https://github.com/metallb/metallb/issues/289))
 
 ## Version 0.7.1
 
@@ -215,7 +260,7 @@ Bugfixes:
 Bugfixes:
 
 - Actually allow layer2 mode to use the Local traffic
-  policy. Oops. ([#279](https://github.com/google/metallb/issues/279))
+  policy. Oops. ([#279](https://github.com/metallb/metallb/issues/279))
 
 ## Version 0.7.0
 
@@ -236,11 +281,11 @@ New features:
 
 - Layer2 mode now supports `externalTrafficPolicy=Local`, meaning layer2
   services can see the true client source
-  IP. ([#257](https://github.com/google/metallb/issues/257))
+  IP. ([#257](https://github.com/metallb/metallb/issues/257))
 - Layer2 mode now selects leader nodes on a per-service level, instead of using
   a single leader node for all services in the cluster. If you have many
   services, this change spreads the load of handling incoming traffic across
-  more than one machine. ([#195](https://github.com/google/metallb/issues/195))
+  more than one machine. ([#195](https://github.com/metallb/metallb/issues/195))
 - MetalLB's maturity has upgraded from _alpha_ to _beta_! Mostly this
   just reflects the increased confidence in the code from the larger
   userbase, and adds some guarantees around graceful upgrades from one
@@ -249,7 +294,7 @@ New features:
 Bugfixes:
 
 - Speaker no longer sends localpref over eBGP sessions
-  ([#266](https://github.com/google/metallb/issues/266))
+  ([#266](https://github.com/metallb/metallb/issues/266))
 
 This release includes contributions from Baul, David Anderson, Ryan
 Roemmich, Sanjeev Rampal, and Steve Sloka. Thanks to all of them for
@@ -261,7 +306,7 @@ making MetalLB better!
 
 Bugfixes:
 
-- Fix nil pointer deref crash on BGP peers that reject MetalLB's OPEN message too promptly ([#250](https://github.com/google/metallb/issues/250))
+- Fix nil pointer deref crash on BGP peers that reject MetalLB's OPEN message too promptly ([#250](https://github.com/metallb/metallb/issues/250))
 
 ## Version 0.6.1
 
@@ -271,7 +316,7 @@ Bugfixes:
 
 - Speaker no longer goes into a tight CPU-burning loop when pods are
   deleted on the
-  node. ([#246](https://github.com/google/metallb/issues/246))
+  node. ([#246](https://github.com/metallb/metallb/issues/246))
 
 ## Version 0.6.0
 
@@ -292,25 +337,25 @@ New features:
   annotations on the Service objects. See
   the
   [IP sharing documentation]({{% relref "usage/_index.md" %}}#ip-address-sharing) for
-  instructions and caveats. ([#121](https://github.com/google/metallb/issues/121))
+  instructions and caveats. ([#121](https://github.com/metallb/metallb/issues/121))
 - Layer 2 mode now listens on all interfaces for ARP and NDP requests,
   not just the interface used for communication by Kubernetes
-  components. ([#165](https://github.com/google/metallb/issues/165))
+  components. ([#165](https://github.com/metallb/metallb/issues/165))
 - MetalLB now uses structured logging instead of Google's glog
   package. Logging events are written to standard output as a series
   of JSON objects suitable for collection by centralized logging
-  systems. ([#189](https://github.com/google/metallb/issues/189))
+  systems. ([#189](https://github.com/metallb/metallb/issues/189))
 - BGP connections can now specify a password for TCP MD5 secured BGP
-  sessions. ([#215](https://github.com/google/metallb/issues/215))
+  sessions. ([#215](https://github.com/metallb/metallb/issues/215))
 - MetalLB is now available as a Helm package in the "stable" Helm
   repository. Note that, due to code review delay, it may take several
   days after a release before the Helm package is
-  updated. ([#177](https://github.com/google/metallb/issues/177))
+  updated. ([#177](https://github.com/metallb/metallb/issues/177))
 
 Bugfixes:
 
 - Correctly use AS_SEQUENCE in eBGP session messages, rather than
-  AS_SET ([#225](https://github.com/google/metallb/issues/225))
+  AS_SET ([#225](https://github.com/metallb/metallb/issues/225))
 
 This release includes contributions from David Anderson, ghorofamike,
 Serguei Bezverkhi, and Zsombor Welker. Thanks to all of them for making
@@ -355,7 +400,7 @@ New features:
   makes it much easier to allocate IP ranges that don't fall cleanly
   on CIDR prefix boundaries.
 - BGP mode supports nodes with multiple interfaces and IP addresses
-  ([#182](https://github.com/google/metallb/issues/182)). Previously,
+  ([#182](https://github.com/metallb/metallb/issues/182)). Previously,
   MetalLB could only establish working BGP sessions on the node's
   "primary" interface, i.e. the one that owned the IP that Kubernetes
   uses to identify the node. Now, peerings may be established via any
@@ -366,7 +411,7 @@ Bugfixes:
 
 - The NDP
   handler
-  [refcounts its sollicited multicast group memberships](https://github.com/google/metallb/issues/184),
+  [refcounts its sollicited multicast group memberships](https://github.com/metallb/metallb/issues/184),
   to avoid extremely rare cases where it might stop responding for a
   service IP.
 
@@ -376,7 +421,7 @@ Bugfixes:
 
 Bugfixes:
 
-- [Remove the --config-ns flag](https://github.com/google/metallb/issues/193)
+- [Remove the --config-ns flag](https://github.com/metallb/metallb/issues/193)
 
 ## Version 0.4.5
 
@@ -384,7 +429,7 @@ Bugfixes:
 
 Bugfixes:
 
-- [Controller doesn't clean up balancers that change their type away from LoadBalancer](https://github.com/google/metallb/issues/190)
+- [Controller doesn't clean up balancers that change their type away from LoadBalancer](https://github.com/metallb/metallb/issues/190)
 
 ## Version 0.4.4
 
@@ -408,7 +453,7 @@ Changes:
 
 Bugfixes:
 
-- [Speaker doesn't readvertise existing services on sessions added by node label changes](https://github.com/google/metallb/issues/181).
+- [Speaker doesn't readvertise existing services on sessions added by node label changes](https://github.com/metallb/metallb/issues/181).
 
 ## Version 0.4.1
 
@@ -416,7 +461,7 @@ Bugfixes:
 
 Bugfixes:
 
-- [Make speaker not crash on machines with IPv6 disabled](https://github.com/google/metallb/issues/180).
+- [Make speaker not crash on machines with IPv6 disabled](https://github.com/metallb/metallb/issues/180).
 
 ## Version 0.4.0
 
@@ -445,7 +490,7 @@ New features:
   [Neighbor Discovery Protocol](https://en.wikipedia.org/wiki/Neighbor_Discovery_Protocol),
   IPv6's analog to ARP. If you have an IPv6 Kubernetes cluster, please
   try it out
-  and [file bugs](https://github.com/google/metallb/issues/new)!
+  and [file bugs](https://github.com/metallb/metallb/issues/new)!
 - BGP peers now have
   a
   [node selector]({{% relref "configuration/_index.md" %}}#limiting-peers-to-certain-nodes). You
@@ -453,7 +498,7 @@ New features:
   topologies.
 - MetalLB now has
   a
-  [Helm chart](https://github.com/google/metallb/tree/main/helm/metallb). If
+  [Helm chart](https://github.com/metallb/metallb/tree/main/helm/metallb). If
   you use [Helm](https://helm.sh) on your cluster, this should make it
   easier to track and manage your MetalLB installation. The chart will
   be submitted for inclusion in the main Helm stable repository
@@ -464,11 +509,11 @@ Other improvements:
 
 - MetalLB
   now
-  [backs off on failing BGP connections](https://github.com/google/metallb/issues/84),
+  [backs off on failing BGP connections](https://github.com/metallb/metallb/issues/84),
   to avoid flooding logs with failures
 - ARP mode should be a little
   more
-  [interoperable with clients](https://github.com/google/metallb/issues/172),
+  [interoperable with clients](https://github.com/metallb/metallb/issues/172),
   and failover should be a little faster, thanks to tweaks to its
   advertisement logic.
 - ARP and NDP modes export [Prometheus](https://prometheus.io) metrics
@@ -487,7 +532,7 @@ all of them for making MetalLB better!
 [Documentation for this release](https://v0-3-1--metallb.netlify.com)
 
 Fixes a couple
-of [embarrassing bugs](https://github.com/google/metallb/issues/142)
+of [embarrassing bugs](https://github.com/metallb/metallb/issues/142)
 that sneaked into 0.3.
 
 Bugfixes:
@@ -511,7 +556,7 @@ Action required if upgrading from 0.2.x:
   ds/bgp-speaker`. This will take down your load-balancers until you
   deploy the new DaemonSet.
 - The
-  [configuration file format](https://raw.githubusercontent.com/google/metallb/main/manifests/example-config.yaml) has
+  [configuration file format](https://raw.githubusercontent.com/metallb/metallb/main/manifests/example-config.yaml) has
   changed in a few backwards-incompatible ways. You need to update
   your ConfigMap by hand:
   - Each `address-pool` must now have a `protocol` field, to select
@@ -570,7 +615,7 @@ Notable fixes:
 
 - MetalLB unable to start because Kubernetes cannot verify that
   "nobody" is a non-root
-  user ([#85](https://github.com/google/metallb/issues/85))
+  user ([#85](https://github.com/metallb/metallb/issues/85))
 
 ## Version 0.2.0
 
@@ -609,7 +654,7 @@ Notable fixes:
 
 ## Version 0.1.0
 
-[Documentation for this release](https://github.com/google/metallb/tree/v0.1)
+[Documentation for this release](https://github.com/metallb/metallb/tree/v0.1)
 
 This was the first tagged version of MetalLB. Its changelog is
 effectively "MetalLB now exists, where previously it did not."
diff --git a/website/content/usage/_index.md b/website/content/usage/_index.md
index 0e7cf67d..079a1657 100644
--- a/website/content/usage/_index.md
+++ b/website/content/usage/_index.md
@@ -146,7 +146,7 @@ In future, MetalLB might be able to overcome the downsides of the
 `Local` traffic policy, in which case it would be unconditionally the
 best mode to use with BGP
 announcements. See
-[issue 1](https://github.com/google/metallb/issues/1) for more
+[issue 1](https://github.com/metallb/metallb/issues/1) for more
 information.
 
 ## IP address sharing
diff --git a/website/content/usage/example.md b/website/content/usage/example.md
index 7b487f64..7f11e52b 100644
--- a/website/content/usage/example.md
+++ b/website/content/usage/example.md
@@ -21,7 +21,7 @@ each set of addresses:
 
 ```yaml
 # Rest of config omitted for brevity
-communities:
+bgp-communities:
   # Our datacenter routers understand a "VPN only" BGP community.
   # Announcements tagged with this community will only be propagated
   # through the corporate VPN tunnel back to developer offices.


Test patch:
diff --git a/dev-env/testsvc.yaml b/dev-env/testsvc.yaml
new file mode 100644
index 00000000..9da7ea7e
--- /dev/null
+++ b/dev-env/testsvc.yaml
@@ -0,0 +1,34 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx
+spec:
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+      - name: nginx
+        image: nginx:1
+        ports:
+        - name: http
+          containerPort: 80
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nginx
+spec:
+  ports:
+  - name: http
+    port: 80
+    protocol: TCP
+    targetPort: 80
+  selector:
+    app: nginx
+  type: LoadBalancer
diff --git a/e2etest/bgp_test.go b/e2etest/bgp_test.go
deleted file mode 100644
index 7ff253c7..00000000
--- a/e2etest/bgp_test.go
+++ /dev/null
@@ -1,318 +0,0 @@
-package main
-
-import (
-	"context"
-	"fmt"
-	"strings"
-	"testing"
-	"time"
-
-	vk "go.universe.tf/virtuakube"
-)
-
-const bgpService = `
-apiVersion: v1
-kind: Service
-metadata:
-  name: mirror-cluster
-spec:
-  ports:
-  - port: 80
-    targetPort: 8080
-  selector:
-    app: mirror
-  type: LoadBalancer
-  loadBalancerIP: 10.249.0.1
-  externalTrafficPolicy: Cluster
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: mirror-local
-spec:
-  ports:
-  - port: 80
-    targetPort: 8080
-  selector:
-    app: mirror
-  type: LoadBalancer
-  loadBalancerIP: 10.249.0.2
-  externalTrafficPolicy: Local
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: mirror-shared1
-  annotations:
-    metallb.universe.tf/allow-shared-ip: mirror
-spec:
-  ports:
-  - port: 80
-    targetPort: 8080
-  selector:
-    app: mirror
-  type: LoadBalancer
-  loadBalancerIP: 10.249.0.3
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: mirror-shared2
-  annotations:
-    metallb.universe.tf/allow-shared-ip: mirror
-spec:
-  ports:
-  - port: 81
-    targetPort: 8081
-  selector:
-    app: mirror
-  type: LoadBalancer
-  loadBalancerIP: 10.249.0.3
-`
-
-// TODO notes:
-//
-//   Does net/http leak connections when there is a context timeout
-//   during an uninterruptible Dial? Sometimes we seem to hit a wall
-//   where SSH refuses to open more connections, implying we're
-//   dropping some.
-//
-//   Need a better way to track, on a fine granularity, "expected
-//   broken" tests, so we can verify that they don't start
-//   unexpectedly passing, and can produce a compatibility matrix.
-//   Should I write my own test framework? That seems fraught with
-//   peril, but maybe something that wraps testing to provide some
-//   extra smarts?
-
-func TestBGP(t *testing.T) { testAll(t, testBGP) }
-func testBGP(t *testing.T, u *vk.Universe) {
-	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
-	defer cancel()
-
-	configureBGP(ctx, t, u)
-
-	cluster := u.Cluster("cluster")
-	client := u.VM("client")
-	//	kube := cluster.KubernetesClient()
-
-	// Create a service, and wait for it to get an IP.
-	if err := cluster.ApplyManifest([]byte(bgpService)); err != nil {
-		t.Fatalf("creating LB service: %v", err)
-	}
-
-	clusterURL := "http://10.249.0.1"
-	localURL := "http://10.249.0.2"
-	sharedURL1 := "http://10.249.0.3:80"
-	sharedURL2 := "http://10.249.0.3:81"
-
-	load := NewLoadGenerator(
-		client,
-		clusterURL,
-		localURL,
-		sharedURL1,
-		sharedURL2,
-	)
-	defer load.Close()
-
-	t.Run("cluster", func(t *testing.T) {
-		var stats *Stats
-		waitFor(ctx, t, func() error {
-			stats = load.Stats(clusterURL, stats)
-
-			if stats.Errors > 0 {
-				return fmt.Errorf("%s still returning errors", clusterURL)
-			}
-			if stats.Pods() != 2 {
-				return fmt.Errorf("%s has %d pods, waiting for 2", clusterURL, stats.Pods())
-			}
-			if err := stats.BalancedByPod(0.2); err != nil {
-				return fmt.Errorf("%s %s", clusterURL, err)
-			}
-			return nil
-		})
-
-		if stats.Nodes() != 2 {
-			t.Errorf("want 2 nodes processing traffic, got %d", stats.Nodes())
-		}
-		if err := stats.BalancedByNode(0.2); err != nil {
-			t.Error(err)
-		}
-
-		testBroken(t, "client-ip-correct", func(t *testing.T) {
-			if stats.Clients() != 2 {
-				t.Errorf("want 2 client sending traffic, got %d", stats.Clients())
-			}
-			if err := stats.BalancedByClient(0.2); err != nil {
-				t.Error(err)
-			}
-		})
-	})
-
-	t.Run("local", func(t *testing.T) {
-		var stats *Stats
-		waitFor(ctx, t, func() error {
-			stats = load.Stats(localURL, stats)
-
-			if stats.Errors > 0 {
-				return fmt.Errorf("%s still returning errors", localURL)
-			}
-			if stats.Pods() != 2 {
-				return fmt.Errorf("%s has %d pods, waiting for 2", localURL, stats.Pods())
-			}
-			if err := stats.BalancedByPod(0.2); err != nil {
-				return fmt.Errorf("%s %s", localURL, err)
-			}
-
-			return nil
-		})
-
-		if stats.Nodes() != 2 {
-			t.Errorf("want 2 nodes processing traffic, got %d", stats.Nodes())
-		}
-		if err := stats.BalancedByNode(0.2); err != nil {
-			t.Error(err)
-		}
-
-		testBroken(t, "client-ip-correct", func(t *testing.T) {
-			if stats.Clients() != 1 {
-				t.Errorf("want 1 client sending traffic, got %d", stats.Clients())
-			}
-			if err := stats.BalancedByClient(0.2); err != nil {
-				t.Error(err)
-			}
-		})
-	})
-
-	t.Run("shared", func(t *testing.T) {
-		var stats1, stats2 *Stats
-		waitFor(ctx, t, func() error {
-			stats1 = load.Stats(sharedURL1, stats2)
-
-			if stats1.Errors > 0 {
-				return fmt.Errorf("%s still returning errors", sharedURL1)
-			}
-			if stats1.Pods() != 2 {
-				return fmt.Errorf("%s has %d pods, waiting for 2", sharedURL1, stats1.Pods())
-			}
-			if err := stats1.BalancedByPod(0.2); err != nil {
-				return fmt.Errorf("%s %s", sharedURL1, err)
-			}
-
-			stats2 = load.Stats(sharedURL2, stats2)
-
-			if stats2.Errors > 0 {
-				return fmt.Errorf("%s still returning errors", sharedURL2)
-			}
-			if stats2.Pods() != 2 {
-				return fmt.Errorf("%s has %d pods, waiting for 2", sharedURL2, stats2.Pods())
-			}
-			if err := stats2.BalancedByPod(0.2); err != nil {
-				return fmt.Errorf("%s %s", sharedURL2, err)
-			}
-
-			return nil
-		})
-
-		if stats1.Nodes() != 2 {
-			t.Errorf("want 2 nodes processing traffic, got %d", stats1.Nodes())
-		}
-		if stats2.Nodes() != 2 {
-			t.Errorf("want 2 nodes processing traffic, got %d", stats2.Nodes())
-		}
-		if err := stats1.BalancedByNode(0.2); err != nil {
-			t.Error(err)
-		}
-		if err := stats2.BalancedByNode(0.2); err != nil {
-			t.Error(err)
-		}
-
-		testBroken(t, "client-ip-correct", func(t *testing.T) {
-			if stats1.Clients() != 2 {
-				t.Errorf("want 2 client sending traffic, got %d", stats1.Clients())
-			}
-			if err := stats1.BalancedByClient(0.2); err != nil {
-				t.Error(err)
-			}
-			if stats2.Clients() != 2 {
-				t.Errorf("want 2 client sending traffic, got %d", stats2.Clients())
-			}
-			if err := stats2.BalancedByClient(0.2); err != nil {
-				t.Error(err)
-			}
-		})
-	})
-}
-
-// Helpers
-
-// configureBGP installs a MetalLB configuration for BGP, and waits
-// for peering to come up on the client.
-func configureBGP(ctx context.Context, t *testing.T, u *vk.Universe) {
-	cluster := u.Cluster("cluster")
-	client := u.VM("client")
-
-	cfg := []byte(fmt.Sprintf(`
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  namespace: metallb-system
-  name: config
-data:
-  config: |
-    peers:
-    - peer-address: %s
-      peer-asn: 64513
-      my-asn: 64512
-    address-pools:
-    - name: default
-      protocol: bgp
-      addresses:
-      - 10.249.0.0/24
-      avoid-buggy-ips: true
-`, client.IPv4("net1")))
-	if err := cluster.ApplyManifest(cfg); err != nil {
-		t.Fatalf("Applying MetalLB configuration: %v", err)
-	}
-
-	waitFor(ctx, t, func() error {
-		for ctx.Err() == nil {
-			bs, err := client.Run("birdc show protocol")
-			if err != nil {
-				t.Fatalf("running birdc failed: %v", err)
-			}
-
-			// First 2 lines are a header, skip that. All other protocols
-			// should be in state "up"
-			//
-			// Sample birdc output:
-			//   BIRD 1.6.3 ready.
-			//   name     proto    table    state  since       info
-			//   kernel1  Kernel   master   up     17:53:32
-			//   device1  Device   master   up     17:53:32
-			//   direct1  Direct   master   up     17:53:32
-			//   controller BGP      master   start  17:53:32    Passive
-			//   node0    BGP      master   start  17:53:32    Passive
-			stilldown := []string{}
-			lines := strings.Split(string(bs), "\n")
-			if len(lines) < 3 {
-				continue
-			}
-			for _, line := range lines[2:] {
-				if strings.TrimSpace(line) == "" {
-					continue
-				}
-				fs := strings.Fields(line)
-				if fs[3] != "up" {
-					stilldown = append(stilldown, fs[0])
-				}
-			}
-
-			if len(stilldown) > 0 {
-				return fmt.Errorf("BGP sessions still down: %s", strings.Join(stilldown, ", "))
-			}
-
-			return nil
-		}
-		return ctx.Err()
-	})
-}
diff --git a/e2etest/broken.go b/e2etest/broken.go
deleted file mode 100644
index 2653dad2..00000000
--- a/e2etest/broken.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package main
-
-import (
-	"os"
-	"strings"
-	"testing"
-)
-
-// This file tracks the subset of test cases that are known to be
-// broken with some combinations of network addons or other k8s
-// features. Each "feature tag" has a list of glob patterns for the
-// tests that cannot pass that feature check.
-
-var broken = map[string][]string{
-	"client-ip-correct": {
-		// Client source IP assignment is broken in kube-proxy, in
-		// that the masquerade IP is not stable. This is reproducible
-		// with multiple network addons. Therefore we don't verify it
-		// here.
-		"*/*/cluster",
-		"*/*/shared",
-		// Weave masquerades traffic even with externalTrafficPolicy=Local.
-		"*/weave/local",
-	},
-}
-
-func testBroken(t *testing.T, featureName string, f func(t *testing.T)) {
-	testName := t.Name()
-patterns:
-	for _, pattern := range broken[featureName] {
-		fields := strings.Split(testName, "/")
-		patternFields := strings.Split(pattern, "/")
-		if len(fields) != len(patternFields) {
-			t.Fatalf("Pattern has wrong length for feature test, %q vs. %q", testName, pattern)
-		}
-		for i := range fields {
-			if patternFields[i] != "*" && patternFields[i] != fields[i] {
-				continue patterns
-			}
-		}
-
-		if os.Getenv("E2E_RUNBROKEN") != "" {
-			continue patterns
-		}
-
-		// Pattern matched, don't run the test. Instead run a stub to
-		// mark it skipped.
-		t.Run(featureName, func(t *testing.T) {
-			t.Skip("known broken")
-		})
-		return
-	}
-
-	// No patterns matched, test needs to be run.
-	t.Run(featureName, func(t *testing.T) {
-		f(t)
-	})
-}
diff --git a/e2etest/helpers.go b/e2etest/helpers.go
deleted file mode 100644
index 546656c1..00000000
--- a/e2etest/helpers.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package main
-
-import (
-	"context"
-	"errors"
-	"testing"
-	"time"
-)
-
-func waitFor(ctx context.Context, t *testing.T, test func() error) {
-	t.Helper()
-	var (
-		lastErr = errors.New("dummy error")
-		count   = 1
-	)
-
-	for {
-		select {
-		case <-ctx.Done():
-			if count > 1 {
-				t.Log("  (last message repeasted", count, "times")
-			}
-			t.Fatalf("timed out")
-		default:
-		}
-
-		err := test()
-		if err == nil {
-			if count > 1 {
-				t.Log("  (last message repeasted", count, "times")
-			}
-			return
-		}
-
-		if err.Error() != lastErr.Error() {
-			if count > 1 {
-				t.Log("  (last message repeated", count, "times")
-			}
-			t.Log(time.Now(), err)
-			lastErr = err
-			count = 1
-		} else {
-			count++
-		}
-
-		time.Sleep(100 * time.Millisecond)
-	}
-}
diff --git a/e2etest/loadgen.go b/e2etest/loadgen.go
deleted file mode 100644
index 0c096f5d..00000000
--- a/e2etest/loadgen.go
+++ /dev/null
@@ -1,228 +0,0 @@
-package main
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"io/ioutil"
-	"net"
-	"net/http"
-	"strings"
-	"sync"
-	"time"
-
-	vk "go.universe.tf/virtuakube"
-)
-
-type LoadGenerator struct {
-	client    *vk.VM
-	transport *http.Transport
-	targets   map[string]*stats
-	stop      chan struct{}
-}
-
-type stats struct {
-	mu    sync.Mutex
-	cond  *sync.Cond
-	start time.Time
-	prev  *Stats
-	cur   *Stats
-}
-
-type Stats struct {
-	HitsPerNode   map[string]int64
-	HitsPerPod    map[string]int64
-	HitsPerClient map[string]int64
-	Total         int64
-	Errors        int64
-}
-
-func (s *Stats) Nodes() int {
-	return len(s.HitsPerNode)
-}
-
-func (s *Stats) Pods() int {
-	return len(s.HitsPerPod)
-}
-
-func (s *Stats) Clients() int {
-	return len(s.HitsPerClient)
-}
-
-func (s *Stats) balancedBy(name string, m map[string]int64, allowedImbalance float64) error {
-	errs := []string{}
-
-	perThing := float64(s.Total) / float64(len(m)) / float64(s.Total)
-	min, max := perThing-allowedImbalance, perThing+allowedImbalance
-	for thing, n := range m {
-		fraction := float64(n) / float64(s.Total)
-		if fraction < min || fraction > max {
-			errs = append(errs, fmt.Sprintf("%s %q got %f%% of traffic, want %f—%f%%", name, thing, fraction*100, min*100, max*100))
-		}
-	}
-
-	if len(errs) > 0 {
-		return errors.New(strings.Join(errs, "; "))
-	}
-	return nil
-}
-
-func (s *Stats) BalancedByNode(allowedImbalance float64) error {
-	return s.balancedBy("node", s.HitsPerNode, allowedImbalance)
-}
-
-func (s *Stats) BalancedByPod(allowedImbalance float64) error {
-	return s.balancedBy("pod", s.HitsPerPod, allowedImbalance)
-}
-
-func (s *Stats) BalancedByClient(allowedImbalance float64) error {
-	return s.balancedBy("client", s.HitsPerClient, allowedImbalance)
-}
-
-func NewLoadGenerator(client *vk.VM, urls ...string) *LoadGenerator {
-	ret := &LoadGenerator{
-		client: client,
-		transport: &http.Transport{
-			Dial: func(network, addr string) (net.Conn, error) {
-				return client.Dial(network, addr)
-			},
-			DisableKeepAlives: true,
-		},
-		targets: map[string]*stats{},
-		stop:    make(chan struct{}),
-	}
-	for _, url := range urls {
-		ret.targets[url] = &stats{
-			start: time.Now(),
-			prev: &Stats{
-				HitsPerNode:   map[string]int64{},
-				HitsPerPod:    map[string]int64{},
-				HitsPerClient: map[string]int64{},
-			},
-			cur: &Stats{
-				HitsPerNode:   map[string]int64{},
-				HitsPerPod:    map[string]int64{},
-				HitsPerClient: map[string]int64{},
-			},
-		}
-		ret.targets[url].cond = sync.NewCond(&ret.targets[url].mu)
-		go ret.run(url)
-	}
-	return ret
-}
-
-// Only returns when the state evolves beyond prev.
-func (l *LoadGenerator) Stats(url string, prev *Stats) *Stats {
-	st := l.targets[url]
-	st.mu.Lock()
-	defer st.mu.Unlock()
-
-	for st.prev == prev {
-		st.cond.Wait()
-	}
-	for st.prev.Total == 0 {
-		st.cond.Wait()
-	}
-
-	return st.prev
-}
-
-func (l *LoadGenerator) AllStats(prev map[string]*Stats) map[string]*Stats {
-	ret := map[string]*Stats{}
-
-	if prev == nil {
-		prev = map[string]*Stats{}
-	}
-
-	for url := range l.targets {
-		ret[url] = l.Stats(url, prev[url])
-	}
-
-	return ret
-}
-
-func (l *LoadGenerator) Close() {
-	select {
-	case <-l.stop:
-	default:
-		close(l.stop)
-	}
-}
-
-func (l *LoadGenerator) run(url string) {
-	for {
-		select {
-		case <-l.stop:
-			return
-		default:
-		}
-		l.oneRequest(url)
-	}
-}
-
-func (l *LoadGenerator) oneRequest(url string) {
-	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
-	defer cancel()
-
-	req, err := http.NewRequest("GET", url, nil)
-	if err != nil {
-		panic(err)
-	}
-	req = req.WithContext(ctx)
-
-	resp, err := l.transport.RoundTrip(req)
-	if err != nil {
-		l.recordError(url)
-		return
-	}
-	defer resp.Body.Close()
-	bs, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		l.recordError(url)
-		return
-	}
-
-	fs := strings.Split(string(bs), "\n")
-	node, pod, client := fs[0], fs[1], fs[2]
-	l.recordHit(url, node, pod, client)
-}
-
-func (l *LoadGenerator) recordHit(url, node, pod, client string) {
-	st := l.targets[url]
-	st.mu.Lock()
-	defer st.mu.Unlock()
-	if time.Since(st.start) > time.Second {
-		st.start = time.Now()
-		st.prev = st.cur
-		st.cur = &Stats{
-			HitsPerNode:   map[string]int64{},
-			HitsPerPod:    map[string]int64{},
-			HitsPerClient: map[string]int64{},
-		}
-		st.cond.Broadcast()
-	}
-
-	st.cur.Total++
-	st.cur.HitsPerNode[node]++
-	st.cur.HitsPerPod[pod]++
-	st.cur.HitsPerClient[client]++
-}
-
-func (l *LoadGenerator) recordError(url string) {
-	st := l.targets[url]
-	st.mu.Lock()
-	defer st.mu.Unlock()
-	if time.Since(st.start) > time.Second {
-		st.start = time.Now()
-		st.prev = st.cur
-		st.cur = &Stats{
-			HitsPerNode:   map[string]int64{},
-			HitsPerPod:    map[string]int64{},
-			HitsPerClient: map[string]int64{},
-		}
-		st.cond.Broadcast()
-	}
-
-	st.cur.Total++
-	st.cur.Errors++
-}
diff --git a/e2etest/main.go b/e2etest/main.go
deleted file mode 100644
index c914ef08..00000000
--- a/e2etest/main.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/spf13/cobra"
-)
-
-func main() {
-	if err := rootCmd.Execute(); err != nil {
-		fmt.Println(err)
-		os.Exit(1)
-	}
-}
-
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
-	Use:   "e2etest",
-	Short: "end to end test helper binary",
-}
diff --git a/e2etest/main_test.go b/e2etest/main_test.go
deleted file mode 100644
index 8e75b942..00000000
--- a/e2etest/main_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package main
-
-import (
-	"flag"
-	"fmt"
-	"os"
-	"os/exec"
-	"testing"
-)
-
-func TestMain(m *testing.M) {
-	flag.Parse()
-
-	if testing.Short() {
-		fmt.Println("Skipping e2e tests because short testing was requested.")
-		os.Exit(0)
-	}
-
-	cmd := exec.Command(
-		"go", "run", "make.go",
-		"-a", "build,image",
-		"-b", "controller,speaker,e2etest-mirror-server",
-		"--tag", "e2e",
-		"--registry", "metallb",
-	)
-	cmd.Dir = ".."
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	if err := cmd.Run(); err != nil {
-		fmt.Printf("Failed to build MetalLB images: %v", err)
-		os.Exit(1)
-	}
-
-	os.Exit(m.Run())
-}
diff --git a/e2etest/manifests/mirror-server.yaml b/e2etest/manifests/mirror-server.yaml
index 4dd13e10..5f723457 100644
--- a/e2etest/manifests/mirror-server.yaml
+++ b/e2etest/manifests/mirror-server.yaml
@@ -17,7 +17,7 @@ spec:
     spec:
       containers:
       - name: mirror
-        image: metallb/mirror-server:main
+        image: quay.io/metallb/mirror-server:main
         env:
         - name: NODE_NAME
           valueFrom:
diff --git a/e2etest/vcluster.go b/e2etest/vcluster.go
deleted file mode 100644
index f9812de7..00000000
--- a/e2etest/vcluster.go
+++ /dev/null
@@ -1,477 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"context"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"os/exec"
-	"strconv"
-	"strings"
-	"testing"
-
-	"github.com/spf13/cobra"
-	kerr "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	vk "go.universe.tf/virtuakube"
-)
-
-const cachedUniverse = `../e2etest-cached-universe`
-
-var mkUniverseCmd = &cobra.Command{
-	Use:   "mkuniverse",
-	Short: "build the multiverse used for tests",
-	Args:  cobra.NoArgs,
-	Run: func(*cobra.Command, []string) {
-		if err := mkUniverse(); err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-	},
-}
-
-var mkUniverseSteps []string
-
-func init() {
-	rootCmd.AddCommand(mkUniverseCmd)
-	mkUniverseCmd.Flags().StringSliceVar(&mkUniverseSteps, "steps", []string{}, "steps to forcibly regenerate")
-}
-
-func testAll(t *testing.T, f func(t *testing.T, u *vk.Universe)) {
-	for _, base := range []string{"calico", "flannel", "weave"} {
-		t.Run(base, func(t *testing.T) {
-			cfg := &vk.UniverseConfig{}
-			if os.Getenv("E2E_VERBOSE") != "" {
-				cfg.CommandLog = os.Stdout
-			}
-			if os.Getenv("E2E_USERSPACE") != "" {
-				cfg.NoAcceleration = true
-			}
-
-			u, err := vk.Open(cachedUniverse, base, cfg)
-			if err != nil {
-				t.Fatalf("Opening universe at snapshot %q: %v", base, err)
-			}
-			defer u.Close()
-
-			c := u.Cluster("cluster")
-
-			err = c.PushImages(
-				"metallb/controller:e2e-amd64",
-				"metallb/speaker:e2e-amd64",
-				"metallb/e2etest-mirror-server:e2e-amd64",
-			)
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			bs, err := ioutil.ReadFile("../manifests/metallb.yaml")
-			if err != nil {
-				t.Fatalf("reading metallb manifest: %v", err)
-			}
-			manifest := string(bs)
-			manifest = strings.Replace(manifest, "metallb/speaker:master", "metallb/speaker:e2e-amd64", -1)
-			manifest = strings.Replace(manifest, "metallb/controller:master", "metallb/controller:e2e-amd64", -1)
-			manifest = strings.Replace(manifest, "PullPolicy: Always", "PullPolicy: IfNotPresent", -1)
-			if err := c.ApplyManifest([]byte(manifest)); err != nil {
-				t.Fatalf("applying metallb manifest: %v", err)
-			}
-
-			bs, err = ioutil.ReadFile("manifests/mirror-server.yaml")
-			if err != nil {
-				t.Fatalf("getting registry manifest bytes: %v", err)
-			}
-			if err := c.ApplyManifest(bs); err != nil {
-				t.Fatalf("deploying mirror server: %v", err)
-			}
-
-			f(t, u)
-		})
-	}
-}
-
-func mkUniverse() error {
-	buildStep("", mkImage, "image")
-	buildStep("image", mkCluster, "cluster")
-	buildStep("cluster", mkClusterNet("calico"), "calico")
-	buildStep("cluster", mkClusterNet("weave"), "weave")
-	buildStep("cluster", mkClusterNet("flannel"), "flannel")
-
-	// TODO: IPVS mode? Can we build separate clusters for that?
-
-	return nil
-}
-
-func buildStep(basesnap string, do func(*vk.Universe) error, resultsnap string) {
-	if err := buildStepInternal(basesnap, do, resultsnap); err != nil {
-		fmt.Println(err)
-		os.Exit(1)
-	}
-}
-
-func buildStepInternal(basesnap string, do func(*vk.Universe) error, resultsnap string) error {
-	cfg := &vk.UniverseConfig{
-		CommandLog: os.Stdout,
-	}
-
-	if os.Getenv("E2E_USERSPACE") != "" {
-		cfg.NoAcceleration = true
-	}
-
-	fmt.Printf("Running build step %q\n", resultsnap)
-	defer fmt.Printf("Build step %q done\n", resultsnap)
-
-	var universe *vk.Universe
-	_, err := os.Stat(cachedUniverse)
-	if os.IsNotExist(err) {
-		universe, err = vk.Create(cachedUniverse, cfg)
-	} else if err != nil {
-		return fmt.Errorf("stat of universe: %v", err)
-	} else {
-		universe, err = vk.Open(cachedUniverse, basesnap, cfg)
-	}
-	if err != nil {
-		return fmt.Errorf("opening universe: %v", err)
-	}
-	defer universe.Close()
-
-	if !hasSnapshot(universe, basesnap) {
-		return fmt.Errorf("universe doesn't have base snapshot %q", basesnap)
-	}
-
-	if hasSnapshot(universe, resultsnap) {
-		return nil
-	}
-
-	if err := do(universe); err != nil {
-		return fmt.Errorf("building %q from %q failed: %v", resultsnap, basesnap, err)
-	}
-
-	if err := universe.Save(resultsnap); err != nil {
-		return fmt.Errorf("saving result snapshot %q: %v", resultsnap, err)
-	}
-
-	return nil
-}
-
-func hasSnapshot(u *vk.Universe, snap string) bool {
-	for _, name := range u.Snapshots() {
-		if name == snap {
-			return true
-		}
-	}
-	return false
-}
-
-func mkImage(u *vk.Universe) error {
-	cfg := &vk.ImageConfig{
-		Name: "image",
-		CustomizeFuncs: []vk.ImageCustomizeFunc{
-			vk.CustomizeInstallK8s,
-			vk.CustomizePreloadK8sImages,
-			func(v *vk.VM) error {
-				return v.RunMultiple(
-					"apt-get install bird",
-					"systemctl disable bird",
-					"systemctl disable bird6",
-				)
-			},
-		},
-	}
-
-	if err := u.NewImage(cfg); err != nil {
-		return fmt.Errorf("creating VM base image: %v", err)
-	}
-
-	return nil
-}
-
-func mkCluster(u *vk.Universe) error {
-	// Two virtual networks, for testing some L2 mode things.
-	if err := u.NewNetwork(&vk.NetworkConfig{Name: "net1"}); err != nil {
-		return fmt.Errorf("creating net1: %v", err)
-	}
-	if err := u.NewNetwork(&vk.NetworkConfig{Name: "net2"}); err != nil {
-		return fmt.Errorf("creating net2: %v", err)
-	}
-
-	// Kubernetes cluster.
-	clusterCfg := &vk.ClusterConfig{
-		Name:     "cluster",
-		NumNodes: 1,
-		VMConfig: &vk.VMConfig{
-			Image:     "image",
-			MemoryMiB: 1024,
-			Networks:  []string{"net1", "net2"},
-		},
-	}
-	cluster, err := u.NewCluster(clusterCfg)
-	if err != nil {
-		return fmt.Errorf("creating cluster: %v", err)
-	}
-	if err := cluster.Start(); err != nil {
-		return fmt.Errorf("starting cluster: %v", err)
-	}
-
-	// Client VM that lives outside the k8s cluster.
-	vmCfg := &vk.VMConfig{
-		Name:      "client",
-		Image:     "image",
-		MemoryMiB: 1024,
-		Networks:  []string{"net1", "net2"},
-	}
-	client, err := u.NewVM(vmCfg)
-	if err != nil {
-		return fmt.Errorf("creating client VM: %v", err)
-	}
-	if err := client.Start(); err != nil {
-		return fmt.Errorf("starting client VM: %v", err)
-	}
-
-	var bird4, bird6 bytes.Buffer
-	fmt.Fprintf(&bird4, birdBaseCfg, client.IPv4("net1"))
-	fmt.Fprintf(&bird6, birdBaseCfg, client.IPv4("net1"))
-
-	fmt.Fprintf(&bird4, birdPeerCfg, "controller", cluster.Controller().IPv4("net1"))
-	fmt.Fprintf(&bird6, birdPeerCfg, "controller", cluster.Controller().IPv6("net1"))
-
-	for i, vm := range cluster.Nodes() {
-		fmt.Fprintf(&bird4, birdPeerCfg, "node"+strconv.Itoa(i), vm.IPv4("net1"))
-		fmt.Fprintf(&bird6, birdPeerCfg, "node"+strconv.Itoa(i), vm.IPv6("net1"))
-	}
-
-	if err := client.WriteFile("/etc/bird/bird.conf", bird4.Bytes()); err != nil {
-		return err
-	}
-	if err := client.WriteFile("/etc/bird/bird6.conf", bird6.Bytes()); err != nil {
-		return err
-	}
-	err = client.RunMultiple(
-		"systemctl enable bird",
-		"systemctl enable bird6",
-		"systemctl restart bird",
-		"systemctl restart bird6",
-	)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func mkClusterNet(addon string) func(*vk.Universe) error {
-	return func(u *vk.Universe) error {
-		c := u.Cluster("cluster")
-
-		bs, err := ioutil.ReadFile(fmt.Sprintf("manifests/%s.yaml", addon))
-		if err != nil {
-			return fmt.Errorf("getting network addon %q: %v", addon, err)
-		}
-
-		if err := c.ApplyManifest(bs); err != nil {
-			return fmt.Errorf("installing network addon %q: %v", addon, err)
-		}
-
-		if err := c.WaitFor(context.Background(), c.NodesReady); err != nil {
-			return fmt.Errorf("waiting for nodes to become ready: %v", err)
-		}
-
-		// Wait for all deployments to schedule, which signals that
-		// the network addon's finished setting up.
-		err = c.WaitFor(context.Background(), func() (bool, error) {
-			deploys, err := c.KubernetesClient().AppsV1().Deployments("").List(metav1.ListOptions{})
-			if err != nil {
-				return false, err
-			}
-			for _, deploy := range deploys.Items {
-				if deploy.Status.AvailableReplicas != deploy.Status.Replicas {
-					return false, nil
-				}
-			}
-			return true, nil
-		})
-		if err != nil {
-			return err
-		}
-
-		return nil
-	}
-}
-
-func buildUniverse() error {
-	universe, err := vk.Open(cachedUniverse, "", nil)
-	if err != nil {
-		return fmt.Errorf("opening universe: %v", err)
-	}
-	defer universe.Close()
-
-	vmCfg := &vk.VMConfig{
-		Name:  "client",
-		Image: "base",
-	}
-
-	clusterCfg := &vk.ClusterConfig{
-		Name:     "cluster",
-		NumNodes: 2,
-		VMConfig: vmCfg,
-	}
-
-	cluster, err := universe.NewCluster(clusterCfg)
-	if err != nil {
-		return fmt.Errorf("creating cluster: %v", err)
-	}
-	if err := cluster.Start(); err != nil {
-		return fmt.Errorf("starting cluster: %v", err)
-	}
-
-	client, err := universe.NewVM(vmCfg)
-	if err != nil {
-		return fmt.Errorf("creating client VM: %v", err)
-	}
-	if err := client.Start(); err != nil {
-		return fmt.Errorf("starting client VM: %v", err)
-	}
-
-	var bird4, bird6 bytes.Buffer
-	fmt.Fprintf(&bird4, birdBaseCfg, client.IPv4(""))
-	fmt.Fprintf(&bird6, birdBaseCfg, client.IPv4(""))
-
-	fmt.Fprintf(&bird4, birdPeerCfg, "controller", cluster.Controller().IPv4(""))
-	fmt.Fprintf(&bird6, birdPeerCfg, "controller", cluster.Controller().IPv6(""))
-
-	for i, vm := range cluster.Nodes() {
-		fmt.Fprintf(&bird4, birdPeerCfg, "node"+strconv.Itoa(i), vm.IPv4(""))
-		fmt.Fprintf(&bird6, birdPeerCfg, "node"+strconv.Itoa(i), vm.IPv6(""))
-	}
-
-	if err := client.WriteFile("/etc/bird/bird.conf", bird4.Bytes()); err != nil {
-		return err
-	}
-	if err := client.WriteFile("/etc/bird/bird6.conf", bird6.Bytes()); err != nil {
-		return err
-	}
-	err = client.RunMultiple(
-		"systemctl restart bird",
-		"systemctl restart bird6",
-	)
-	if err != nil {
-		return err
-	}
-
-	cmd := exec.Command(
-		"make", "push-images",
-		"REGISTRY=localhost:"+strconv.Itoa(cluster.Controller().ForwardedPort(30000)),
-		"TAG=e2e",
-		"ARCH=amd64",
-		"BINARIES=e2etest-mirror-server",
-	)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("pushing mirror server image: %v", err)
-	}
-
-	cmd = exec.Command("kubectl", "apply", "-f", "-")
-	cmd.Stdin = bytes.NewBufferString(mirrorServerManifest)
-	cmd.Env = []string{"KUBECONFIG=" + cluster.Kubeconfig()}
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("starting mirror server daemonset: %v", err)
-	}
-
-	err = cluster.WaitFor(context.Background(), func() (bool, error) {
-		ds, err := cluster.KubernetesClient().AppsV1().DaemonSets("default").Get("mirror", metav1.GetOptions{})
-		if err != nil {
-			if kerr.IsNotFound(err) {
-				return false, nil
-			}
-			return false, err
-		}
-
-		if int(ds.Status.DesiredNumberScheduled) != clusterCfg.NumNodes+1 {
-			return false, err
-		}
-
-		if ds.Status.NumberAvailable != ds.Status.DesiredNumberScheduled {
-			return false, err
-		}
-
-		return true, nil
-	})
-	if err != nil {
-		return fmt.Errorf("waiting for mirror server to start: %v", err)
-	}
-
-	if err := universe.Save(""); err != nil {
-		return fmt.Errorf("saving universe: %v", err)
-	}
-
-	return nil
-}
-
-const (
-	birdBaseCfg = `
-router id %s;
-
-protocol kernel {
-  persist;
-  merge paths;
-  scan time 60;
-  import none;
-  export all;
-}
-
-protocol device {
-  scan time 60;
-}
-
-protocol direct {
-  interface "ens4";
-  check link;
-  import all;
-}`
-
-	birdPeerCfg = `
-protocol bgp %s {
-  local as 64513;
-  neighbor %s as 64512;
-  passive;
-  import keep filtered;
-  import all;
-  export none;  
-}`
-	mirrorServerManifest = `
-apiVersion: apps/v1beta2
-kind: DaemonSet
-metadata:
-  namespace: default
-  name: mirror
-  labels:
-    app: mirror
-spec:
-  selector:
-    matchLabels:
-      app: mirror
-  template:
-    metadata:
-      labels:
-        app: mirror
-    spec:
-      containers:
-      - name: mirror
-        image: 127.0.0.1:30000/e2etest-mirror-server:e2e-amd64
-        env:
-        - name: NODE_NAME
-          valueFrom:
-            fieldRef:
-              fieldPath: spec.nodeName
-        - name: POD_UID
-          valueFrom:
-            fieldRef:
-              fieldPath: metadata.uid
-        ports:
-        - name: http
-          containerPort: 8080
-`
-)
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 2ff39125..0a50d10b 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -345,6 +345,19 @@ peers:
 `,
 		},
 
+		{
+			desc: "duplicate peers",
+			raw: `
+peers:
+- my-asn: 42
+  peer-asn: 42
+  peer-address: 1.2.3.4
+- my-asn: 42
+  peer-asn: 42
+  peer-address: 1.2.3.4
+`,
+		},
+
 		{
 			desc: "no pool name",
 			raw: `
diff --git a/internal/layer2/announcer_test.go b/internal/layer2/announcer_test.go
index 20deb0b2..737cc398 100644
--- a/internal/layer2/announcer_test.go
+++ b/internal/layer2/announcer_test.go
@@ -9,6 +9,7 @@ func Test_SetBalancer_AddsToAnnouncedServices(t *testing.T) {
 	announce := &Announce{
 		ips:      map[string]net.IP{},
 		ipRefcnt: map[string]int{},
+		spamCh:   make(chan net.IP, 1),
 	}
 
 	services := []struct {
@@ -27,6 +28,8 @@ func Test_SetBalancer_AddsToAnnouncedServices(t *testing.T) {
 
 	for _, service := range services {
 		announce.SetBalancer(service.name, service.ip)
+		// We need to empty spamCh as spamLoop() is not started.
+		<-announce.spamCh
 
 		if !announce.AnnounceName(service.name) {
 			t.Fatalf("service %v is not anounced", service.name)
diff --git a/speaker/layer2_controller_test.go b/speaker/layer2_controller_test.go
index 384fe8b5..48e3acf5 100644
--- a/speaker/layer2_controller_test.go
+++ b/speaker/layer2_controller_test.go
@@ -13,6 +13,18 @@ import (
 	v1 "k8s.io/api/core/v1"
 )
 
+type fakeSpeakerList struct {
+	speakers map[string]bool
+}
+
+func (sl *fakeSpeakerList) UsableSpeakers() map[string]bool {
+	return sl.speakers
+}
+
+func (sl *fakeSpeakerList) Rejoin() {
+	return
+}
+
 func compareUseableNodesReturnedValue(a, b []string) bool {
 	if &a == &b {
 		return true
@@ -43,6 +55,8 @@ func TestUsableNodes(t *testing.T) {
 
 		eps *v1.Endpoints
 
+		usableSpeakers map[string]bool
+
 		cExpectedResult []string
 	}{
 		{
@@ -63,6 +77,7 @@ func TestUsableNodes(t *testing.T) {
 					},
 				},
 			},
+			usableSpeakers:  map[string]bool{"iris1": true, "iris2": true},
 			cExpectedResult: []string{"iris1", "iris2"},
 		},
 
@@ -84,6 +99,7 @@ func TestUsableNodes(t *testing.T) {
 					},
 				},
 			},
+			usableSpeakers:  map[string]bool{"iris1": true, "iris2": true},
 			cExpectedResult: []string{"iris1"},
 		},
 
@@ -107,12 +123,13 @@ func TestUsableNodes(t *testing.T) {
 					},
 				},
 			},
+			usableSpeakers:  map[string]bool{"iris1": true, "iris2": true},
 			cExpectedResult: []string{"iris1"},
 		},
 	}
 
 	for _, test := range tests {
-		response := usableNodes(test.eps, nil)
+		response := usableNodes(test.eps, test.usableSpeakers)
 		sort.Strings(response)
 		if !compareUseableNodesReturnedValue(response, test.cExpectedResult) {
 			t.Errorf("%q: shouldAnnounce for controller returned incorrect result, expected '%s', but received '%s'", test.desc, test.cExpectedResult, response)
@@ -121,9 +138,16 @@ func TestUsableNodes(t *testing.T) {
 }
 
 func TestShouldAnnounce(t *testing.T) {
+	fakeSL := &fakeSpeakerList{
+		speakers: map[string]bool{
+			"iris1": true,
+			"iris2": true,
+		},
+	}
 	c1, err := newController(controllerConfig{
 		MyNode: "iris1",
 		Logger: log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+		SList:  fakeSL,
 	})
 	if err != nil {
 		t.Fatalf("creating controller: %s", err)
@@ -133,6 +157,7 @@ func TestShouldAnnounce(t *testing.T) {
 	c2, err := newController(controllerConfig{
 		MyNode: "iris2",
 		Logger: log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+		SList:  fakeSL,
 	})
 	if err != nil {
 		t.Fatalf("creating controller: %s", err)