Repository: linode/linode-cloud-controller-manager (go)

Issue:
Support tagging resources
## 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?

----

## Feature Requests:
* [x] Have you explained your rationale for why this feature is needed? 
* [ ] Have you offered a proposed implementation/solution? 

----

Would like to specify default tags that would be applied to all resources created. This would be useful for identifying and reaping resources in a multi-cluster account.


* https://www.linode.com/docs/api/tags/
* https://www.linode.com/docs/guides/tags-and-groups/


Pull request description:
Support adding tags to nodebalancers
### 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?

Support adding tags to a nodebalancer - supply a comma-separated string in an annotation to a service of type `LoadBalancer` to automatically add tags to the created node-balancer resource. Fixes #137 


### Pull Request Guidelines:

1. [x] Does your submission pass tests?
1. [x] 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/)




Patch:
diff --git a/README.md b/README.md
index 5c29627..118d75f 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,7 @@ Annotation (Suffix) | Values | Default | Description
 `preserve` | [bool](#annotation-bool-values) | `false` | When `true`, deleting a `LoadBalancer` service does not delete the underlying NodeBalancer. This will also prevent deletion of the former LoadBalancer when another one is specified with the `nodebalancer-id` annotation.
 `nodebalancer-id` | string | | The ID of the NodeBalancer to front the service. When not specified, a new NodeBalancer will be created. This can be configured on service creation or patching
 `hostname-only-ingress` | [bool](#annotation-bool-values) | `false` | When `true`, the LoadBalancerStatus for the service will only contain the Hostname. This is useful for bypassing kube-proxy's rerouting of in-cluster requests originally intended for the external LoadBalancer to the service's constituent pod IPs.
+`tags` | string | | A comma seperated list of tags to be applied to the createad NodeBalancer instance
 
 #### Deprecated Annotations
 
diff --git a/cloud/linode/loadbalancers.go b/cloud/linode/loadbalancers.go
index b3e4cda..6248796 100644
--- a/cloud/linode/loadbalancers.go
+++ b/cloud/linode/loadbalancers.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net/http"
 	"os"
+	"reflect"
 	"strconv"
 	"strings"
 	"time"
@@ -47,6 +48,7 @@ const (
 	annLinodeNodeBalancerID       = "service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id"
 
 	annLinodeHostnameOnlyIngress = "service.beta.kubernetes.io/linode-loadbalancer-hostname-only-ingress"
+	annLinodeLoadBalancerTags    = "service.beta.kubernetes.io/linode-loadbalancer-tags"
 )
 
 type lbNotFoundError struct {
@@ -266,6 +268,17 @@ func (l *loadbalancers) updateNodeBalancer(ctx context.Context, service *v1.Serv
 		}
 	}
 
+	tags := l.getLoadbalancerTags(ctx, service)
+	if !reflect.DeepEqual(nb.Tags, tags) {
+		update := nb.GetUpdateOptions()
+		update.Tags = &tags
+		nb, err = l.client.UpdateNodeBalancer(ctx, nb.ID, update)
+		if err != nil {
+			sentry.CaptureError(ctx, err)
+			return err
+		}
+	}
+
 	// Get all of the NodeBalancer's configs
 	nbCfgs, err := l.client.ListNodeBalancerConfigs(ctx, nb.ID, nil)
 	if err != nil {
@@ -480,15 +493,25 @@ func (l *loadbalancers) getNodeBalancerByID(ctx context.Context, service *v1.Ser
 	return nb, nil
 }
 
+func (l *loadbalancers) getLoadbalancerTags(ctx context.Context, service *v1.Service) []string {
+	tagStr, ok := getServiceAnnotation(service, annLinodeLoadBalancerTags)
+	if ok {
+		return strings.Split(tagStr, ",")
+	}
+	return []string{}
+}
+
 func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName string, service *v1.Service, configs []*linodego.NodeBalancerConfigCreateOptions) (lb *linodego.NodeBalancer, err error) {
 	connThrottle := getConnectionThrottle(service)
 
 	label := l.GetLoadBalancerName(ctx, clusterName, service)
+	tags := l.getLoadbalancerTags(ctx, service)
 	createOpts := linodego.NodeBalancerCreateOptions{
 		Label:              &label,
 		Region:             l.zone,
 		ClientConnThrottle: &connThrottle,
 		Configs:            configs,
+		Tags:               tags,
 	}
 	return l.client.CreateNodeBalancer(ctx, createOpts)
 }


Test patch:
diff --git a/cloud/linode/fake_linode_test.go b/cloud/linode/fake_linode_test.go
index cf3e7d4..59b326e 100644
--- a/cloud/linode/fake_linode_test.go
+++ b/cloud/linode/fake_linode_test.go
@@ -251,6 +251,7 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				Region:   nbco.Region,
 				IPv4:     &ip,
 				Hostname: &hostname,
+				Tags:     nbco.Tags,
 			}
 
 			if nbco.ClientConnThrottle != nil {
@@ -561,6 +562,9 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				if nbuo.Label != nil {
 					nb.Label = nbuo.Label
 				}
+				if nbuo.Tags != nil {
+					nb.Tags = *nbuo.Tags
+				}
 
 				f.nb[strconv.Itoa(nb.ID)] = nb
 				resp, err := json.Marshal(nb)
diff --git a/cloud/linode/loadbalancers_test.go b/cloud/linode/loadbalancers_test.go
index f4ad2c6..a15b916 100644
--- a/cloud/linode/loadbalancers_test.go
+++ b/cloud/linode/loadbalancers_test.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"reflect"
 	"strconv"
+	"strings"
 	"testing"
 
 	"github.com/linode/linodego"
@@ -127,6 +128,10 @@ func TestCCMLoadBalancers(t *testing.T) {
 			name: "Update Load Balancer - Add TLS Port",
 			f:    testUpdateLoadBalancerAddTLSPort,
 		},
+		{
+			name: "Update Load Balancer - Add Tags",
+			f:    testUpdateLoadBalancerAddTags,
+		},
 		{
 			name: "Update Load Balancer - Specify NodeBalancerID",
 			f:    testUpdateLoadBalancerAddNodeBalancerID,
@@ -201,7 +206,8 @@ func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI) {
 			Name: randString(10),
 			UID:  "foobar123",
 			Annotations: map[string]string{
-				annLinodeThrottle: "15",
+				annLinodeThrottle:         "15",
+				annLinodeLoadBalancerTags: "fake,test,yolo",
 			},
 		},
 		Spec: v1.ServiceSpec{
@@ -264,6 +270,13 @@ func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI) {
 		t.Logf("actual: %v", nb.ClientConnThrottle)
 	}
 
+	expectedTags := []string{"fake", "test", "yolo"}
+	if !reflect.DeepEqual(nb.Tags, expectedTags) {
+		t.Error("unexpected Tags")
+		t.Logf("expected: %v", expectedTags)
+		t.Logf("actual: %v", nb.Tags)
+	}
+
 	defer func() { _ = lb.EnsureLoadBalancerDeleted(context.TODO(), "linodelb", svc) }()
 }
 
@@ -415,6 +428,74 @@ func testUpdateLoadBalancerAddPortAnnotation(t *testing.T, client *linodego.Clie
 	}
 }
 
