Repository: linode/linode-cloud-controller-manager
Pull request #184 (2024-03-01 15:29:31)

feat: add route-controller to linode ccm
**What type of PR is this?**
/kind feature

**What this PR does / why we need it:**
Cloud-controller-manager provides [interfaces](https://github.com/kubernetes/cloud-provider/blob/v0.23.17/cloud.go#L230-L240) to implement functionality of managing routes in cluster. For k8s clusters provisioned within VPC, we need to automatically add/remove podCIDRs allocated to nodes to the linode's interface configs so that the traffic can be routed within the VPC. This PR implements the Routes interfaces.

This PR also makes sure that if a node is a part of VPC, then VPC ip is shown as the node internal ip.
```
root@rah6-control-plane-bjwwd:~# k get nodes -o wide
NAME                       STATUS   ROLES           AGE    VERSION   INTERNAL-IP   EXTERNAL-IP      OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
rah6-control-plane-bjwwd   Ready    control-plane   14m    v1.29.2   10.0.0.2      172.232.1.230    Ubuntu 22.04.4 LTS   5.15.0-94-generic   containerd://1.7.2
rah6-md-0-zrzwr            Ready    <none>          12m    v1.29.2   10.0.0.4      172.233.209.90   Ubuntu 22.04.4 LTS   5.15.0-94-generic   containerd://1.7.2

root@rah6-control-plane-bjwwd:~# k get node rah6-control-plane-bjwwd -o yaml | yq .status.addresses
- address: rah6-control-plane-bjwwd
  type: Hostname
- address: 172.232.1.230
  type: ExternalIP
- address: 10.0.0.2
  type: InternalIP
- address: 192.168.132.88
  type: InternalIP
root@rah6-control-plane-bjwwd:~#

root@rah6-control-plane-bjwwd:~# k get node rah6-control-plane-bjwwd -o yaml | yq .spec.providerID
linode://55914087
root@rah6-control-plane-bjwwd:~# k get node rah6-control-plane-bjwwd -o yaml | yq .spec.podCIDRs
- 10.192.0.0/24
root@rah6-control-plane-bjwwd:~# curl -s --header "Authorization: Bearer $LINODE_API_TOKEN" -X GET https://api.linode.com/v4/linode/instances/55914087/configs | yq .data[0].interfaces[].ip_ranges
null
["10.192.0.0/24"]
root@rah6-control-plane-bjwwd:~#

root@rah6-control-plane-bjwwd:~# k get node rah6-md-0-zrzwr -o yaml | yq .status.addresses
- address: rah6-md-0-zrzwr
  type: Hostname
- address: 172.233.209.90
  type: ExternalIP
- address: 10.0.0.4
  type: InternalIP
- address: 192.168.132.220
  type: InternalIP
root@rah6-control-plane-bjwwd:~# k get node rah6-md-0-zrzwr -o yaml | yq .spec.podCIDRs
- 10.192.3.0/24
root@rah6-control-plane-bjwwd:~# k get node rah6-md-0-zrzwr -o yaml | yq .spec.providerID
linode://55914178
root@rah6-control-plane-bjwwd:~# curl -s --header "Authorization: Bearer $LINODE_API_TOKEN" -X GET https://api.linode.com/v4/linode/instances/55914178/configs | yq .data[0].interfaces[].ip_ranges
null
["10.192.3.0/24"]
root@rah6-control-plane-bjwwd:~#
```

Key points:
* Implements routes interface
* Disabled by default and can be enabled with certain helm flags
* Uses cache to avoid hitting linode API too many times
* VPC ip set as node internal ip

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes # https://github.com/linode/linode-cloud-controller-manager/issues/183

**Special notes for your reviewer**:

**TODO**:
Add unittests

### General:

* [x] Have you removed all sensitive information, including but not limited to access keys and passwords?
* [x] Have you checked to ensure there aren't other open or closed [Pull Requests](../../pulls) for the same bug/feature/question?

### Pull Request Guidelines:

1. [x] Does your submission pass tests?
1. [ ] Have you added tests? 
1. [x] Are you addressing a single feature in this PR? 
1. [x] Are your commits atomic, addressing one change per commit?
1. [x] Are you following the conventions of the language? 
1. [x] Have you saved your large formatting changes for a different PR, so we can focus on your work?
1. [x] Have you explained your rationale for why this feature is needed? 
1. [x] Have you linked your PR to an [open issue](https://blog.github.com/2013-05-14-closing-issues-via-pull-requests/)



rahulait: Hitting https://github.com/linode/linodego/issues/462 with linodego. It doesn't affect the PR as the interface also gets deleted when a node is deleted and we don't need DeleteRoute() call to always work, but it would be nice if linodego is fixed.

Patch:
diff --git a/README.md b/README.md
index 2236b2b..b18e4dc 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,16 @@ Users can create CloudFirewall instances, supply their own rules and attach them
 **Note**<br/>
 If the user supplies a firewall-id, and later switches to using an ACL, the CCM will take over the CloudFirewall Instance. To avoid this, delete the service, and re-create it so the original CloudFirewall is left undisturbed.
 
+#### Routes
+When running k8s clusters within VPC, node specific podCIDRs need to be allowed on the VPC interface. Linode CCM comes with route-controller functionality which can be enabled for automatically adding/deleting routes on VPC interfaces. When installing CCM with helm, make sure to specify routeController settings.
 
+##### Example usage in values.yaml
+```yaml
+routeController:
+  vpcName: <name of VPC>
+  clusterCIDR: 10.0.0.0/8
+  configureCloudRoutes: true
+```
 
 ### Nodes
 Kubernetes Nodes can be configured with the following annotations.
diff --git a/cloud/linode/client/client.go b/cloud/linode/client/client.go
index 3b9bb74..e9d4b15 100644
--- a/cloud/linode/client/client.go
+++ b/cloud/linode/client/client.go
@@ -16,6 +16,11 @@ type Client interface {
 	ListInstances(context.Context, *linodego.ListOptions) ([]linodego.Instance, error)
 	GetInstanceIPAddresses(context.Context, int) (*linodego.InstanceIPAddressResponse, error)
 
+	ListInstanceConfigs(context.Context, int, *linodego.ListOptions) ([]linodego.InstanceConfig, error)
+	UpdateInstanceConfigInterface(context.Context, int, int, int, linodego.InstanceConfigInterfaceUpdateOptions) (*linodego.InstanceConfigInterface, error)
+
+	ListVPCs(context.Context, *linodego.ListOptions) ([]linodego.VPC, 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)
diff --git a/cloud/linode/cloud.go b/cloud/linode/cloud.go
index 123c203..5e9b043 100644
--- a/cloud/linode/cloud.go
+++ b/cloud/linode/cloud.go
@@ -25,14 +25,17 @@ const (
 // We expect it to be initialized with flags external to this package, likely in
 // main.go
 var Options struct {
-	KubeconfigFlag *pflag.Flag
-	LinodeGoDebug  bool
+	KubeconfigFlag        *pflag.Flag
+	LinodeGoDebug         bool
+	EnableRouteController bool
+	VPCName               string
 }
 
 type linodeCloud struct {
 	client        client.Client
 	instances     cloudprovider.InstancesV2
 	loadbalancers cloudprovider.LoadBalancer
+	routes        cloudprovider.Routes
 }
 
 func init() {
@@ -67,12 +70,19 @@ func newCloud() (cloudprovider.Interface, error) {
 		linodeClient.SetDebug(true)
 	}
 
-	// Return struct that satisfies cloudprovider.Interface
-	return &linodeCloud{
+	routes, err := newRoutes(linodeClient)
+	if err != nil {
+		return nil, fmt.Errorf("routes client was not created successfully: %w", err)
+	}
+
+	// create struct that satisfies cloudprovider.Interface
+	lcloud := &linodeCloud{
 		client:        linodeClient,
 		instances:     newInstances(linodeClient),
 		loadbalancers: newLoadbalancers(linodeClient, region),
-	}, nil
+		routes:        routes,
+	}
+	return lcloud, nil
 }
 
 func (c *linodeCloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stopCh <-chan struct{}) {
@@ -109,6 +119,9 @@ func (c *linodeCloud) Clusters() (cloudprovider.Clusters, bool) {
 }
 
 func (c *linodeCloud) Routes() (cloudprovider.Routes, bool) {
+	if Options.EnableRouteController {
+		return c.routes, true
+	}
 	return nil, false
 }
 
diff --git a/cloud/linode/instances.go b/cloud/linode/instances.go
index da38ccb..ee02742 100644
--- a/cloud/linode/instances.go
+++ b/cloud/linode/instances.go
@@ -9,21 +9,72 @@ import (
 	"time"
 
 	"github.com/linode/linodego"
+	"golang.org/x/sync/errgroup"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	cloudprovider "k8s.io/cloud-provider"
+	"k8s.io/klog/v2"
 
 	"github.com/linode/linode-cloud-controller-manager/cloud/linode/client"
 	"github.com/linode/linode-cloud-controller-manager/sentry"
 )
 
+type nodeIP struct {
+	ip     string
+	ipType v1.NodeAddressType
+}
+
+type linodeInstance struct {
+	instance *linodego.Instance
+	ips      []nodeIP
+}
+
 type nodeCache struct {
 	sync.RWMutex
-	nodes      map[int]*linodego.Instance
+	nodes      map[int]linodeInstance
 	lastUpdate time.Time
 	ttl        time.Duration
 }
 
+// getInstanceIPv4Addresses returns all ipv4 addresses configured on a linode.
+func (nc *nodeCache) getInstanceIPv4Addresses(ctx context.Context, id int, client client.Client) ([]nodeIP, error) {
+	// Retrieve ipaddresses for the linode
+	addresses, err := client.GetInstanceIPAddresses(ctx, id)
+	if err != nil {
+		return nil, err
+	}
+
+	var ips []nodeIP
+	if len(addresses.IPv4.Public) != 0 {
+		for _, ip := range addresses.IPv4.Public {
+			ips = append(ips, nodeIP{ip: ip.Address, ipType: v1.NodeExternalIP})
+		}
+	}
+
+	// Retrieve instance configs for the linode
+	configs, err := client.ListInstanceConfigs(ctx, id, &linodego.ListOptions{})
+	if err != nil || len(configs) == 0 {
+		return nil, err
+	}
+
+	// Iterate over interfaces in config and find VPC specific ips
+	for _, iface := range configs[0].Interfaces {
+		if iface.VPCID != nil && iface.IPv4.VPC != "" {
+			ips = append(ips, nodeIP{ip: iface.IPv4.VPC, ipType: v1.NodeInternalIP})
+		}
+	}
+
+	// NOTE: We specifically store VPC ips first so that if they exist, they are
+	//       used as internal ip for the nodes than the private ip
+	if len(addresses.IPv4.Private) != 0 {
+		for _, ip := range addresses.IPv4.Private {
+			ips = append(ips, nodeIP{ip: ip.Address, ipType: v1.NodeInternalIP})
+		}
+	}
+
+	return ips, nil
+}
+
 // refreshInstances conditionally loads all instances from the Linode API and caches them.
 // It does not refresh if the last update happened less than `nodeCache.ttl` ago.
 func (nc *nodeCache) refreshInstances(ctx context.Context, client client.Client) error {
@@ -38,11 +89,32 @@ func (nc *nodeCache) refreshInstances(ctx context.Context, client client.Client)
 	if err != nil {
 		return err
 	}
-	nc.nodes = make(map[int]*linodego.Instance)
+
+	nc.nodes = make(map[int]linodeInstance, len(instances))
+
+	mtx := sync.Mutex{}
+	g := new(errgroup.Group)
 	for _, instance := range instances {
 		instance := instance
-		nc.nodes[instance.ID] = &instance
+		g.Go(func() error {
+			addresses, err := nc.getInstanceIPv4Addresses(ctx, instance.ID, client)
+			if err != nil {
+				klog.Errorf("Failed fetching ip addresses for instance id %d. Error: %s", instance.ID, err.Error())
+				return err
+			}
+			// take lock on map so that concurrent writes are safe
+			mtx.Lock()
+			defer mtx.Unlock()
+			node := linodeInstance{instance: &instance, ips: addresses}
+			nc.nodes[instance.ID] = node
+			return nil
+		})
 	}
+
+	if err := g.Wait(); err != nil {
+		return err
+	}
+
 	nc.lastUpdate = time.Now()
 
 	return nil
@@ -61,9 +133,10 @@ func newInstances(client client.Client) *instances {
 			timeout = t
 		}
 	}
+	klog.V(3).Infof("TTL for nodeCache set to %d", timeout)
 
 	return &instances{client, &nodeCache{
-		nodes: make(map[int]*linodego.Instance),
+		nodes: make(map[int]linodeInstance, 0),
 		ttl:   time.Duration(timeout) * time.Second,
 	}}
 }
@@ -80,8 +153,8 @@ func (i *instances) linodeByName(nodeName types.NodeName) (*linodego.Instance, e
 	i.nodeCache.RLock()
 	defer i.nodeCache.RUnlock()
 	for _, node := range i.nodeCache.nodes {
-		if node.Label == string(nodeName) {
-			return node, nil
+		if node.instance.Label == string(nodeName) {
+			return node.instance, nil
 		}
 	}
 
@@ -91,11 +164,24 @@ func (i *instances) linodeByName(nodeName types.NodeName) (*linodego.Instance, e
 func (i *instances) linodeByID(id int) (*linodego.Instance, error) {
 	i.nodeCache.RLock()
 	defer i.nodeCache.RUnlock()
-	instance, ok := i.nodeCache.nodes[id]
+	linodeInstance, ok := i.nodeCache.nodes[id]
 	if !ok {
 		return nil, cloudprovider.InstanceNotFound
 	}
-	return instance, nil
+	return linodeInstance.instance, nil
+}
+
+// listAllInstances returns all instances in nodeCache
+func (i *instances) listAllInstances(ctx context.Context) ([]linodego.Instance, error) {
+	if err := i.nodeCache.refreshInstances(ctx, i.client); err != nil {
+		return nil, err
+	}
+
+	instances := []linodego.Instance{}
+	for _, linodeInstance := range i.nodeCache.nodes {
+		instances = append(instances, *linodeInstance.instance)
+	}
+	return instances, nil
 }
 
 func (i *instances) lookupLinode(ctx context.Context, node *v1.Node) (*linodego.Instance, error) {
@@ -162,7 +248,13 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloud
 		return nil, err
 	}
 
-	if len(linode.IPv4) == 0 {
+	ips, err := i.getLinodeIPv4Addresses(ctx, node)
+	if err != nil {
+		sentry.CaptureError(ctx, err)
+		return nil, err
+	}
+
+	if len(ips) == 0 {
 		err := instanceNoIPAddressesError{linode.ID}
 		sentry.CaptureError(ctx, err)
 		return nil, err
@@ -170,12 +262,8 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloud
 
 	addresses := []v1.NodeAddress{{Type: v1.NodeHostName, Address: linode.Label}}
 
-	for _, ip := range linode.IPv4 {
-		ipType := v1.NodeExternalIP
-		if ip.IsPrivate() {
-			ipType = v1.NodeInternalIP
-		}
-		addresses = append(addresses, v1.NodeAddress{Type: ipType, Address: ip.String()})
+	for _, ip := range ips {
+		addresses = append(addresses, v1.NodeAddress{Type: ip.ipType, Address: ip.ip})
 	}
 
 	// note that Zone is omitted as it's not a thing in Linode
@@ -188,3 +276,23 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloud
 
 	return meta, nil
 }
+
+func (i *instances) getLinodeIPv4Addresses(ctx context.Context, node *v1.Node) ([]nodeIP, error) {
+	ctx = sentry.SetHubOnContext(ctx)
+	instance, err := i.lookupLinode(ctx, node)
+	if err != nil {
+		sentry.CaptureError(ctx, err)
+		return nil, err
+	}
+
+	i.nodeCache.RLock()
+	defer i.nodeCache.RUnlock()
+	linodeInstance, ok := i.nodeCache.nodes[instance.ID]
+	if !ok || len(linodeInstance.ips) == 0 {
+		err := instanceNoIPAddressesError{instance.ID}
+		sentry.CaptureError(ctx, err)
+		return nil, err
+	}
+
+	return linodeInstance.ips, nil
+}
diff --git a/cloud/linode/route_controller.go b/cloud/linode/route_controller.go
new file mode 100644
index 0000000..ee2d352
--- /dev/null
+++ b/cloud/linode/route_controller.go
@@ -0,0 +1,250 @@
+package linode
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/linode/linodego"
+	"golang.org/x/sync/errgroup"
+	"golang.org/x/exp/slices"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	cloudprovider "k8s.io/cloud-provider"
+	"k8s.io/klog/v2"
+
+	"github.com/linode/linode-cloud-controller-manager/cloud/linode/client"
+)
+
+type routeCache struct {
+	sync.RWMutex
+	routes     map[int][]linodego.InstanceConfig
+	lastUpdate time.Time
+	ttl        time.Duration
+}
+
+func (rc *routeCache) refreshRoutes(ctx context.Context, client client.Client) error {
+	rc.Lock()
+	defer rc.Unlock()
+
+	if time.Since(rc.lastUpdate) < rc.ttl {
+		return nil
+	}
+
+	instances, err := client.ListInstances(ctx, nil)
+	if err != nil {
+		return err
+	}
+
+	rc.routes = make(map[int][]linodego.InstanceConfig, len(instances))
+
+	mtx := sync.Mutex{}
+	g := new(errgroup.Group)
+	for _, instance := range instances {
+		id := instance.ID
+		g.Go(func() error {
+			configs, err := client.ListInstanceConfigs(ctx, id, &linodego.ListOptions{})
+			if err != nil {
+				klog.Errorf("Failed fetching instance configs for instance id %d. Error: %s", id, err.Error())
+				return err
+			}
+			// take lock on map so that concurrent writes are safe
+			mtx.Lock()
+			defer mtx.Unlock()
+			rc.routes[id] = configs
+			return nil
+		})
+	}
+
+	if err := g.Wait(); err != nil {
+		return err
+	}
+
+	rc.lastUpdate = time.Now()
+	return nil
+}
+
+type routes struct {
+	vpcid      int
+	client     client.Client
+	instances  *instances
+	routeCache *routeCache
+}
+
+func newRoutes(client client.Client) (cloudprovider.Routes, error) {
+	timeout := 60
+	if raw, ok := os.LookupEnv("LINODE_ROUTES_CACHE_TTL"); ok {
+		if t, _ := strconv.Atoi(raw); t > 0 {
+			timeout = t
+		}
+	}
+	klog.V(3).Infof("TTL for routeCache set to %d", timeout)
+
+	vpcid, err := getVPCID(client, Options.VPCName)
+	if err != nil {
+		return nil, err
+	}
+
+	return &routes{
+		vpcid:     vpcid,
+		client:    client,
+		instances: newInstances(client),
+		routeCache: &routeCache{
+			routes: make(map[int][]linodego.InstanceConfig, 0),
+			ttl:    time.Duration(timeout) * time.Second,
+		},
+	}, nil
+}
+
+// instanceConfigsByID returns InstanceConfigs for given instance id
+func (r *routes) instanceConfigsByID(id int) ([]linodego.InstanceConfig, error) {
+	r.routeCache.RLock()
+	defer r.routeCache.RUnlock()
+	instanceConfigs, ok := r.routeCache.routes[id]
+	if !ok {
+		return nil, fmt.Errorf("no configs found for instance %d", id)
+	}
+	return instanceConfigs, nil
+}
+
+// getInstanceConfigs returns InstanceConfigs for given instance id
+// It refreshes routeCache if it has expired
+func (r *routes) getInstanceConfigs(ctx context.Context, id int) ([]linodego.InstanceConfig, error) {
+	if err := r.routeCache.refreshRoutes(ctx, r.client); err != nil {
+		return nil, err
+	}
+
+	return r.instanceConfigsByID(id)
+}
+
+// getInstanceFromName returns linode instance with given name if it exists
+func (r *routes) getInstanceFromName(ctx context.Context, name string) (*linodego.Instance, error) {
+	// create node object
+	node := &v1.Node{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+	}
+
+	// fetch instance with specified node name
+	instance, err := r.instances.lookupLinode(ctx, node)
+	if err != nil {
+		klog.Errorf("failed getting linode %s", name)
+		return nil, err
+	}
+	return instance, nil
+}
+
+// CreateRoute adds route's subnet to ip_ranges of target node's VPC interface
+func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint string, route *cloudprovider.Route) error {
+	instance, err := r.getInstanceFromName(ctx, string(route.TargetNode))
+	if err != nil {
+		return err
+	}
+
+	// fetch instance configs
+	configs, err := r.getInstanceConfigs(ctx, instance.ID)
+	if err != nil {
+		return err
+	}
+
+	// find VPC interface and add route to it
+	for _, iface := range configs[0].Interfaces {
+		if iface.VPCID == nil || r.vpcid != *iface.VPCID || iface.IPv4.VPC == "" {
+			continue
+		}
+
+		if slices.Contains(iface.IPRanges, route.DestinationCIDR) {
+			klog.V(4).Infof("Route already exists for node %s", route.TargetNode)
+			return nil
+		}
+
+		ipRanges := append(iface.IPRanges, route.DestinationCIDR)
+		interfaceUpdateOptions := linodego.InstanceConfigInterfaceUpdateOptions{
+			IPRanges: ipRanges,
+		}
+		resp, err := r.client.UpdateInstanceConfigInterface(ctx, instance.ID, configs[0].ID, iface.ID, interfaceUpdateOptions)
+		if err != nil {
+			return err
+		}
+		klog.V(4).Infof("Added routes for node %s. Current routes: %v", route.TargetNode, resp.IPRanges)
+		return nil
+	}
+
+	return fmt.Errorf("unable to add route %s for node %s. no valid interface found", route.DestinationCIDR, route.TargetNode)
+}
+
+// DeleteRoute removes route's subnet from ip_ranges of target node's VPC interface
+func (r *routes) DeleteRoute(ctx context.Context, clusterName string, route *cloudprovider.Route) error {
+	instance, err := r.getInstanceFromName(ctx, string(route.TargetNode))
+	if err != nil {
+		return err
+	}
+
+	configs, err := r.getInstanceConfigs(ctx, instance.ID)
+	if err != nil {
+		return err
+	}
+
+	for _, iface := range configs[0].Interfaces {
+		if iface.VPCID == nil || r.vpcid != *iface.VPCID || iface.IPv4.VPC == "" {
+			continue
+		}
+
+		ipRanges := []string{}
+		for _, configured_route := range iface.IPRanges {
+			if configured_route != route.DestinationCIDR {
+				ipRanges = append(ipRanges, configured_route)
+			}
+		}
+
+		interfaceUpdateOptions := linodego.InstanceConfigInterfaceUpdateOptions{
+			IPRanges: ipRanges,
+		}
+		resp, err := r.client.UpdateInstanceConfigInterface(ctx, instance.ID, configs[0].ID, iface.ID, interfaceUpdateOptions)
+		if err != nil {
+			return err
+		}
+		klog.V(4).Infof("Deleted route for node %s. Current routes: %v", route.TargetNode, resp.IPRanges)
+		return nil
+	}
+	return fmt.Errorf("unable to remove route %s for node %s", route.DestinationCIDR, route.TargetNode)
+}
+
+// ListRoutes fetches routes configured on all instances which have VPC interfaces
+func (r *routes) ListRoutes(ctx context.Context, clusterName string) ([]*cloudprovider.Route, error) {
+	klog.V(4).Infof("Fetching routes configured on the cluster")
+	instances, err := r.instances.listAllInstances(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	var routes []*cloudprovider.Route
+	for _, instance := range instances {
+		configs, err := r.getInstanceConfigs(ctx, instance.ID)
+		if err != nil {
+			klog.Errorf("Failed finding routes for instance id %d. Error: %v", instance.ID, err)
+			continue
+		}
+
+		for _, iface := range configs[0].Interfaces {
+			if iface.VPCID == nil || r.vpcid != *iface.VPCID || iface.IPv4.VPC == "" {
+				continue
+			}
+
+			for _, ipsubnet := range iface.IPRanges {
+				route := &cloudprovider.Route{
+					TargetNode:      types.NodeName(instance.Label),
+					DestinationCIDR: ipsubnet,
+				}
+				klog.V(4).Infof("Found route: node %s, route %s", instance.Label, route.DestinationCIDR)
+				routes = append(routes, route)
+			}
+		}
+	}
+	return routes, nil
+}
diff --git a/cloud/linode/vpc.go b/cloud/linode/vpc.go
new file mode 100644
index 0000000..fd40e73
--- /dev/null
+++ b/cloud/linode/vpc.go
@@ -0,0 +1,31 @@
+package linode
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/linode/linode-cloud-controller-manager/cloud/linode/client"
+	"github.com/linode/linodego"
+)
+
+type vpcLookupError struct {
+	value string
+}
+
+func (e vpcLookupError) Error() string {
+	return fmt.Sprintf("failed to find VPC: %q", e.value)
+}
+
+// getVPCID returns the VPC id using the VPC label
+func getVPCID(client client.Client, vpcName string) (int, error) {
+	vpcs, err := client.ListVPCs(context.TODO(), &linodego.ListOptions{})
+	if err != nil {
+		return 0, err
+	}
+	for _, vpc := range vpcs {
+		if vpc.Label == vpcName {
+			return vpc.ID, nil
+		}
+	}
+	return 0, vpcLookupError{vpcName}
+}
diff --git a/deploy/chart/templates/daemonset.yaml b/deploy/chart/templates/daemonset.yaml
index 86d45dd..4b2b3d4 100644
--- a/deploy/chart/templates/daemonset.yaml
+++ b/deploy/chart/templates/daemonset.yaml
@@ -33,6 +33,18 @@ spec:
             - --v=3
             - --port=0
             - --secure-port=10253
+            {{- if .Values.linodegoDebug }}
+            - --linodego-debug={{ .Values.linodegoDebug }}
+            {{- end }}
+            {{- if .Values.routeController }}
+            - --enable-route-controller=true
+            - --vpc-name={{ .Values.routeController.vpcName }}
+            - --configure-cloud-routes={{ .Values.routeController.configureCloudRoutes }}
+            - --cluster-cidr={{ .Values.routeController.clusterCIDR }}
+            {{- if .Values.routeController.routeReconciliationPeriod }}
+            - --route-reconciliation-period={{ .Values.routeController.routeReconciliationPeriod }}
+            {{- end }}
+            {{- end }}
           volumeMounts:
             - mountPath: /etc/kubernetes
               name: k8s
diff --git a/deploy/chart/values.yaml b/deploy/chart/values.yaml
index 42ee251..2df40a1 100644
--- a/deploy/chart/values.yaml
+++ b/deploy/chart/values.yaml
@@ -43,6 +43,13 @@ tolerations:
   - key: node.kubernetes.io/unreachable
     operator: Exists
     effect: NoSchedule
+
+# This section adds ability to enable route-controller for ccm
+# routeController:
+#   vpcName: <name of VPC>
+#   clusterCIDR: 10.0.0.0/8
+#   configureCloudRoutes: true
+
 # This section adds the ability to pass environment variables to adjust CCM defaults
 # https://github.com/linode/linode-cloud-controller-manager/blob/master/cloud/linode/loadbalancers.go
 # LINODE_HOSTNAME_ONLY_INGRESS type bool is supported
diff --git a/main.go b/main.go
index f82c9e1..dfcf10b 100644
--- a/main.go
+++ b/main.go
@@ -76,6 +76,8 @@ func main() {
 
 	// Add Linode-specific flags
 	command.Flags().BoolVar(&linode.Options.LinodeGoDebug, "linodego-debug", false, "enables debug output for the LinodeAPI wrapper")
+	command.Flags().BoolVar(&linode.Options.EnableRouteController, "enable-route-controller", false, "enables route_controller for ccm")
+	command.Flags().StringVar(&linode.Options.VPCName, "vpc-name", "", "vpc name whose routes will be managed by route-controller")
 
 	// Set static flags
 	command.Flags().VisitAll(func(fl *pflag.Flag) {


Test patch:
diff --git a/cloud/linode/client/mock_client_test.go b/cloud/linode/client/mock_client_test.go
index c753589..b39dca0 100644
--- a/cloud/linode/client/mock_client_test.go
+++ b/cloud/linode/client/mock_client_test.go
@@ -226,6 +226,21 @@ func (mr *MockClientMockRecorder) ListFirewallDevices(arg0, arg1, arg2 interface
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFirewallDevices", reflect.TypeOf((*MockClient)(nil).ListFirewallDevices), arg0, arg1, arg2)
 }
 
+// ListInstanceConfigs mocks base method.
+func (m *MockClient) ListInstanceConfigs(arg0 context.Context, arg1 int, arg2 *linodego.ListOptions) ([]linodego.InstanceConfig, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListInstanceConfigs", arg0, arg1, arg2)
+	ret0, _ := ret[0].([]linodego.InstanceConfig)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListInstanceConfigs indicates an expected call of ListInstanceConfigs.
+func (mr *MockClientMockRecorder) ListInstanceConfigs(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstanceConfigs", reflect.TypeOf((*MockClient)(nil).ListInstanceConfigs), arg0, arg1, arg2)
+}
+
 // ListInstances mocks base method.
 func (m *MockClient) ListInstances(arg0 context.Context, arg1 *linodego.ListOptions) ([]linodego.Instance, error) {
 	m.ctrl.T.Helper()
@@ -286,6 +301,21 @@ func (mr *MockClientMockRecorder) ListNodeBalancers(arg0, arg1 interface{}) *gom
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNodeBalancers", reflect.TypeOf((*MockClient)(nil).ListNodeBalancers), arg0, arg1)
 }
 
+// ListVPCs mocks base method.
+func (m *MockClient) ListVPCs(arg0 context.Context, arg1 *linodego.ListOptions) ([]linodego.VPC, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListVPCs", arg0, arg1)
+	ret0, _ := ret[0].([]linodego.VPC)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListVPCs indicates an expected call of ListVPCs.
+func (mr *MockClientMockRecorder) ListVPCs(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVPCs", reflect.TypeOf((*MockClient)(nil).ListVPCs), arg0, arg1)
+}
+
 // RebuildNodeBalancerConfig mocks base method.
 func (m *MockClient) RebuildNodeBalancerConfig(arg0 context.Context, arg1, arg2 int, arg3 linodego.NodeBalancerConfigRebuildOptions) (*linodego.NodeBalancerConfig, error) {
 	m.ctrl.T.Helper()
@@ -316,6 +346,21 @@ func (mr *MockClientMockRecorder) UpdateFirewallRules(arg0, arg1, arg2 interface
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFirewallRules", reflect.TypeOf((*MockClient)(nil).UpdateFirewallRules), arg0, arg1, arg2)
 }
 
+// UpdateInstanceConfigInterface mocks base method.
+func (m *MockClient) UpdateInstanceConfigInterface(arg0 context.Context, arg1, arg2, arg3 int, arg4 linodego.InstanceConfigInterfaceUpdateOptions) (*linodego.InstanceConfigInterface, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateInstanceConfigInterface", arg0, arg1, arg2, arg3, arg4)
+	ret0, _ := ret[0].(*linodego.InstanceConfigInterface)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UpdateInstanceConfigInterface indicates an expected call of UpdateInstanceConfigInterface.
+func (mr *MockClientMockRecorder) UpdateInstanceConfigInterface(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstanceConfigInterface", reflect.TypeOf((*MockClient)(nil).UpdateInstanceConfigInterface), arg0, arg1, arg2, arg3, arg4)
+}
+
 // UpdateNodeBalancer mocks base method.
 func (m *MockClient) UpdateNodeBalancer(arg0 context.Context, arg1 int, arg2 linodego.NodeBalancerUpdateOptions) (*linodego.NodeBalancer, error) {
 	m.ctrl.T.Helper()
diff --git a/cloud/linode/instances_test.go b/cloud/linode/instances_test.go
index a93711a..2eadd4e 100644
--- a/cloud/linode/instances_test.go
+++ b/cloud/linode/instances_test.go
@@ -64,6 +64,18 @@ func TestInstanceExists(t *testing.T) {
 				Type:   "g6-standard-2",
 			},
 		}, nil)
+		client.EXPECT().GetInstanceIPAddresses(gomock.Any(), 123).Times(1).Return(&linodego.InstanceIPAddressResponse{
+			IPv4: &linodego.InstanceIPv4Response{
+				Public: []*linodego.InstanceIP{
+					{Address: "45.76.101.25"},
+				},
+				Private: []*linodego.InstanceIP{
+					{Address: "192.168.133.65"},
+				},
+			},
+			IPv6: nil,
+		}, nil)
+		client.EXPECT().ListInstanceConfigs(gomock.Any(), 123, gomock.Any()).Times(1).Return([]linodego.InstanceConfig{}, nil)
 
 		exists, err := instances.InstanceExists(ctx, node)
 		assert.NoError(t, err)
@@ -78,6 +90,18 @@ func TestInstanceExists(t *testing.T) {
 		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
 			{ID: 123, Label: name},
 		}, nil)
+		client.EXPECT().GetInstanceIPAddresses(gomock.Any(), 123).Times(1).Return(&linodego.InstanceIPAddressResponse{
+			IPv4: &linodego.InstanceIPv4Response{
+				Public: []*linodego.InstanceIP{
+					{Address: "45.76.101.25"},
+				},
+				Private: []*linodego.InstanceIP{
+					{Address: "192.168.133.65"},
+				},
+			},
+			IPv6: nil,
+		}, nil)
+		client.EXPECT().ListInstanceConfigs(gomock.Any(), 123, gomock.Any()).Times(1).Return([]linodego.InstanceConfig{}, nil)
 
 		exists, err := instances.InstanceExists(ctx, node)
 		assert.NoError(t, err)
@@ -127,13 +151,27 @@ func TestMetadataRetrieval(t *testing.T) {
 		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
 			{ID: id, Label: name, Type: linodeType, Region: region, IPv4: []*net.IP{&publicIPv4, &privateIPv4}},
 		}, nil)
+		client.EXPECT().GetInstanceIPAddresses(gomock.Any(), id).Times(1).Return(&linodego.InstanceIPAddressResponse{
+			IPv4: &linodego.InstanceIPv4Response{
+				Public: []*linodego.InstanceIP{
+					{Address: "45.76.101.25"},
+				},
+				Private: []*linodego.InstanceIP{
+					{Address: "192.168.133.65"},
+				},
+			},
+			IPv6: nil,
+		}, nil)
+		client.EXPECT().ListInstanceConfigs(gomock.Any(), 123, gomock.Any()).Times(1).Return([]linodego.InstanceConfig{
+			{ID: 123456},
+		}, nil)
 
 		meta, err := instances.InstanceMetadata(ctx, node)
 		assert.NoError(t, err)
 		assert.Equal(t, providerIDPrefix+strconv.Itoa(id), meta.ProviderID)
 		assert.Equal(t, region, meta.Region)
 		assert.Equal(t, linodeType, meta.InstanceType)
-		assert.Equal(t, meta.NodeAddresses, []v1.NodeAddress{
+		assert.Equal(t, []v1.NodeAddress{
 			{
 				Type:    v1.NodeHostName,
 				Address: name,
@@ -146,7 +184,7 @@ func TestMetadataRetrieval(t *testing.T) {
 				Type:    v1.NodeInternalIP,
 				Address: privateIPv4.String(),
 			},
-		})
+		}, meta.NodeAddresses)
 	})
 
 	ipTests := []struct {
@@ -197,12 +235,24 @@ func TestMetadataRetrieval(t *testing.T) {
 			node := nodeWithProviderID(providerID)
 
 			ips := make([]*net.IP, 0, len(test.inputIPs))
+			pubIPs := make([]*linodego.InstanceIP, 0)
+			privIPs := make([]*linodego.InstanceIP, 0)
 			for _, ip := range test.inputIPs {
 				parsed := net.ParseIP(ip)
 				if parsed == nil {
 					t.Fatalf("cannot parse %v as an ipv4", ip)
 				}
 				ips = append(ips, &parsed)
+				if parsed.IsPrivate() {
+					privIPs = append(privIPs, &linodego.InstanceIP{Address: ip})
+				} else {
+					pubIPs = append(pubIPs, &linodego.InstanceIP{Address: ip})
+				}
+			}
+
+			ipv4s := &linodego.InstanceIPv4Response{
+				Public:  pubIPs,
+				Private: privIPs,
 			}
 
 			linodeType := "g6-standard-1"
@@ -210,6 +260,13 @@ func TestMetadataRetrieval(t *testing.T) {
 			client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
 				{ID: id, Label: name, Type: linodeType, Region: region, IPv4: ips},
 			}, nil)
+			client.EXPECT().GetInstanceIPAddresses(gomock.Any(), id).Times(1).Return(&linodego.InstanceIPAddressResponse{
+				IPv4: ipv4s,
+				IPv6: nil,
+			}, nil)
+			client.EXPECT().ListInstanceConfigs(gomock.Any(), id, gomock.Any()).Times(1).Return([]linodego.InstanceConfig{
+				{ID: 123456},
+			}, nil)
 
 			meta, err := instances.InstanceMetadata(ctx, node)
 
@@ -281,6 +338,16 @@ func TestInstanceShutdown(t *testing.T) {
 		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
 			{ID: id, Label: "offline-linode", Status: linodego.InstanceOffline},
 		}, nil)
+		client.EXPECT().GetInstanceIPAddresses(gomock.Any(), id).Times(1).Return(&linodego.InstanceIPAddressResponse{
+			IPv4: &linodego.InstanceIPv4Response{
+				Public:  []*linodego.InstanceIP{},
+				Private: []*linodego.InstanceIP{},
+			},
+			IPv6: nil,
+		}, nil)
+		client.EXPECT().ListInstanceConfigs(gomock.Any(), id, gomock.Any()).Times(1).Return([]linodego.InstanceConfig{
+			{ID: 123456},
+		}, nil)
 		shutdown, err := instances.InstanceShutdown(ctx, node)
 
 		assert.NoError(t, err)
@@ -294,6 +361,16 @@ func TestInstanceShutdown(t *testing.T) {
 		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
 			{ID: id, Label: "shutting-down-linode", Status: linodego.InstanceShuttingDown},
 		}, nil)
+		client.EXPECT().GetInstanceIPAddresses(gomock.Any(), id).Times(1).Return(&linodego.InstanceIPAddressResponse{
+			IPv4: &linodego.InstanceIPv4Response{
+				Public:  []*linodego.InstanceIP{},
+				Private: []*linodego.InstanceIP{},
+			},
+			IPv6: nil,
+		}, nil)
+		client.EXPECT().ListInstanceConfigs(gomock.Any(), id, gomock.Any()).Times(1).Return([]linodego.InstanceConfig{
+			{ID: 123456},
+		}, nil)
 		shutdown, err := instances.InstanceShutdown(ctx, node)
 
 		assert.NoError(t, err)
@@ -307,6 +384,16 @@ func TestInstanceShutdown(t *testing.T) {
 		client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{
 			{ID: id, Label: "running-linode", Status: linodego.InstanceRunning},
 		}, nil)
+		client.EXPECT().GetInstanceIPAddresses(gomock.Any(), id).Times(1).Return(&linodego.InstanceIPAddressResponse{
+			IPv4: &linodego.InstanceIPv4Response{
+				Public:  []*linodego.InstanceIP{},
+				Private: []*linodego.InstanceIP{},
+			},
+			IPv6: nil,
+		}, nil)
+		client.EXPECT().ListInstanceConfigs(gomock.Any(), id, gomock.Any()).Times(1).Return([]linodego.InstanceConfig{
+			{ID: 123456},
+		}, nil)
 		shutdown, err := instances.InstanceShutdown(ctx, node)
 
 		assert.NoError(t, err)
diff --git a/cloud/linode/mock_client_test.go b/cloud/linode/mock_client_test.go
index d7f5b98..3a2e461 100644
--- a/cloud/linode/mock_client_test.go
+++ b/cloud/linode/mock_client_test.go
@@ -226,6 +226,21 @@ func (mr *MockClientMockRecorder) ListFirewallDevices(arg0, arg1, arg2 interface
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFirewallDevices", reflect.TypeOf((*MockClient)(nil).ListFirewallDevices), arg0, arg1, arg2)
 }
 
+// ListInstanceConfigs mocks base method.
+func (m *MockClient) ListInstanceConfigs(arg0 context.Context, arg1 int, arg2 *linodego.ListOptions) ([]linodego.InstanceConfig, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListInstanceConfigs", arg0, arg1, arg2)
+	ret0, _ := ret[0].([]linodego.InstanceConfig)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListInstanceConfigs indicates an expected call of ListInstanceConfigs.
+func (mr *MockClientMockRecorder) ListInstanceConfigs(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstanceConfigs", reflect.TypeOf((*MockClient)(nil).ListInstanceConfigs), arg0, arg1, arg2)
+}
+
 // ListInstances mocks base method.
 func (m *MockClient) ListInstances(arg0 context.Context, arg1 *linodego.ListOptions) ([]linodego.Instance, error) {
 	m.ctrl.T.Helper()
@@ -286,6 +301,21 @@ func (mr *MockClientMockRecorder) ListNodeBalancers(arg0, arg1 interface{}) *gom
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNodeBalancers", reflect.TypeOf((*MockClient)(nil).ListNodeBalancers), arg0, arg1)
 }
 
+// ListVPCs mocks base method.
+func (m *MockClient) ListVPCs(arg0 context.Context, arg1 *linodego.ListOptions) ([]linodego.VPC, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListVPCs", arg0, arg1)
+	ret0, _ := ret[0].([]linodego.VPC)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListVPCs indicates an expected call of ListVPCs.
+func (mr *MockClientMockRecorder) ListVPCs(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVPCs", reflect.TypeOf((*MockClient)(nil).ListVPCs), arg0, arg1)
+}
+
 // RebuildNodeBalancerConfig mocks base method.
 func (m *MockClient) RebuildNodeBalancerConfig(arg0 context.Context, arg1, arg2 int, arg3 linodego.NodeBalancerConfigRebuildOptions) (*linodego.NodeBalancerConfig, error) {
 	m.ctrl.T.Helper()
@@ -316,6 +346,21 @@ func (mr *MockClientMockRecorder) UpdateFirewallRules(arg0, arg1, arg2 interface
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFirewallRules", reflect.TypeOf((*MockClient)(nil).UpdateFirewallRules), arg0, arg1, arg2)
 }
 
+// UpdateInstanceConfigInterface mocks base method.
+func (m *MockClient) UpdateInstanceConfigInterface(arg0 context.Context, arg1, arg2, arg3 int, arg4 linodego.InstanceConfigInterfaceUpdateOptions) (*linodego.InstanceConfigInterface, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateInstanceConfigInterface", arg0, arg1, arg2, arg3, arg4)
+	ret0, _ := ret[0].(*linodego.InstanceConfigInterface)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UpdateInstanceConfigInterface indicates an expected call of UpdateInstanceConfigInterface.
+func (mr *MockClientMockRecorder) UpdateInstanceConfigInterface(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstanceConfigInterface", reflect.TypeOf((*MockClient)(nil).UpdateInstanceConfigInterface), arg0, arg1, arg2, arg3, arg4)
+}
+
 // UpdateNodeBalancer mocks base method.
 func (m *MockClient) UpdateNodeBalancer(arg0 context.Context, arg1 int, arg2 linodego.NodeBalancerUpdateOptions) (*linodego.NodeBalancer, error) {
 	m.ctrl.T.Helper()