Skip to content

Commit e4a2ad5

Browse files
authored
feat: Add route_controller to linode CCM (#199)
* add back changes reverted in PR #195 * get instanceConfig only when running within VPC * add and fix unittests * use lock when reading/writing vpc id * updated route-controller using /v4/vpcs/ips api * fix tests * switch to new api returning ips for specific vpc * when running with vpc set, only cache instances which are part of VPC * address review comments * update linodego to v1.33.0 * address review comment, make variable required if routecontroller is enabled --------- Co-authored-by: Rahul Sharma <[email protected]>
1 parent 1e9005a commit e4a2ad5

18 files changed

+1054
-106
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ Users can create CloudFirewall instances, supply their own rules and attach them
126126
**Note**<br/>
127127
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.
128128

129+
#### Routes
130+
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.
131+
132+
##### Example usage in values.yaml
133+
```yaml
134+
routeController:
135+
vpcName: <name of VPC>
136+
clusterCIDR: 10.0.0.0/8
137+
configureCloudRoutes: true
138+
```
139+
129140
### Nodes
130141
Kubernetes Nodes can be configured with the following annotations.
131142

cloud/linode/client/client.go

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ type Client interface {
1616
ListInstances(context.Context, *linodego.ListOptions) ([]linodego.Instance, error)
1717
GetInstanceIPAddresses(context.Context, int) (*linodego.InstanceIPAddressResponse, error)
1818

19+
UpdateInstanceConfigInterface(context.Context, int, int, int, linodego.InstanceConfigInterfaceUpdateOptions) (*linodego.InstanceConfigInterface, error)
20+
21+
ListVPCs(context.Context, *linodego.ListOptions) ([]linodego.VPC, error)
22+
ListVPCIPAddresses(context.Context, int, *linodego.ListOptions) ([]linodego.VPCIP, error)
23+
1924
CreateNodeBalancer(context.Context, linodego.NodeBalancerCreateOptions) (*linodego.NodeBalancer, error)
2025
GetNodeBalancer(context.Context, int) (*linodego.NodeBalancer, error)
2126
UpdateNodeBalancer(context.Context, int, linodego.NodeBalancerUpdateOptions) (*linodego.NodeBalancer, error)

cloud/linode/client/mocks/mock_client.go

+45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud/linode/cloud.go

+55-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"io"
66
"os"
7+
"sync"
78

89
"github.com/linode/linodego"
910
"github.com/spf13/pflag"
@@ -25,14 +26,46 @@ const (
2526
// We expect it to be initialized with flags external to this package, likely in
2627
// main.go
2728
var Options struct {
28-
KubeconfigFlag *pflag.Flag
29-
LinodeGoDebug bool
29+
KubeconfigFlag *pflag.Flag
30+
LinodeGoDebug bool
31+
EnableRouteController bool
32+
VPCName string
3033
}
3134

35+
// vpcDetails is set when VPCName options flag is set.
36+
// We use it to list instances running within the VPC if set
37+
type vpcDetails struct {
38+
mu sync.RWMutex
39+
id int
40+
name string
41+
}
42+
43+
func (v *vpcDetails) setDetails(client client.Client, name string) error {
44+
v.mu.Lock()
45+
defer v.mu.Unlock()
46+
47+
id, err := getVPCID(client, Options.VPCName)
48+
if err != nil {
49+
return fmt.Errorf("failed finding VPC ID: %w", err)
50+
}
51+
v.id = id
52+
v.name = name
53+
return nil
54+
}
55+
56+
func (v *vpcDetails) getID() int {
57+
v.mu.Lock()
58+
defer v.mu.Unlock()
59+
return v.id
60+
}
61+
62+
var vpcInfo vpcDetails = vpcDetails{id: 0, name: ""}
63+
3264
type linodeCloud struct {
3365
client client.Client
3466
instances cloudprovider.InstancesV2
3567
loadbalancers cloudprovider.LoadBalancer
68+
routes cloudprovider.Routes
3669
}
3770

3871
func init() {
@@ -67,12 +100,26 @@ func newCloud() (cloudprovider.Interface, error) {
67100
linodeClient.SetDebug(true)
68101
}
69102

70-
// Return struct that satisfies cloudprovider.Interface
71-
return &linodeCloud{
103+
if Options.VPCName != "" {
104+
err := vpcInfo.setDetails(linodeClient, Options.VPCName)
105+
if err != nil {
106+
return nil, fmt.Errorf("failed finding VPC ID: %w", err)
107+
}
108+
}
109+
110+
routes, err := newRoutes(linodeClient)
111+
if err != nil {
112+
return nil, fmt.Errorf("routes client was not created successfully: %w", err)
113+
}
114+
115+
// create struct that satisfies cloudprovider.Interface
116+
lcloud := &linodeCloud{
72117
client: linodeClient,
73118
instances: newInstances(linodeClient),
74119
loadbalancers: newLoadbalancers(linodeClient, region),
75-
}, nil
120+
routes: routes,
121+
}
122+
return lcloud, nil
76123
}
77124

78125
func (c *linodeCloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stopCh <-chan struct{}) {
@@ -109,6 +156,9 @@ func (c *linodeCloud) Clusters() (cloudprovider.Clusters, bool) {
109156
}
110157

111158
func (c *linodeCloud) Routes() (cloudprovider.Routes, bool) {
159+
if Options.EnableRouteController {
160+
return c.routes, true
161+
}
112162
return nil, false
113163
}
114164

cloud/linode/cloud_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package linode
2+
3+
import (
4+
"testing"
5+
6+
"github.com/golang/mock/gomock"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestNewCloudRouteControllerDisabled(t *testing.T) {
11+
ctrl := gomock.NewController(t)
12+
defer ctrl.Finish()
13+
14+
t.Setenv("LINODE_API_TOKEN", "dummyapitoken")
15+
t.Setenv("LINODE_REGION", "us-east")
16+
17+
t.Run("should not fail if vpc is empty and routecontroller is disabled", func(t *testing.T) {
18+
Options.VPCName = ""
19+
Options.EnableRouteController = false
20+
_, err := newCloud()
21+
assert.NoError(t, err)
22+
})
23+
24+
t.Run("fail if vpcname is empty and routecontroller is enabled", func(t *testing.T) {
25+
Options.VPCName = ""
26+
Options.EnableRouteController = true
27+
_, err := newCloud()
28+
assert.Error(t, err)
29+
})
30+
}

0 commit comments

Comments
 (0)