diff --git a/README.md b/README.md index b23cfc25..4c73c5e0 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ Key | Values | Default | Description ---|---|---|--- `linodego-debug` | `boolean` | `false` | enables debug output for the LinodeAPI wrapper. `linode-node-private-subnet` | `net.IPNet` | `192.168.128.0/17` | specifies backend network to use with nodebalancers. - +`auto-annotate-node` | `boolean` | `true` | enables automatically adding node annotation containing ipaddress to use with nodebalancers. ### Configurations There are other variables that can be set to a different value. For list of all the modifiable variables/values, take a look at './deploy/chart/values.yaml'. diff --git a/cloud/linode/client.go b/cloud/linode/client.go index a2fcde09..22b5ce1c 100644 --- a/cloud/linode/client.go +++ b/cloud/linode/client.go @@ -15,6 +15,7 @@ type Client interface { GetInstance(context.Context, int) (*linodego.Instance, error) ListInstances(context.Context, *linodego.ListOptions) ([]linodego.Instance, error) GetInstanceIPAddresses(context.Context, int) (*linodego.InstanceIPAddressResponse, error) + ListInstanceConfigs(context.Context, int, *linodego.ListOptions) ([]linodego.InstanceConfig, error) CreateNodeBalancer(context.Context, linodego.NodeBalancerCreateOptions) (*linodego.NodeBalancer, error) GetNodeBalancer(context.Context, int) (*linodego.NodeBalancer, error) diff --git a/cloud/linode/cloud.go b/cloud/linode/cloud.go index 629bc7d1..a03a01a4 100644 --- a/cloud/linode/cloud.go +++ b/cloud/linode/cloud.go @@ -27,6 +27,7 @@ var Options struct { KubeconfigFlag *pflag.Flag LinodeGoDebug bool LinodeNodePrivateSubnet net.IPNet + AutoAnnotateNode bool } type linodeCloud struct { diff --git a/cloud/linode/instances.go b/cloud/linode/instances.go index 26c762d0..4424b278 100644 --- a/cloud/linode/instances.go +++ b/cloud/linode/instances.go @@ -20,6 +20,43 @@ type nodeCache struct { nodes map[int]*linodego.Instance lastUpdate time.Time ttl time.Duration + ips map[int][]string +} + +// getInstanceIPv4Addresses returns all ipv4 addresses configured on a linode. +func (nc *nodeCache) getInstanceIPv4Addresses(ctx context.Context, id int, client Client) ([]string, error) { + // Retrieve ipaddresses for the linode + addresses, err := client.GetInstanceIPAddresses(ctx, id) + if err != nil { + return nil, err + } + + var ips []string + if len(addresses.IPv4.Private) != 0 { + ips = append(ips, addresses.IPv4.Private[0].Address) + } + if len(addresses.IPv4.Public) != 0 { + ips = append(ips, addresses.IPv4.Public[0].Address) + } + + // Retrieve interface configs for the linode + configs, err := client.ListInstanceConfigs(ctx, id, &linodego.ListOptions{}) + if err != nil { + return nil, err + } + + if len(configs) == 0 { + return ips, nil + } + + // Iterate over interface config and find VPC specific ips + for _, iface := range configs[0].Interfaces { + if iface.VPCID != nil && iface.IPv4.VPC != "" { + ips = append(ips, iface.IPv4.VPC) + } + } + + return ips, nil } // refreshInstances conditionally loads all instances from the Linode API and caches them. @@ -36,10 +73,26 @@ func (nc *nodeCache) refreshInstances(ctx context.Context, client Client) error if err != nil { return err } + + // get ips for each instance and store temporarily in a new map + ips := make(map[int][]string) + if Options.AutoAnnotateNode { + for _, instance := range instances { + addresses, err := nc.getInstanceIPv4Addresses(ctx, instance.ID, client) + if err != nil { + return err + } + ips[instance.ID] = addresses + } + } + + // add/update existing map with node and ip information nc.nodes = make(map[int]*linodego.Instance) + nc.ips = make(map[int][]string) for _, instance := range instances { instance := instance nc.nodes[instance.ID] = &instance + nc.ips[instance.ID] = ips[instance.ID] } nc.lastUpdate = time.Now() @@ -186,3 +239,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) ([]string, 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() + ips, ok := i.nodeCache.ips[instance.ID] + if !ok { + err := instanceNoIPAddressesError{instance.ID} + sentry.CaptureError(ctx, err) + return nil, err + } + + return ips, nil +} diff --git a/cloud/linode/mock_client_test.go b/cloud/linode/mock_client_test.go index d7f5b984..956e4812 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() diff --git a/cloud/linode/node_controller.go b/cloud/linode/node_controller.go index 653542d0..bbb9ac59 100644 --- a/cloud/linode/node_controller.go +++ b/cloud/linode/node_controller.go @@ -2,6 +2,7 @@ package linode import ( "context" + "net" "net/http" "os" "strconv" @@ -124,13 +125,13 @@ func (s *nodeController) handleNode(ctx context.Context, node *v1.Node) error { lastUpdate := s.LastMetadataUpdate(node.Name) - uuid, ok := node.Labels[annLinodeHostUUID] - if ok && time.Since(lastUpdate) < s.ttl { + uuid, foundLabel := node.Labels[annLinodeHostUUID] + privIP, foundAnnotation := node.Annotations[annLinodeNodePrivateIP] + if foundLabel && foundAnnotation && time.Since(lastUpdate) < s.ttl { return nil } - privIP, ok := node.Annotations[annLinodeNodePrivateIP] - if ok && time.Since(lastUpdate) < s.ttl { + if foundLabel && !Options.AutoAnnotateNode && time.Since(lastUpdate) < s.ttl { return nil } @@ -140,14 +141,23 @@ func (s *nodeController) handleNode(ctx context.Context, node *v1.Node) error { return err } - // TODO: Once nodebalancer supports other subnets, instead of using linode.IPv4 - // which contains public and private ip, use different call which returns - // all ip's configured on the node linodePrivIP := "" - for _, ip := range linode.IPv4 { - if Options.LinodeNodePrivateSubnet.Contains(*ip) { - linodePrivIP = ip.String() - break + if Options.AutoAnnotateNode { + ips, err := s.instances.getLinodeIPv4Addresses(ctx, node) + if err != nil { + klog.Infof("failed to get ips for linode: %s", err) + return err + } + + if len(ips) == 0 { + klog.Infof("no ips found for node %s", node.Name) + } + + for _, ip := range ips { + if Options.LinodeNodePrivateSubnet.Contains(net.ParseIP(ip)) { + linodePrivIP = ip + break + } } } @@ -170,7 +180,9 @@ func (s *nodeController) handleNode(ctx context.Context, node *v1.Node) error { // Try to update the node n.Labels[annLinodeHostUUID] = linode.HostUUID - n.Annotations[annLinodeNodePrivateIP] = linodePrivIP + if Options.AutoAnnotateNode { + n.Annotations[annLinodeNodePrivateIP] = linodePrivIP + } _, err = s.kubeclient.CoreV1().Nodes().Update(ctx, n, metav1.UpdateOptions{}) return err }); err != nil { diff --git a/deploy/chart/templates/daemonset.yaml b/deploy/chart/templates/daemonset.yaml index 86cd0a20..ae587574 100644 --- a/deploy/chart/templates/daemonset.yaml +++ b/deploy/chart/templates/daemonset.yaml @@ -39,6 +39,9 @@ spec: {{- if .Values.linodeNodePrivateSubnet }} - --linode-node-private-subnet={{ .Values.linodeNodePrivateSubnet }} {{- end }} + {{- if .Values.autoAnnotateNode }} + - --auto-annotate-node={{ .Values.autoAnnotateNode }} + {{- end }} volumeMounts: - mountPath: /etc/kubernetes name: k8s diff --git a/deploy/chart/values.yaml b/deploy/chart/values.yaml index 31b24b5f..98dc3be3 100644 --- a/deploy/chart/values.yaml +++ b/deploy/chart/values.yaml @@ -25,6 +25,9 @@ image: # enable debug output for the LinodeAPI wrapper # linodegoDebug: false +# Disable if one doesn't want to add annotations to nodes for ip to be configured with nodebalancer +autoAnnotateNode: true + # Set only if one wants to change the default backend network(192.168.128.0/17) to be configured with nodebalancers # linodeNodePrivateSubnet: "192.168.128.0/17" diff --git a/main.go b/main.go index 466bd217..1cbdacc1 100644 --- a/main.go +++ b/main.go @@ -78,6 +78,7 @@ func main() { // Add Linode-specific flags command.Flags().BoolVar(&linode.Options.LinodeGoDebug, "linodego-debug", false, "enables debug output for the LinodeAPI wrapper") command.Flags().IPNetVar(&linode.Options.LinodeNodePrivateSubnet, "linode-node-private-subnet", net.IPNet{IP: net.ParseIP("192.168.128.0"), Mask: net.CIDRMask(17, 32)}, "specifies backend network to use for nodebalancers") + command.Flags().BoolVar(&linode.Options.AutoAnnotateNode, "auto-annotate-node", false, "enables automatically adding node annotation for ip in subnet specified in linode-node-private-subnet") // Set static flags command.Flags().VisitAll(func(fl *pflag.Flag) {