Repository: linode/linode-cloud-controller-manager
Pull request #99 (2021-09-13 14:24:10)

Fix bug that caused generic API errors via InstanceExistsByProviderID to delete nodes
These changes refactor the implementation of `InstanceExistsByProviderID` to only return false without error, and thereby causing the corresponding node to be deleted, when the linodego client returns a "Not Found" error. Any other errors will be propagated, and the call to `InstanceExistsByProviderID` will be retried.

The actual bug fix is in [this commit](https://github.com/linode/linode-cloud-controller-manager/commit/8d70a47ae286d8deb3b9bbe7667f074551635fe9).

The codebase needed to be refactored to test for this bug:
- adopt gomock and generate linodego client mock - a34db90
- break out shared helpers, replace `*linodego.Client` with `linodeClient`, etc. - 5b959a7
- Remove unneeded test code for helpers in `fakeAPI` - b0111b2

Eventually, `fakeAPI` should be removed in favor of the `linodeClient` mock. The loadbalancers test code can be updated later on.



Patch:
diff --git a/Makefile b/Makefile
index 9c397a8..30420f3 100644
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,10 @@ $(GOPATH)/bin/goimports:
 $(GOPATH)/bin/ginkgo:
 	GO111MODULE=off go get -u github.com/onsi/ginkgo/ginkgo
 
+.PHONY: codegen
+codegen:
+	go generate ./...
+
 .PHONY: vet
 # lint the codebase
 vet:
@@ -29,18 +33,18 @@ fmt: vet $(GOPATH)/bin/goimports
 
 .PHONY: test
 # we say code is not worth testing unless it's formatted
-test: $(GOPATH)/bin/ginkgo fmt
+test: $(GOPATH)/bin/ginkgo fmt codegen
 	ginkgo -r --v --progress --trace --cover --skipPackage=test $(TEST_ARGS)
 
 .PHONY: build-linux
-build-linux:
+build-linux: codegen
 	echo "cross compiling linode-cloud-controller-manager for linux/amd64" && \
 		GOOS=linux GOARCH=amd64 \
 		CGO_ENABLED=0 \
 		go build -o dist/linode-cloud-controller-manager-linux-amd64 .
 
 .PHONY: build
-build:
+build: codegen
 	echo "compiling linode-cloud-controller-manager" && \
 		CGO_ENABLED=0 \
 		go build -o dist/linode-cloud-controller-manager .
diff --git a/cloud/linode/client.go b/cloud/linode/client.go
new file mode 100644
index 0000000..b1db8c5
--- /dev/null
+++ b/cloud/linode/client.go
@@ -0,0 +1,29 @@
+package linode
+
+//go:generate go run github.com/golang/mock/mockgen -destination mock_client_test.go -package linode github.com/linode/linode-cloud-controller-manager/cloud/linode LinodeClient
+
+import (
+	"context"
+
+	"github.com/linode/linodego"
+)
+
+type LinodeClient interface {
+	GetInstance(context.Context, int) (*linodego.Instance, error)
+	ListInstances(context.Context, *linodego.ListOptions) ([]linodego.Instance, error)
+	GetInstanceIPAddresses(context.Context, int) (*linodego.InstanceIPAddressResponse, error)
+
+	CreateNodeBalancer(context.Context, linodego.NodeBalancerCreateOptions) (*linodego.NodeBalancer, error)
+	GetNodeBalancer(context.Context, int) (*linodego.NodeBalancer, error)
+	UpdateNodeBalancer(context.Context, int, linodego.NodeBalancerUpdateOptions) (*linodego.NodeBalancer, error)
+	DeleteNodeBalancer(context.Context, int) error
+	ListNodeBalancers(context.Context, *linodego.ListOptions) ([]linodego.NodeBalancer, error)
+
+	CreateNodeBalancerConfig(context.Context, int, linodego.NodeBalancerConfigCreateOptions) (*linodego.NodeBalancerConfig, error)
+	DeleteNodeBalancerConfig(context.Context, int, int) error
+	ListNodeBalancerConfigs(context.Context, int, *linodego.ListOptions) ([]linodego.NodeBalancerConfig, error)
+	RebuildNodeBalancerConfig(context.Context, int, int, linodego.NodeBalancerConfigRebuildOptions) (*linodego.NodeBalancerConfig, error)
+}
+
+// linodego.Client implements LinodeClient
+var _ LinodeClient = (*linodego.Client)(nil)
diff --git a/cloud/linode/cloud.go b/cloud/linode/cloud.go
index c02e5fd..b355690 100644
--- a/cloud/linode/cloud.go
+++ b/cloud/linode/cloud.go
@@ -27,7 +27,7 @@ var Options struct {
 }
 
 type linodeCloud struct {
-	client        *linodego.Client
+	client        LinodeClient
 	instances     cloudprovider.Instances
 	zones         cloudprovider.Zones
 	loadbalancers cloudprovider.LoadBalancer
diff --git a/cloud/linode/common.go b/cloud/linode/common.go
new file mode 100644
index 0000000..b4a7cd5
--- /dev/null
+++ b/cloud/linode/common.go
@@ -0,0 +1,65 @@
+package linode
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/linode/linodego"
+	"github.com/pkg/errors"
+	"k8s.io/apimachinery/pkg/types"
+	cloudprovider "k8s.io/cloud-provider"
+)
+
+const providerIDPrefix = "linode://"
+
+type invalidProviderIDError struct {
+	value string
+}
+
+func (e invalidProviderIDError) Error() string {
+	return fmt.Sprintf("invalid provider ID %q", e.value)
+}
+
+func parseProviderID(providerID string) (int, error) {
+	if !strings.HasPrefix(providerID, providerIDPrefix) {
+		return 0, invalidProviderIDError{providerID}
+	}
+	id, err := strconv.Atoi(strings.TrimPrefix(providerID, providerIDPrefix))
+	if err != nil {
+		return 0, invalidProviderIDError{providerID}
+	}
+	return id, nil
+}
+
+func linodeFilterListOptions(targetLabel string) *linodego.ListOptions {
+	jsonFilter := fmt.Sprintf(`{"label":%q}`, targetLabel)
+	return linodego.NewListOptions(0, jsonFilter)
+}
+
+func linodeByName(ctx context.Context, client LinodeClient, nodeName types.NodeName) (*linodego.Instance, error) {
+	linodes, err := client.ListInstances(ctx, linodeFilterListOptions(string(nodeName)))
+	if err != nil {
+		return nil, err
+	}
+
+	if len(linodes) == 0 {
+		return nil, cloudprovider.InstanceNotFound
+	} else if len(linodes) > 1 {
+		return nil, errors.New(fmt.Sprintf("Multiple instances found with name %v", nodeName))
+	}
+
+	return &linodes[0], nil
+}
+
+func linodeByID(ctx context.Context, client LinodeClient, id int) (*linodego.Instance, error) {
+	instance, err := client.GetInstance(ctx, id)
+	if err != nil {
+		return nil, err
+	}
+	if instance == nil {
+		return nil, fmt.Errorf("linode not found with id %v", id)
+	}
+	return instance, nil
+}
diff --git a/cloud/linode/instances.go b/cloud/linode/instances.go
index 7233994..21af4f7 100644
--- a/cloud/linode/instances.go
+++ b/cloud/linode/instances.go
@@ -2,28 +2,33 @@ package linode
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
+	"net/http"
 	"strconv"
-	"strings"
 
-	"github.com/linode/linode-cloud-controller-manager/cloud"
 	"github.com/linode/linode-cloud-controller-manager/sentry"
 	"github.com/linode/linodego"
-	"github.com/pkg/errors"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	cloudprovider "k8s.io/cloud-provider"
 )
 
 type instances struct {
-	client *linodego.Client
+	client LinodeClient
 }
 