+func testUpdateLoadBalancerAddTags(t *testing.T, client *linodego.Client, _ *fakeAPI) {
+	svc := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        randString(10),
+			UID:         "foobar123",
+			Annotations: map[string]string{},
+		},
+		Spec: v1.ServiceSpec{
+			Ports: []v1.ServicePort{
+				{
+					Name:     randString(10),
+					Protocol: "TCP",
+					Port:     int32(80),
+					NodePort: int32(30000),
+				},
+			},
+		},
+	}
+
+	nodes := []*v1.Node{
+		&v1.Node{
+			Status: v1.NodeStatus{
+				Addresses: []v1.NodeAddress{
+					{
+						Type:    v1.NodeInternalIP,
+						Address: "127.0.0.1",
+					},
+				},
+			},
+		},
+	}
+
+	lb := &loadbalancers{client, "us-west", nil}
+	fakeClientset := fake.NewSimpleClientset()
+	lb.kubeClient = fakeClientset
+
+	defer lb.EnsureLoadBalancerDeleted(context.TODO(), "linodelb", svc)
+
+	lbStatus, err := lb.EnsureLoadBalancer(context.TODO(), "linodelb", svc, nodes)
+	if err != nil {
+		t.Errorf("EnsureLoadBalancer returned an error: %s", err)
+	}
+	svc.Status.LoadBalancer = *lbStatus
+	stubService(fakeClientset, svc)
+
+	testTags := "test,new,tags"
+	svc.ObjectMeta.SetAnnotations(map[string]string{
+		annLinodeLoadBalancerTags: testTags,
+	})
+
+	err = lb.UpdateLoadBalancer(context.TODO(), "linodelb", svc, nodes)
+	if err != nil {
+		t.Fatalf("UpdateLoadBalancer returned an error while updated annotations: %s", err)
+	}
+
+	nb, err := lb.getNodeBalancerByStatus(context.TODO(), svc)
+	if err != nil {
+		t.Fatalf("failed to get NodeBalancer by status: %v", err)
+	}
+
+	expectedTags := strings.Split(testTags, ",")
+	observedTags := nb.Tags
+
+	if !reflect.DeepEqual(expectedTags, observedTags) {
+		t.Errorf("NodeBalancer tags mismatch: expected %v, got %v", expectedTags, observedTags)
+	}
+}
+
 func testUpdateLoadBalancerAddTLSPort(t *testing.T, client *linodego.Client, _ *fakeAPI) {
 	svc := &v1.Service{
 		ObjectMeta: metav1.ObjectMeta{