Skip to content

Commit 5103be6

Browse files
authored
feat: add linode UUID as a metadata to each node (#162)
* fix(build): correct image os/arch on ARM Mac Signed-off-by: Mateusz Urbanek <[email protected]> * feat: add client builder method Signed-off-by: Mateusz Urbanek <[email protected]> * feat: add node controller Signed-off-by: Mateusz Urbanek <[email protected]> * feat: integrate node controller with rest of code Signed-off-by: Mateusz Urbanek <[email protected]> * feat: node update should trigger metadata update Signed-off-by: Mateusz Urbanek <[email protected]> * fix: prevent backslash escape Signed-off-by: Mateusz Urbanek <[email protected]> --------- Signed-off-by: Mateusz Urbanek <[email protected]>
1 parent aded8b1 commit 5103be6

File tree

7 files changed

+209
-41
lines changed

7 files changed

+209
-41
lines changed

Makefile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,8 @@ imgname:
6464
echo IMG=${IMG}
6565

6666
.PHONY: docker-build
67-
# we cross compile the binary for linux, then build a container
68-
docker-build: build-linux
69-
docker build . -t ${IMG}
67+
docker-build:
68+
docker build --platform=linux/amd64 --tag=${IMG} .
7069

7170
.PHONY: docker-push
7271
# must run the docker build before pushing the image

cloud/linode/annotations.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package linode
2+
3+
const (
4+
// annLinodeDefaultProtocol is the annotation used to specify the default protocol
5+
// for Linode load balancers. Options are tcp, http and https. Defaults to tcp.
6+
annLinodeDefaultProtocol = "service.beta.kubernetes.io/linode-loadbalancer-default-protocol"
7+
annLinodePortConfigPrefix = "service.beta.kubernetes.io/linode-loadbalancer-port-"
8+
annLinodeDefaultProxyProtocol = "service.beta.kubernetes.io/linode-loadbalancer-default-proxy-protocol"
9+
10+
annLinodeCheckPath = "service.beta.kubernetes.io/linode-loadbalancer-check-path"
11+
annLinodeCheckBody = "service.beta.kubernetes.io/linode-loadbalancer-check-body"
12+
annLinodeHealthCheckType = "service.beta.kubernetes.io/linode-loadbalancer-check-type"
13+
14+
annLinodeHealthCheckInterval = "service.beta.kubernetes.io/linode-loadbalancer-check-interval"
15+
annLinodeHealthCheckTimeout = "service.beta.kubernetes.io/linode-loadbalancer-check-timeout"
16+
annLinodeHealthCheckAttempts = "service.beta.kubernetes.io/linode-loadbalancer-check-attempts"
17+
annLinodeHealthCheckPassive = "service.beta.kubernetes.io/linode-loadbalancer-check-passive"
18+
19+
// annLinodeThrottle is the annotation specifying the value of the Client Connection
20+
// Throttle, which limits the number of subsequent new connections per second from the
21+
// same client IP. Options are a number between 1-20, or 0 to disable. Defaults to 20.
22+
annLinodeThrottle = "service.beta.kubernetes.io/linode-loadbalancer-throttle"
23+
24+
annLinodeLoadBalancerPreserve = "service.beta.kubernetes.io/linode-loadbalancer-preserve"
25+
annLinodeNodeBalancerID = "service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id"
26+
27+
annLinodeHostnameOnlyIngress = "service.beta.kubernetes.io/linode-loadbalancer-hostname-only-ingress"
28+
annLinodeLoadBalancerTags = "service.beta.kubernetes.io/linode-loadbalancer-tags"
29+
annLinodeCloudFirewallID = "service.beta.kubernetes.io/linode-loadbalancer-firewall-id"
30+
31+
annLinodeNodePrivateIP = "node.k8s.linode.com/private-ip"
32+
annLinodeHostUUID = "node.k8s.linode.com/host-uuid"
33+
)

cloud/linode/client.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ package linode
44

55
import (
66
"context"
7+
"net/url"
8+
"regexp"
9+
"strings"
710

811
"github.com/linode/linodego"
912
)
@@ -27,3 +30,33 @@ type Client interface {
2730

2831
// linodego.Client implements Client
2932
var _ Client = (*linodego.Client)(nil)
33+
34+
func newLinodeClient(token, ua, apiURL string) (*linodego.Client, error) {
35+
linodeClient := linodego.NewClient(nil)
36+
linodeClient.SetUserAgent(ua)
37+
linodeClient.SetToken(token)
38+
39+
// Validate apiURL
40+
parsedURL, err := url.Parse(apiURL)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
validatedURL := &url.URL{
46+
Host: parsedURL.Host,
47+
Scheme: parsedURL.Scheme,
48+
}
49+
50+
linodeClient.SetBaseURL(validatedURL.String())
51+
52+
version := ""
53+
matches := regexp.MustCompile(`/v\d+`).FindAllString(parsedURL.Path, -1)
54+
55+
if len(matches) > 0 {
56+
version = strings.Trim(matches[len(matches)-1], "/")
57+
}
58+
59+
linodeClient.SetAPIVersion(version)
60+
61+
return &linodeClient, nil
62+
}

cloud/linode/cloud.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
ProviderName = "linode"
1717
accessTokenEnv = "LINODE_API_TOKEN"
1818
regionEnv = "LINODE_REGION"
19+
urlEnv = "LINODE_URL"
1920
)
2021

2122
// Options is a configuration object for this cloudprovider implementation.
@@ -52,28 +53,37 @@ func newCloud() (cloudprovider.Interface, error) {
5253
return nil, fmt.Errorf("%s must be set in the environment (use a k8s secret)", regionEnv)
5354
}
5455

55-
linodeClient := linodego.NewClient(nil)
56-
linodeClient.SetToken(apiToken)
56+
url := os.Getenv(urlEnv)
57+
ua := fmt.Sprintf("linode-cloud-controller-manager %s", linodego.DefaultUserAgent)
58+
59+
linodeClient, err := newLinodeClient(apiToken, ua, url)
60+
if err != nil {
61+
return nil, fmt.Errorf("client was not created succesfully: %w", err)
62+
}
63+
5764
if Options.LinodeGoDebug {
5865
linodeClient.SetDebug(true)
5966
}
60-
linodeClient.SetUserAgent(fmt.Sprintf("linode-cloud-controller-manager %s", linodego.DefaultUserAgent))
6167

6268
// Return struct that satisfies cloudprovider.Interface
6369
return &linodeCloud{
64-
client: &linodeClient,
65-
instances: newInstances(&linodeClient),
66-
loadbalancers: newLoadbalancers(&linodeClient, region),
70+
client: linodeClient,
71+
instances: newInstances(linodeClient),
72+
loadbalancers: newLoadbalancers(linodeClient, region),
6773
}, nil
6874
}
6975