-func newInstances(client *linodego.Client) cloudprovider.Instances {
+func newInstances(client LinodeClient) cloudprovider.Instances {
 	return &instances{client}
 }
 
+type instanceNoIPAddressesError struct {
+	id int
+}
+
+func (e instanceNoIPAddressesError) Error() string {
+	return fmt.Sprintf("instance %d has no IP addresses", e.id)
+}
+
 func (i *instances) NodeAddresses(ctx context.Context, name types.NodeName) ([]v1.NodeAddress, error) {
 	ctx = sentry.SetHubOnContext(ctx)
 	sentry.SetTag(ctx, "node_name", string(name))
@@ -47,7 +52,7 @@ func (i *instances) NodeAddressesByProviderID(ctx context.Context, providerID st
 	ctx = sentry.SetHubOnContext(ctx)
 	sentry.SetTag(ctx, "provider_id", providerID)
 
-	id, err := linodeIDFromProviderID(providerID)
+	id, err := parseProviderID(providerID)
 	if err != nil {
 		sentry.CaptureError(ctx, err)
 		return nil, err
@@ -78,7 +83,7 @@ func (i *instances) nodeAddresses(ctx context.Context, linode *linodego.Instance
 	}
 
 	if (len(ips.IPv4.Public) == 0) && (len(ips.IPv4.Private) == 0) {
-		return nil, fmt.Errorf("instance has no IP addresses")
+		return nil, instanceNoIPAddressesError{linode.ID}
 	}
 
 	if len(ips.IPv4.Public) > 0 {
@@ -122,13 +127,13 @@ func (i *instances) InstanceTypeByProviderID(ctx context.Context, providerID str
 	ctx = sentry.SetHubOnContext(ctx)
 	sentry.SetTag(ctx, "provider_id", providerID)
 
-	id, err := linodeIDFromProviderID(providerID)
+	id, err := parseProviderID(providerID)
 	if err != nil {
 		sentry.CaptureError(ctx, err)
 		return "", err
 	}
 
-	sentry.SetTag(ctx, "linode_id", id)
+	sentry.SetTag(ctx, "linode_id", strconv.Itoa(id))
 
 	linode, err := linodeByID(ctx, i.client, id)
 	if err != nil {
@@ -139,7 +144,7 @@ func (i *instances) InstanceTypeByProviderID(ctx context.Context, providerID str
 }
 
 func (i *instances) AddSSHKeyToAllInstances(_ context.Context, user string, keyData []byte) error {
-	return cloud.ErrNotImplemented
+	return cloudprovider.NotImplemented
 }
 
 func (i *instances) CurrentNodeName(_ context.Context, hostname string) (types.NodeName, error) {
@@ -150,81 +155,26 @@ func (i *instances) InstanceExistsByProviderID(ctx context.Context, providerID s
 	ctx = sentry.SetHubOnContext(ctx)
 	sentry.SetTag(ctx, "provider_id", providerID)
 
-	id, err := linodeIDFromProviderID(providerID)
+	id, err := parseProviderID(providerID)
 	if err != nil {
 		sentry.CaptureError(ctx, err)
 		return false, err
 	}
 
-	sentry.SetTag(ctx, "linode_id", id)
+	sentry.SetTag(ctx, "linode_id", strconv.Itoa(id))
 
 	_, err = linodeByID(ctx, i.client, id)
-	if err == nil {
-		return true, nil
+	if err != nil {
+		if apiError, ok := err.(*linodego.Error); ok && apiError.Code == http.StatusNotFound {
+			return false, nil
+		}
+		sentry.CaptureError(ctx, err)
+		return false, err
 	}
 
-	sentry.CaptureError(ctx, err)
-
-	return false, nil
+	return true, nil
 }
 
 func (i *instances) InstanceShutdownByProviderID(ctx context.Context, providerID string) (bool, error) {
 	return false, cloudprovider.NotImplemented
 }
-
-func linodeByID(ctx context.Context, client *linodego.Client, id string) (*linodego.Instance, error) {
-	linodeID, err := strconv.Atoi(id)
-	if err != nil {
-		return nil, err
-	}
-
-	instance, err := client.GetInstance(ctx, linodeID)
-	if err != nil {
-		return nil, err
-	}
-	if instance == nil {
-		return nil, fmt.Errorf("linode not found with id %v", linodeID)
-	}
-	return instance, nil
-}
-
-func linodeByName(ctx context.Context, client *linodego.Client, nodeName types.NodeName) (*linodego.Instance, error) {
-	jsonFilter, err := json.Marshal(map[string]string{"label": string(nodeName)})
-	if err != nil {
-		return nil, err
-	}
-
-	linodes, err := client.ListInstances(ctx, linodego.NewListOptions(0, string(jsonFilter)))
-	if err != nil {
-		return nil, err
-	}
-
-	if len(linodes) == 0 {
-		return nil, cloudprovider.InstanceNotFound
-	} else if len(linodes) > 1 {
-		return nil, errors.New(fmt.Sprintf("Multiple instances found with name %v", nodeName))
-	}
-
-	return &linodes[0], nil
-}
-
-// serverIDFromProviderID returns a Linode ID from a providerID.
-//
-// The providerID can be seen on the Kubernetes Node object. The expected
-// format is: linode://linodeID
-func linodeIDFromProviderID(providerID string) (string, error) {
-	if providerID == "" {
-		return "", errors.New("providerID cannot be empty string")
-	}
-
-	split := strings.Split(providerID, "://")
-	if len(split) != 2 {
-		return "", fmt.Errorf("unexpected providerID format: %s, format should be: linode://12345", providerID)
-	}
-
-	if split[0] != ProviderName {
-		return "", fmt.Errorf("provider scheme from providerID should be 'linode://', %s", providerID)
-	}
-
-	return split[1], nil
-}
diff --git a/cloud/linode/loadbalancers.go b/cloud/linode/loadbalancers.go
index 1432053..38e7ab9 100644
--- a/cloud/linode/loadbalancers.go
+++ b/cloud/linode/loadbalancers.go
@@ -61,7 +61,7 @@ func (e lbNotFoundError) Error() string {
 }
 
 type loadbalancers struct {
-	client *linodego.Client
+	client LinodeClient
 	zone   string
 
 	kubeClient kubernetes.Interface
@@ -81,7 +81,7 @@ type portConfig struct {
 }
 
 // newLoadbalancers returns a cloudprovider.LoadBalancer whose concrete type is a *loadbalancer.
-func newLoadbalancers(client *linodego.Client, zone string) cloudprovider.LoadBalancer {
+func newLoadbalancers(client LinodeClient, zone string) cloudprovider.LoadBalancer {
 	return &loadbalancers{client: client, zone: zone}
 }
 
diff --git a/cloud/linode/zones.go b/cloud/linode/zones.go
index c394e6b..00d2f13 100644
--- a/cloud/linode/zones.go
+++ b/cloud/linode/zones.go
@@ -3,17 +3,16 @@ package linode
 import (
 	"context"
 
-	"github.com/linode/linodego"
 	"k8s.io/apimachinery/pkg/types"
 	cloudprovider "k8s.io/cloud-provider"
 )
 
 type zones struct {
-	client *linodego.Client
+	client LinodeClient
 	region string
 }
 
-func newZones(client *linodego.Client, zone string) cloudprovider.Zones {
+func newZones(client LinodeClient, zone string) cloudprovider.Zones {
 	return zones{client, zone}
 }
 
@@ -22,7 +21,7 @@ func (z zones) GetZone(_ context.Context) (cloudprovider.Zone, error) {
 }
 
 func (z zones) GetZoneByProviderID(ctx context.Context, providerID string) (cloudprovider.Zone, error) {
-	id, err := linodeIDFromProviderID(providerID)
+	id, err := parseProviderID(providerID)
 	if err != nil {
 		return cloudprovider.Zone{}, err
 	}
diff --git a/go.mod b/go.mod
index ac05bee..3de1c59 100644
--- a/go.mod
+++ b/go.mod
@@ -5,10 +5,12 @@ go 1.15
 require (
 	github.com/appscode/go v0.0.0-20200323182826-54e98e09185a
 	github.com/getsentry/sentry-go v0.4.0
+	github.com/golang/mock v1.6.0
 	github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 // indirect
 	github.com/linode/linodego v0.21.1
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/pflag v1.0.5
+	github.com/stretchr/testify v1.7.0
 	go.uber.org/zap v1.13.0 // indirect
 	k8s.io/api v0.19.2
 	k8s.io/apimachinery v0.19.2
diff --git a/go.sum b/go.sum
index cce0329..4672b34 100644
--- a/go.sum
+++ b/go.sum
@@ -44,12 +44,10 @@ github.com/Microsoft/hcsshim v0.8.10-0.20200715222032-5eafd1556990/go.mod h1:ay/
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0=
 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.0.0 h1:0GoNN3taZV6QI81IXgCbxMyEaJDXMSIjArYBCYzVVvs=
 github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
-github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2 h1:JCHLVE3B+kJde7bIEo5N4J+ZbLhp0J1Fs+ulyRws4gE=
 github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
@@ -122,13 +120,10 @@ github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kw
 github.com/containerd/typeurl v1.0.0/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
 github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
 github.com/coredns/corefile-migration v1.0.10/go.mod h1:RMy/mXdeDlYwzt0vdMEJvT2hGJ2I86/eO0UdXmH9XNI=
-github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
 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-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
-github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -145,7 +140,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 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=
@@ -171,7 +165,6 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
 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 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -189,7 +182,6 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
 github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 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=
@@ -197,7 +189,6 @@ github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv
 github.com/getsentry/sentry-go v0.4.0 h1:WqRI2/7EiALbdG9qGB47c0Aks1tdznG5DZd6GSQ1y/8=
 github.com/getsentry/sentry-go v0.4.0/go.mod h1:xkGcb82SipKQloDNa5b7hTV4VdEyc2bhwd1/UczP52k=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
 github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
@@ -226,14 +217,12 @@ github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2
 github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
 github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
 github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
-github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1 h1:wSt/4CYxs70xbATrGXhokKF1i0tZjENLOo1ioIO13zk=
 github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
 github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
 github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
 github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
 github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
 github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
-github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9 h1:tF+augKRWlWx0J0B7ZyyKSiTyV6E1zZe+7b3qQlcEf8=
 github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
 github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
 github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
@@ -276,14 +265,12 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
 github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
 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/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 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=
@@ -292,11 +279,10 @@ github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4er
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 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/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 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/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 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.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
@@ -318,7 +304,6 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
 github.com/google/cadvisor v0.37.0/go.mod h1:OhDE+goNVel0eGY8mR7Ifq1QUI1in5vJBIgIpcajK/I=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -383,7 +368,6 @@ github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
@@ -409,13 +393,11 @@ github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
 github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
-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/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 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=
@@ -454,7 +436,6 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
 github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
-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/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -500,7 +481,6 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v
 github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
 github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
 github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -523,12 +503,10 @@ github.com/opencontainers/selinux v1.5.2/go.mod h1:yTcKuYAh6R95iDpefGLQaPaRwJFwy
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 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/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -573,7 +551,6 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
-github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@@ -588,7 +565,6 @@ 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/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-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/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
 github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
@@ -596,7 +572,6 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 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 v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -607,10 +582,10 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 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/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/thecodeteam/goscaleio v0.1.0/go.mod h1:68sdkZAsK8bvEwBlbQnlLS+xU+hvLYM/iQ8KXej1AwM=
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
@@ -618,10 +593,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
-github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
-github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -648,7 +621,7 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
 github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
@@ -685,7 +658,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 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-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -709,7 +681,6 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
 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 h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
@@ -719,14 +690,14 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
+golang.org/x/mod v0.4.2/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-20181005035420-146acd28ed58/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-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
 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-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -735,7 +706,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 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=
@@ -745,19 +715,17 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
 golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/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-20200226121028-0de0cce0169b/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-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -766,12 +734,12 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
 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/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU=
 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=
@@ -782,7 +750,6 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -792,7 +759,6 @@ golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7w
 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-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -812,9 +778,12 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
 golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 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=
@@ -853,17 +822,18 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
 golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/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-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200616133436-c1934b75d054 h1:HHeAlu5H9b71C+Fx0K+1dGgVFN1DM1/wz4aoGOA5qS8=
 golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gomodules.xyz/version v0.1.0/go.mod h1:Y8xuV02mL/45psyPKG3NCVOwvAOy6T5Kx0l3rCjKSjU=
 gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
 gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
@@ -879,27 +849,23 @@ google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb
 google.golang.org/api v0.15.1-0.20200106000736-b8fc810ca6b5/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
 google.golang.org/api v0.15.1/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 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 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 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 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
 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-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-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 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
 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.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -921,7 +887,6 @@ google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEG
 google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 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=
@@ -938,7 +903,6 @@ gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm
 gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
-gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA=
 gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
@@ -947,13 +911,13 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
 gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 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 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+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=
 gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
@@ -1018,7 +982,6 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9/go.mod h1:dzAXnQb
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
-sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
 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=


Test patch:
diff --git a/cloud/linode/common_test.go b/cloud/linode/common_test.go
new file mode 100644
index 0000000..98b4499
--- /dev/null
+++ b/cloud/linode/common_test.go
@@ -0,0 +1,52 @@
+package linode
+
+import "testing"
+
+func TestParseProviderID(t *testing.T) {
+	for _, tc := range []struct {
+		name        string
+		providerID  string
+		expectedID  int
+		errExpected bool
+	}{
+		{
+			name:        "empty string is invalid",
+			providerID:  "",
+			errExpected: true,
+		},
+		{
+			name:        "malformed provider id",
+			providerID:  "invalidproviderid!",
+			errExpected: true,
+		},
+		{
+			name:        "wrong prefix",
+			providerID:  "notlinode://123",
+			errExpected: true,
+		},
+		{
+			name:       "valid",
+			providerID: "linode://123",
+			expectedID: 123,
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			id, err := parseProviderID(tc.providerID)
+			if err != nil {
+				if !tc.errExpected {
+					t.Errorf("unexpected error: %v", err)
+				}
+			} else if tc.errExpected {
+				t.Error("expected an error; got nil")
+			}
+
+			if id != tc.expectedID {
+				t.Errorf("expected id to be %d; got %d", tc.expectedID, id)
+			}
+		})
+	}
+}
+
+func stringPtr(s string) *string {
+	return &s
+}
diff --git a/cloud/linode/fake_linode_test.go b/cloud/linode/fake_linode_test.go
index e7a6cca..4bb22be 100644
--- a/cloud/linode/fake_linode_test.go
+++ b/cloud/linode/fake_linode_test.go
@@ -18,12 +18,10 @@ import (
 )
 
 type fakeAPI struct {
-	t        *testing.T
-	instance *linodego.Instance
-	ips      []*linodego.InstanceIP
-	nb       map[string]*linodego.NodeBalancer
-	nbc      map[string]*linodego.NodeBalancerConfig
-	nbn      map[string]*linodego.NodeBalancerNode
+	t   *testing.T
+	nb  map[string]*linodego.NodeBalancer
+	nbc map[string]*linodego.NodeBalancerConfig
+	nbn map[string]*linodego.NodeBalancerNode
 
 	requests map[fakeRequest]struct{}
 }
@@ -40,42 +38,8 @@ type filterStruct struct {
 }
 
 func newFake(t *testing.T) *fakeAPI {
-	publicIP := net.ParseIP("45.79.101.25")
-	privateIP := net.ParseIP("192.168.133.65")
-	instanceName := "test-instance"
-	region := "us-east"
 	return &fakeAPI{
-		t: t,
-		instance: &linodego.Instance{
-			Label:      instanceName,
-			Region:     region,
-			Image:      "linode/ubuntu16.04lts",
-			Type:       "g6-standard-2",
-			Group:      "Linode-Group",
-			ID:         123,
-			Status:     "running",
-			Hypervisor: "kvm",
-			IPv4: []*net.IP{
-				&publicIP,
-				&privateIP,
-			},
-		},
-		ips: []*linodego.InstanceIP{
-			{
-				Address:  publicIP.String(),
-				Public:   true,
-				LinodeID: 123,
-				Type:     "ipv4",
-				Region:   region,
-			},
-			{
-				Address:  privateIP.String(),
-				Public:   false,
-				LinodeID: 123,
-				Type:     "ipv4",
-				Region:   region,
-			},
-		},
+		t:        t,
 		nb:       make(map[string]*linodego.NodeBalancer),
 		nbc:      make(map[string]*linodego.NodeBalancerConfig),
 		nbn:      make(map[string]*linodego.NodeBalancerNode),
@@ -112,62 +76,6 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	case "GET":
 		whichAPI := strings.Split(urlPath[1:], "/")
 		switch whichAPI[0] {
-		case "linode":
-			switch whichAPI[1] {
-			case "instances":
-				rx, _ := regexp.Compile("/linode/instances/[0-9]+/ips")
-				if rx.MatchString(urlPath) {
-					resp := linodego.InstanceIPAddressResponse{
-						IPv4: &linodego.InstanceIPv4Response{
-							Public:  []*linodego.InstanceIP{f.ips[0]},
-							Private: []*linodego.InstanceIP{f.ips[1]},
-						},
-					}
-					rr, _ := json.Marshal(resp)
-					_, _ = w.Write(rr)
-					return
-				}
-
-				rx, _ = regexp.Compile("/linode/instances/[0-9]+")
-				if rx.MatchString(urlPath) {
-					id := filepath.Base(urlPath)
-					if id == strconv.Itoa(f.instance.ID) {
-						rr, _ := json.Marshal(&f.instance)
-						_, _ = w.Write(rr)
-					}
-					return
-				}
-
-				rx, _ = regexp.Compile("/linode/instances")
-				if rx.MatchString(urlPath) {
-					res := 0
-					data := []linodego.Instance{}
-					filter := r.Header.Get("X-Filter")
-					if filter == "" {
-						data = append(data, *f.instance)
-					} else {
-						var fs filterStruct
-						err := json.Unmarshal([]byte(filter), &fs)
-						if err != nil {
-							f.t.Fatal(err)
-						}
-						if fs.Label == f.instance.Label {
-							data = append(data, *f.instance)
-						}
-					}
-					resp := linodego.InstancesPagedResponse{
-						PageOptions: &linodego.PageOptions{
-							Page:    1,
-							Pages:   1,
-							Results: res,
-						},
-						Data: data,
-					}
-					rr, _ := json.Marshal(resp)
-					_, _ = w.Write(rr)
-					return
-				}
-			}
 		case "nodebalancers":
 			rx, _ := regexp.Compile("/nodebalancers/[0-9]+/configs/[0-9]+/nodes/[0-9]+")
 			if rx.MatchString(urlPath) {
diff --git a/cloud/linode/instances_test.go b/cloud/linode/instances_test.go
index 8ba663e..828f343 100644
--- a/cloud/linode/instances_test.go
+++ b/cloud/linode/instances_test.go
@@ -3,291 +3,318 @@ package linode
 import (
 	"context"
 	"net/http"
-	"net/http/httptest"
-	"reflect"
+	"strconv"
 	"testing"
 
+	"github.com/golang/mock/gomock"
 	"github.com/linode/linodego"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
 	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	cloudprovider "k8s.io/cloud-provider"
 )
 
-func TestCCMInstances(t *testing.T) {
-	fake := newFake(t)
-	ts := httptest.NewServer(fake)
-	defer ts.Close()
-
-	linodeClient := linodego.NewClient(http.DefaultClient)
-	linodeClient.SetBaseURL(ts.URL)
-
-	testCases := []struct {
-		name string
-		f    func(*testing.T, *linodego.Client)
-	}{
-		{
-			name: "Instances Init",
-			f:    testNewInstances,
-		},
-		{
-			name: "Node Addresses",
-			f:    testNodeAddresses,
-		},
-		{
-			name: "Node Addresses by ProviderID",
-			f:    testNodeAddressesByProviderID,
-		},
-		{
-			name: "Intance ID",
-			f:    testInstanceID,
-		},
-		{
-			name: "Instance Type",
-			f:    testInstanceType,
-		},
-		{
-			name: "Instance Type by ProviderID",
-			f:    testInstanceTypeByProviderID,
-		},
-		{
-			name: "Instance Exists By Provider ID",
-			f:    testInstanceExistsByProviderID,
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			tc.f(t, &linodeClient)
-		})
-	}
-}
-
-func testNewInstances(t *testing.T, client *linodego.Client) {
-	linodeInstances := newInstances(client)
-	if linodeInstances.(*instances).client != client {
-		t.Errorf("instances not initialized")
-	}
-}
+func TestInstanceExistsByProviderID(t *testing.T) {
+	ctx := context.TODO()
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
 
-func testNodeAddresses(t *testing.T, client *linodego.Client) {
-	testCases := []struct {
-		name string
-		f    func(*testing.T, *linodego.Client)
-	}{
-		{
-			name: "Node Addresses Found",
-			f:    testNodeAddressesFound,
-		},
-		{
-			name: "Node Addresses Not Found",
-			f:    testNodeAddressesNotFound,
-		},
-		// TODO: Add test for the failure mode of multiple Linodes returned for the
-		// same label. The API should prevent this but we must handle it gracefully on
-		// the Kubernetes side.
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			tc.f(t, client)
-		})
-	}
-}
+	client := NewMockLinodeClient(ctrl)
+	instances := newInstances(client)
 
-func testNodeAddressesFound(t *testing.T, client *linodego.Client) {
-	expectedAddresses := []v1.NodeAddress{
-		{
-			Type:    v1.NodeHostName,
-			Address: "test-instance",
-		},
-		{
-			Type:    v1.NodeExternalIP,
-			Address: "45.79.101.25",
-		},
-		{
-			Type:    v1.NodeInternalIP,
-			Address: "192.168.133.65",
-		},
-	}
+	t.Run("should propagate generic api error", func(t *testing.T) {
+		providerID := providerIDPrefix + "123"
+		expectedErr := errors.New("some error")
+		client.EXPECT().GetInstance(gomock.Any(), 123).Times(1).Return(nil, expectedErr)
 
-	instances := newInstances(client)
+		exists, err := instances.InstanceExistsByProviderID(ctx, providerID)
+		assert.ErrorIs(t, err, expectedErr)
+		assert.False(t, exists)
+	})
 
-	addresses, err := instances.NodeAddresses(context.TODO(), "test-instance")
-	if !reflect.DeepEqual(addresses, expectedAddresses) {
-		t.Errorf("unexpected node addresses. got: %v want: %v", addresses, expectedAddresses)
-	}
+	t.Run("should return false if linode does not exist", func(t *testing.T) {
+		providerID := providerIDPrefix + "123"
+		client.EXPECT().GetInstance(gomock.Any(), 123).Times(1).Return(nil, &linodego.Error{
+			Code: http.StatusNotFound,
+		})
 
-	if err != nil {
-		t.Errorf("unexpected err, expected nil. got: %v", err)
-	}
+		exists, err := instances.InstanceExistsByProviderID(ctx, providerID)
+		assert.NoError(t, err)
+		assert.False(t, exists)
+	})
+
+	t.Run("should return true if linode exists", func(t *testing.T) {
+		providerID := providerIDPrefix + "123"
+		client.EXPECT().GetInstance(gomock.Any(), 123).Times(1).Return(&linodego.Instance{
+			ID:     123,
+			Label:  "mock",
+			Region: "us-east",
+			Type:   "g6-standard-2",
+		}, nil)
+
+		exists, err := instances.InstanceExistsByProviderID(ctx, providerID)
+		assert.NoError(t, err)
+		assert.True(t, exists)
+	})
 }
 
-func testNodeAddressesNotFound(t *testing.T, client *linodego.Client) {
-	instances := newInstances(client)
+func TestNodeAddresses(t *testing.T) {
+	ctx := context.TODO()
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
 
-	_, err := instances.NodeAddresses(context.TODO(), "non-existant")
-	if err == nil {
-		t.Errorf("Expected error for nonexistent Linode label")
-	}
-}
+	client := NewMockLinodeClient(ctrl)
+	instances := newInstances(client)
 
-func testNodeAddressesByProviderID(t *testing.T, client *linodego.Client) {
-	testCases := []struct {
-		name string
-		f    func(*testing.T, *linodego.Client)
-	}{
-		{
-			name: "Node Addresses by ProviderID Found",
-			f:    testNodeAddressesByProviderIDFound,
-		},
-		{
-			name: "Node Addresses by ProviderID Bad LinodeID",
-			f:    testNodeAddressesByProviderIDBadLinodeID,
-		},
-		{
-			name: "Node Addresses by ProviderID Bad ProviderName",
-			f:    testNodeAddressesByProviderIDBadProviderName,
-		},
-		{
-			name: "Node Addresses by ProviderID Bad Format",
-			f:    testNodeAddressesByProviderIDBadProviderIDFormat,
-		},
-		{
-			name: "Node Addresses by ProviderID Empty ProviderID",
-			f:    testNodeAddressesByProviderIDEmptyProviderID,
-		},
-		// TODO: Add a test for a Linode which returns no IP addresses. This should be
-		// prevented by the API but we must handle it gracefully on the Kubernetes
-		// side.
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			tc.f(t, client)
+	t.Run("errors when linode does not exist", func(t *testing.T) {
+		name := "does-not-exist"
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return([]linodego.Instance{}, nil)
+
+		addrs, err := instances.NodeAddresses(ctx, types.NodeName(name))
+		assert.ErrorIs(t, err, cloudprovider.InstanceNotFound)
+		assert.Nil(t, addrs)
+	})
+
+	t.Run("errors when linode does not have any ips", func(t *testing.T) {
+		id := 29392
+		name := "an-instance"
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return([]linodego.Instance{
+			{ID: id, Label: name},
+		}, nil)
+		client.EXPECT().GetInstanceIPAddresses(gomock.Any(), id).Times(1).Return(&linodego.InstanceIPAddressResponse{
+			IPv4: &linodego.InstanceIPv4Response{
+				Public:  []*linodego.InstanceIP{},
+				Private: []*linodego.InstanceIP{},
+			},
+		}, nil)
+
+		addrs, err := instances.NodeAddresses(ctx, types.NodeName(name))
+		assert.Error(t, err, instanceNoIPAddressesError{id})
+		assert.Nil(t, addrs)
+	})
+
+	t.Run("should return addresses when linode is found", func(t *testing.T) {
+		id := 123
+		name := "mock-instance"
+		publicIPv4 := "45.76.101.25"
+		privateIPv4 := "192.168.133.65"
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return([]linodego.Instance{
+			{ID: id, Label: name},
+		}, nil)
+		client.EXPECT().GetInstanceIPAddresses(gomock.Any(), id).Times(1).Return(&linodego.InstanceIPAddressResponse{
+			IPv4: &linodego.InstanceIPv4Response{
+				Public:  []*linodego.InstanceIP{{Address: publicIPv4}},
+				Private: []*linodego.InstanceIP{{Address: privateIPv4}},
+			},
+		}, nil)
+
+		addrs, err := instances.NodeAddresses(ctx, types.NodeName(name))
+		assert.NoError(t, err)
+		assert.Equal(t, addrs, []v1.NodeAddress{
+			{
+				Type:    v1.NodeHostName,
+				Address: name,
+			},
+			{
+				Type:    v1.NodeExternalIP,
+				Address: publicIPv4,
+			},
+			{
+				Type:    v1.NodeInternalIP,
+				Address: privateIPv4,
+			},
 		})
-	}
+	})
 }
 
-func testNodeAddressesByProviderIDFound(t *testing.T, client *linodego.Client) {
-	expectedAddresses := []v1.NodeAddress{
-		{
-			Type:    v1.NodeHostName,
-			Address: "test-instance",
-		},
-		{
-			Type:    v1.NodeExternalIP,
-			Address: "45.79.101.25",
-		},
-		{
-			Type:    v1.NodeInternalIP,
-			Address: "192.168.133.65",
-		},
-	}
+func TestNodeAddressesByProviderID(t *testing.T) {
+	ctx := context.TODO()
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
 
+	client := NewMockLinodeClient(ctrl)
 	instances := newInstances(client)
 
-	addresses, err := instances.NodeAddressesByProviderID(context.TODO(), "linode://123")
-	if !reflect.DeepEqual(addresses, expectedAddresses) {
-		t.Errorf("unexpected node addresses. got: %v want: %v", addresses, expectedAddresses)
-	}
-
-	if err != nil {
-		t.Errorf("unexpected err, expected nil. got: %v", err)
-	}
+	t.Run("fails on malformed providerID", func(t *testing.T) {
+		providerID := "bogus://bogus"
+		addrs, err := instances.NodeAddressesByProviderID(ctx, providerID)
+
+		assert.ErrorIs(t, err, invalidProviderIDError{providerID})
+		assert.Nil(t, addrs)
+	})
+
+	t.Run("fails on non-numeric providerID", func(t *testing.T) {
+		providerID := providerIDPrefix + "abc"
+		addrs, err := instances.NodeAddressesByProviderID(ctx, providerID)
+
+		assert.ErrorIs(t, err, invalidProviderIDError{providerID})
+		assert.Nil(t, addrs)
+	})
+
+	t.Run("fails when linode does not exist", func(t *testing.T) {
+		id := 456302
+		providerID := providerIDPrefix + strconv.Itoa(id)
+		getInstanceErr := &linodego.Error{Code: http.StatusNotFound}
+		client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(nil, getInstanceErr)
+		addrs, err := instances.NodeAddressesByProviderID(ctx, providerID)
+
+		assert.ErrorIs(t, err, getInstanceErr)
+		assert.Nil(t, addrs)
+	})
+
+	t.Run("should return addresses when linode is found", func(t *testing.T) {
+		id := 192910
+		name := "my-instance"
+		providerID := providerIDPrefix + strconv.Itoa(id)
+		publicIPv4 := "32.74.121.25"
+		privateIPv4 := "192.168.121.42"
+		client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(&linodego.Instance{
+			ID: id, Label: name,
+		}, nil)
+		client.EXPECT().GetInstanceIPAddresses(gomock.Any(), id).Times(1).Return(&linodego.InstanceIPAddressResponse{
+			IPv4: &linodego.InstanceIPv4Response{
+				Public:  []*linodego.InstanceIP{{Address: publicIPv4}},
+				Private: []*linodego.InstanceIP{{Address: privateIPv4}},
+			},
+		}, nil)
+
+		addrs, err := instances.NodeAddressesByProviderID(ctx, providerID)
+
+		assert.NoError(t, err)
+		assert.Equal(t, addrs, []v1.NodeAddress{
+			{
+				Type:    v1.NodeHostName,
+				Address: name,
+			},
+			{
+				Type:    v1.NodeExternalIP,
+				Address: publicIPv4,
+			},
+			{
+				Type:    v1.NodeInternalIP,
+				Address: privateIPv4,
+			},
+		})
+	})
 }
 
-func testNodeAddressesByProviderIDBadLinodeID(t *testing.T, client *linodego.Client) {
-	instances := newInstances(client)
-
-	_, err := instances.NodeAddressesByProviderID(context.TODO(), "linode://456")
-	if err == nil {
-		t.Errorf("Expected error for nonexistent LinodeID")
-	}
-}
+func TestInstanceID(t *testing.T) {
+	ctx := context.TODO()
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
 
-func testNodeAddressesByProviderIDBadProviderName(t *testing.T, client *linodego.Client) {
+	client := NewMockLinodeClient(ctrl)
 	instances := newInstances(client)
 
-	_, err := instances.NodeAddressesByProviderID(context.TODO(), "thecloud://123")
-	if err == nil {
-		t.Errorf("Expected error for bad ProviderID")
-	}
+	t.Run("fails when instance not found", func(t *testing.T) {
+		name := "non-existant-instance"
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return([]linodego.Instance{}, nil)
+		instanceID, err := instances.InstanceID(ctx, types.NodeName(name))
+
+		assert.ErrorIs(t, err, cloudprovider.InstanceNotFound)
+		assert.Zero(t, instanceID)
+	})
+
+	t.Run("succeeds when instance exists", func(t *testing.T) {
+		id := 129289
+		name := "existing-instance"
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return([]linodego.Instance{
+			{ID: id, Label: name},
+		}, nil)
+		instanceID, err := instances.InstanceID(ctx, types.NodeName(name))
+
+		assert.NoError(t, err)
+		assert.Equal(t, strconv.Itoa(id), instanceID)
+	})
 }
 
-func testNodeAddressesByProviderIDBadProviderIDFormat(t *testing.T, client *linodego.Client) {
-	instances := newInstances(client)
+func TestInstanceType(t *testing.T) {
+	ctx := context.TODO()
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
 
-	_, err := instances.NodeAddressesByProviderID(context.TODO(), "linode123")
-	if err == nil {
-		t.Errorf("Expected error for bad ProviderID format")
-	}
-}
-
-func testNodeAddressesByProviderIDEmptyProviderID(t *testing.T, client *linodego.Client) {
+	client := NewMockLinodeClient(ctrl)
 	instances := newInstances(client)
 
-	_, err := instances.NodeAddressesByProviderID(context.TODO(), "")
-	if err == nil {
-		t.Errorf("Expected error for empty ProviderID")
-	}
+	t.Run("fails when instance not found", func(t *testing.T) {
+		name := "bogus"
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return([]linodego.Instance{}, nil)
+		instanceType, err := instances.InstanceType(ctx, types.NodeName(name))
+
+		assert.ErrorIs(t, err, cloudprovider.InstanceNotFound)
+		assert.Empty(t, instanceType)
+	})
+
+	t.Run("succeeds when instance exists", func(t *testing.T) {
+		id := 39399
+		name := "my-nanode"
+		linodeType := "g6-standard-1"
+		filter := linodeFilterListOptions(name)
+		client.EXPECT().ListInstances(gomock.Any(), filter).Times(1).Return([]linodego.Instance{
+			{ID: id, Label: name, Type: linodeType},
+		}, nil)
+		instanceType, err := instances.InstanceType(ctx, types.NodeName(name))
+
+		assert.NoError(t, err)
+		assert.Equal(t, linodeType, instanceType)
+	})
 }
 
-func testInstanceID(t *testing.T, client *linodego.Client) {
-	instances := newInstances(client)
-	id, err := instances.InstanceID(context.TODO(), "test-instance")
-	if err != nil {
-		t.Errorf("expected nil error, got: %v", err)
-	}
-	if id != "123" {
-		t.Errorf("expected id 123, got %s", id)
-	}
-
-	_, err = instances.InstanceID(context.TODO(), "test-linode")
-	if err == nil {
-		t.Errorf("expected error, got: nil")
-	}
-
-}
+func TestInstanceTypeByProviderID(t *testing.T) {
+	ctx := context.TODO()
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
 
-func testInstanceType(t *testing.T, client *linodego.Client) {
+	client := NewMockLinodeClient(ctrl)
 	instances := newInstances(client)
 
-	insType, err := instances.InstanceType(context.TODO(), "test-instance")
-	if err != nil {
-		t.Errorf("expected nil error, got: %v", err)
-	}
-	if insType != "g6-standard-2" {
-		t.Errorf("expected id g6-standard-2, got %s", insType)
-	}
+	t.Run("fails when instance not found", func(t *testing.T) {
+		id := 39281
+		providerID := providerIDPrefix + strconv.Itoa(id)
+		client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(nil, linodego.Error{Code: http.StatusNotFound})
+		instanceType, err := instances.InstanceTypeByProviderID(ctx, providerID)
+
+		assert.Error(t, err)
+		assert.Empty(t, instanceType)
+	})
+
+	t.Run("succeeds when instance exists", func(t *testing.T) {
+		id := 39281
+		providerID := providerIDPrefix + strconv.Itoa(id)
+		linodeType := "g6-standard-2"
+		client.EXPECT().GetInstance(gomock.Any(), id).Times(1).Return(&linodego.Instance{
+			ID: id, Label: "some-linode", Type: linodeType,
+		}, nil)
+		instanceType, err := instances.InstanceTypeByProviderID(ctx, providerID)
+
+		assert.NoError(t, err)
+		assert.Equal(t, linodeType, instanceType)
+	})
 }
 
-func testInstanceTypeByProviderID(t *testing.T, client *linodego.Client) {
-	instances := newInstances(client)
-
-	insType, err := instances.InstanceTypeByProviderID(context.TODO(), "linode://123")
-	if err != nil {
-		t.Errorf("expected nil error, got: %v", err)
-	}
-	if insType != "g6-standard-2" {
-		t.Errorf("expected id g6-standard-2, got %s", insType)
-	}
+func TestCurrentNodeName(t *testing.T) {
+	instances := newInstances(nil)
+	hostname := "lke2901-2920fa-3838f-28a21"
+	nodeName, err := instances.CurrentNodeName(context.TODO(), hostname)
+	assert.NoError(t, err)
+	assert.Equal(t, types.NodeName(hostname), nodeName)
 }
 
-func testInstanceExistsByProviderID(t *testing.T, client *linodego.Client) {
-	instances := newInstances(client)
-
-	found, err := instances.InstanceExistsByProviderID(context.TODO(), "linode://123")
-	if err != nil {
-		t.Errorf("expected nil error, got: %v", err)
-	}
-	if !found {
-		t.Errorf("expected found true, got %v", found)
-	}
-
-	found, err = instances.InstanceExistsByProviderID(context.TODO(), "linode://12345")
-	if err != nil {
-		t.Errorf("expected nil error, got: %v", err)
-	}
-	if found {
-		t.Errorf("expected found false, got %v", found)
-	}
+func TestAddSSHKeyToAllInstances(t *testing.T) {
+	instances := newInstances(nil)
+	err := instances.AddSSHKeyToAllInstances(context.TODO(), "root", []byte{})
+	assert.ErrorIs(t, err, cloudprovider.NotImplemented)
+}
 
+func TestInstanceShutdownByProviderID(t *testing.T) {
+	instances := newInstances(nil)
+	_, err := instances.InstanceShutdownByProviderID(context.TODO(), providerIDPrefix+"12345")
+	assert.ErrorIs(t, err, cloudprovider.NotImplemented)
 }
diff --git a/cloud/linode/mock_client_test.go b/cloud/linode/mock_client_test.go
new file mode 100644
index 0000000..abd6919
--- /dev/null
+++ b/cloud/linode/mock_client_test.go
@@ -0,0 +1,214 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/linode/linode-cloud-controller-manager/cloud/linode (interfaces: LinodeClient)
+
+// Package linode is a generated GoMock package.
+package linode
+
+import (
+	context "context"
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	linodego "github.com/linode/linodego"
+)
+
+// MockLinodeClient is a mock of LinodeClient interface.
+type MockLinodeClient struct {
+	ctrl     *gomock.Controller
+	recorder *MockLinodeClientMockRecorder
+}
+
+// MockLinodeClientMockRecorder is the mock recorder for MockLinodeClient.
+type MockLinodeClientMockRecorder struct {
+	mock *MockLinodeClient
+}
+
+// NewMockLinodeClient creates a new mock instance.
+func NewMockLinodeClient(ctrl *gomock.Controller) *MockLinodeClient {
+	mock := &MockLinodeClient{ctrl: ctrl}
+	mock.recorder = &MockLinodeClientMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockLinodeClient) EXPECT() *MockLinodeClientMockRecorder {
+	return m.recorder
+}
+
+// CreateNodeBalancer mocks base method.
+func (m *MockLinodeClient) CreateNodeBalancer(arg0 context.Context, arg1 linodego.NodeBalancerCreateOptions) (*linodego.NodeBalancer, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateNodeBalancer", arg0, arg1)
+	ret0, _ := ret[0].(*linodego.NodeBalancer)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateNodeBalancer indicates an expected call of CreateNodeBalancer.
+func (mr *MockLinodeClientMockRecorder) CreateNodeBalancer(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNodeBalancer", reflect.TypeOf((*MockLinodeClient)(nil).CreateNodeBalancer), arg0, arg1)
+}
+
+// CreateNodeBalancerConfig mocks base method.
+func (m *MockLinodeClient) CreateNodeBalancerConfig(arg0 context.Context, arg1 int, arg2 linodego.NodeBalancerConfigCreateOptions) (*linodego.NodeBalancerConfig, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateNodeBalancerConfig", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*linodego.NodeBalancerConfig)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateNodeBalancerConfig indicates an expected call of CreateNodeBalancerConfig.
+func (mr *MockLinodeClientMockRecorder) CreateNodeBalancerConfig(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNodeBalancerConfig", reflect.TypeOf((*MockLinodeClient)(nil).CreateNodeBalancerConfig), arg0, arg1, arg2)
+}
+
+// DeleteNodeBalancer mocks base method.
+func (m *MockLinodeClient) DeleteNodeBalancer(arg0 context.Context, arg1 int) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteNodeBalancer", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeleteNodeBalancer indicates an expected call of DeleteNodeBalancer.
+func (mr *MockLinodeClientMockRecorder) DeleteNodeBalancer(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodeBalancer", reflect.TypeOf((*MockLinodeClient)(nil).DeleteNodeBalancer), arg0, arg1)
+}
+
+// DeleteNodeBalancerConfig mocks base method.
+func (m *MockLinodeClient) DeleteNodeBalancerConfig(arg0 context.Context, arg1, arg2 int) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteNodeBalancerConfig", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeleteNodeBalancerConfig indicates an expected call of DeleteNodeBalancerConfig.
+func (mr *MockLinodeClientMockRecorder) DeleteNodeBalancerConfig(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodeBalancerConfig", reflect.TypeOf((*MockLinodeClient)(nil).DeleteNodeBalancerConfig), arg0, arg1, arg2)
+}
+
+// GetInstance mocks base method.
+func (m *MockLinodeClient) GetInstance(arg0 context.Context, arg1 int) (*linodego.Instance, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetInstance", arg0, arg1)
+	ret0, _ := ret[0].(*linodego.Instance)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetInstance indicates an expected call of GetInstance.
+func (mr *MockLinodeClientMockRecorder) GetInstance(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstance", reflect.TypeOf((*MockLinodeClient)(nil).GetInstance), arg0, arg1)
+}
+
+// GetInstanceIPAddresses mocks base method.
+func (m *MockLinodeClient) GetInstanceIPAddresses(arg0 context.Context, arg1 int) (*linodego.InstanceIPAddressResponse, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetInstanceIPAddresses", arg0, arg1)
+	ret0, _ := ret[0].(*linodego.InstanceIPAddressResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetInstanceIPAddresses indicates an expected call of GetInstanceIPAddresses.
+func (mr *MockLinodeClientMockRecorder) GetInstanceIPAddresses(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceIPAddresses", reflect.TypeOf((*MockLinodeClient)(nil).GetInstanceIPAddresses), arg0, arg1)
+}
+
+// GetNodeBalancer mocks base method.
+func (m *MockLinodeClient) GetNodeBalancer(arg0 context.Context, arg1 int) (*linodego.NodeBalancer, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetNodeBalancer", arg0, arg1)
+	ret0, _ := ret[0].(*linodego.NodeBalancer)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetNodeBalancer indicates an expected call of GetNodeBalancer.
+func (mr *MockLinodeClientMockRecorder) GetNodeBalancer(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeBalancer", reflect.TypeOf((*MockLinodeClient)(nil).GetNodeBalancer), arg0, arg1)
+}
+
+// ListInstances mocks base method.
+func (m *MockLinodeClient) ListInstances(arg0 context.Context, arg1 *linodego.ListOptions) ([]linodego.Instance, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListInstances", arg0, arg1)
+	ret0, _ := ret[0].([]linodego.Instance)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListInstances indicates an expected call of ListInstances.
+func (mr *MockLinodeClientMockRecorder) ListInstances(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstances", reflect.TypeOf((*MockLinodeClient)(nil).ListInstances), arg0, arg1)
+}
+
+// ListNodeBalancerConfigs mocks base method.
+func (m *MockLinodeClient) ListNodeBalancerConfigs(arg0 context.Context, arg1 int, arg2 *linodego.ListOptions) ([]linodego.NodeBalancerConfig, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListNodeBalancerConfigs", arg0, arg1, arg2)
+	ret0, _ := ret[0].([]linodego.NodeBalancerConfig)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListNodeBalancerConfigs indicates an expected call of ListNodeBalancerConfigs.
+func (mr *MockLinodeClientMockRecorder) ListNodeBalancerConfigs(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNodeBalancerConfigs", reflect.TypeOf((*MockLinodeClient)(nil).ListNodeBalancerConfigs), arg0, arg1, arg2)
+}
+
+// ListNodeBalancers mocks base method.
+func (m *MockLinodeClient) ListNodeBalancers(arg0 context.Context, arg1 *linodego.ListOptions) ([]linodego.NodeBalancer, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListNodeBalancers", arg0, arg1)
+	ret0, _ := ret[0].([]linodego.NodeBalancer)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListNodeBalancers indicates an expected call of ListNodeBalancers.
+func (mr *MockLinodeClientMockRecorder) ListNodeBalancers(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNodeBalancers", reflect.TypeOf((*MockLinodeClient)(nil).ListNodeBalancers), arg0, arg1)
+}
+
+// RebuildNodeBalancerConfig mocks base method.
+func (m *MockLinodeClient) RebuildNodeBalancerConfig(arg0 context.Context, arg1, arg2 int, arg3 linodego.NodeBalancerConfigRebuildOptions) (*linodego.NodeBalancerConfig, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RebuildNodeBalancerConfig", arg0, arg1, arg2, arg3)
+	ret0, _ := ret[0].(*linodego.NodeBalancerConfig)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// RebuildNodeBalancerConfig indicates an expected call of RebuildNodeBalancerConfig.
+func (mr *MockLinodeClientMockRecorder) RebuildNodeBalancerConfig(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RebuildNodeBalancerConfig", reflect.TypeOf((*MockLinodeClient)(nil).RebuildNodeBalancerConfig), arg0, arg1, arg2, arg3)
+}
+
+// UpdateNodeBalancer mocks base method.
+func (m *MockLinodeClient) UpdateNodeBalancer(arg0 context.Context, arg1 int, arg2 linodego.NodeBalancerUpdateOptions) (*linodego.NodeBalancer, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateNodeBalancer", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*linodego.NodeBalancer)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UpdateNodeBalancer indicates an expected call of UpdateNodeBalancer.
+func (mr *MockLinodeClientMockRecorder) UpdateNodeBalancer(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateNodeBalancer", reflect.TypeOf((*MockLinodeClient)(nil).UpdateNodeBalancer), arg0, arg1, arg2)
+}