7076
func (c *linodeCloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stopCh <-chan struct{}) {
7177
kubeclient := clientBuilder.ClientOrDie("linode-shared-informers")
7278
sharedInformer := informers.NewSharedInformerFactory(kubeclient, 0)
7379
serviceInformer := sharedInformer.Core().V1().Services()
80+
nodeInformer := sharedInformer.Core().V1().Nodes()
7481

7582
serviceController := newServiceController(c.loadbalancers.(*loadbalancers), serviceInformer)
7683
go serviceController.Run(stopCh)
84+
85+
nodeController := newNodeController(kubeclient, c.client, nodeInformer)
86+
go nodeController.Run(stopCh)
7787
}
7888

7989
func (c *linodeCloud) LoadBalancer() (cloudprovider.LoadBalancer, bool) {

cloud/linode/instances.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type instances struct {
5252
nodeCache *nodeCache
5353
}
5454

55-
func newInstances(client Client) cloudprovider.InstancesV2 {
55+
func newInstances(client Client) *instances {
5656
var timeout int
5757
if raw, ok := os.LookupEnv("LINODE_INSTANCE_CACHE_TTL"); ok {
5858
timeout, _ = strconv.Atoi(raw)

cloud/linode/loadbalancers.go

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,37 +24,6 @@ import (
2424
"github.com/linode/linodego"
2525
)
2626

27-
const (
28-
// annLinodeDefaultProtocol is the annotation used to specify the default protocol
29-
// for Linode load balancers. Options are tcp, http and https. Defaults to tcp.
30-
annLinodeDefaultProtocol = "service.beta.kubernetes.io/linode-loadbalancer-default-protocol"
31-
annLinodePortConfigPrefix = "service.beta.kubernetes.io/linode-loadbalancer-port-"
32-
annLinodeDefaultProxyProtocol = "service.beta.kubernetes.io/linode-loadbalancer-default-proxy-protocol"
33-
34-
annLinodeCheckPath = "service.beta.kubernetes.io/linode-loadbalancer-check-path"
35-
annLinodeCheckBody = "service.beta.kubernetes.io/linode-loadbalancer-check-body"
36-
annLinodeHealthCheckType = "service.beta.kubernetes.io/linode-loadbalancer-check-type"
37-
38-
annLinodeHealthCheckInterval = "service.beta.kubernetes.io/linode-loadbalancer-check-interval"
39-
annLinodeHealthCheckTimeout = "service.beta.kubernetes.io/linode-loadbalancer-check-timeout"
40-
annLinodeHealthCheckAttempts = "service.beta.kubernetes.io/linode-loadbalancer-check-attempts"
41-
annLinodeHealthCheckPassive = "service.beta.kubernetes.io/linode-loadbalancer-check-passive"
42-
43-
// annLinodeThrottle is the annotation specifying the value of the Client Connection
44-
// Throttle, which limits the number of subsequent new connections per second from the
45-
// same client IP. Options are a number between 1-20, or 0 to disable. Defaults to 20.
46-
annLinodeThrottle = "service.beta.kubernetes.io/linode-loadbalancer-throttle"
47-
48-
annLinodeLoadBalancerPreserve = "service.beta.kubernetes.io/linode-loadbalancer-preserve"
49-
annLinodeNodeBalancerID = "service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id"
50-
51-
annLinodeHostnameOnlyIngress = "service.beta.kubernetes.io/linode-loadbalancer-hostname-only-ingress"
52-
annLinodeLoadBalancerTags = "service.beta.kubernetes.io/linode-loadbalancer-tags"
53-
annLinodeCloudFirewallID = "service.beta.kubernetes.io/linode-loadbalancer-firewall-id"
54-
55-
annLinodeNodePrivateIP = "node.k8s.linode.com/private-ip"
56-
)
57-
5827
var errNoNodesAvailable = errors.New("no nodes available for nodebalancer")
5928

6029
type lbNotFoundError struct {

cloud/linode/node_controller.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package linode
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"time"
7+
8+
"github.com/appscode/go/wait"
9+
"github.com/linode/linodego"
10+
v1 "k8s.io/api/core/v1"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
v1informers "k8s.io/client-go/informers/core/v1"
13+
"k8s.io/client-go/kubernetes"
14+
"k8s.io/client-go/tools/cache"
15+
"k8s.io/client-go/util/workqueue"
16+
"k8s.io/klog/v2"
17+
)
18+
19+
type nodeController struct {
20+
client Client
21+
instances *instances
22+
kubeclient kubernetes.Interface
23+
informer v1informers.NodeInformer
24+
25+
queue workqueue.DelayingInterface
26+
}
27+
28+
func newNodeController(kubeclient kubernetes.Interface, client Client, informer v1informers.NodeInformer) *nodeController {
29+
return &nodeController{
30+
client: client,
31+
instances: newInstances(client),
32+
kubeclient: kubeclient,
33+
informer: informer,
34+
queue: workqueue.NewDelayingQueue(),
35+
}
36+
}
37+
38+
func (s *nodeController) Run(stopCh <-chan struct{}) {
39+
s.informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
40+
AddFunc: func(obj interface{}) {
41+
node, ok := obj.(*v1.Node)
42+
if !ok {
43+
return
44+
}
45+
46+
klog.Infof("NodeController will handle newly created node (%s) metadata", node.Name)
47+
s.queue.Add(node)
48+
},
49+
UpdateFunc: func(_, new interface{}) {
50+
node, ok := new.(*v1.Node)
51+
if !ok {
52+
return
53+
}
54+
55+
klog.Infof("NodeController will handle updated node (%s) metadata", node.Name)
56+
s.queue.Add(node)
57+
},
58+
})
59+
60+
go wait.Until(s.worker, time.Second, stopCh)
61+
s.informer.Informer().Run(stopCh)
62+
}
63+
64+
// worker runs a worker thread that dequeues new or modified nodes and processes
65+
// metadata (host UUID) on each of them.
66+
func (s *nodeController) worker() {
67+
for s.processNext() {
68+
}
69+
}
70+
71+
func (s *nodeController) processNext() bool {
72+
key, quit := s.queue.Get()
73+
if quit {
74+
return false
75+
}
76+
defer s.queue.Done(key)
77+
78+
node, ok := key.(*v1.Node)
79+
if !ok {
80+
klog.Errorf("expected dequeued key to be of type *v1.Node but got %T", node)
81+
return true
82+
}
83+
84+
err := s.handleNodeAdded(context.TODO(), node)
85+
switch deleteErr := err.(type) {
86+
case nil:
87+
break
88+
89+
case *linodego.Error:
90+
if deleteErr.Code >= http.StatusInternalServerError || deleteErr.Code == http.StatusTooManyRequests {
91+
klog.Errorf("failed to add metadata for node (%s); retrying in 1 minute: %s", node.Name, err)
92+
s.queue.AddAfter(node, retryInterval)
93+
}
94+
95+
default:
96+
klog.Errorf("failed to add metadata for node (%s); will not retry: %s", node.Name, err)
97+
}
98+
return true
99+
}
100+
101+
func (s *nodeController) handleNodeAdded(ctx context.Context, node *v1.Node) error {
102+
klog.Infof("NodeController handling node (%s) addition", node.Name)
103+
104+
linode, err := s.instances.lookupLinode(ctx, node)
105+
if err != nil {
106+
klog.Infof("instance lookup error: %s", err.Error())
107+
return err
108+
}
109+
110+
uuid, ok := node.Labels[annLinodeHostUUID]
111+
if ok && uuid == linode.HostUUID {
112+
return nil
113+
}
114+
115+
node.Labels[annLinodeHostUUID] = linode.HostUUID
116+
117+
_, err = s.kubeclient.CoreV1().Nodes().Update(ctx, node, metav1.UpdateOptions{})
118+
if err != nil {
119+
klog.Infof("node update error: %s", err.Error())
120+
return err
121+
}
122+
123+
return nil
124+
}

0 commit comments

Comments
 (0)