Skip to content

Commit

Permalink
Merge branch 'master' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
luthermonson committed Dec 6, 2023
2 parents af2f05c + af0a0b4 commit f5f23c3
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 80 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Annotation (Suffix) | Values | Default | Description
`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
`firewall-id` | string | | The Firewall ID that's applied to the NodeBalancer instance.

#### Deprecated Annotations
These annotations are deprecated, and will be removed in a future release.
Expand Down
44 changes: 26 additions & 18 deletions cloud/linode/fake_linode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/linode/linodego"
)

const apiVersion = "v4"

type fakeAPI struct {
t *testing.T
nb map[string]*linodego.NodeBalancer
Expand Down Expand Up @@ -46,12 +48,12 @@ func (f *fakeAPI) ResetRequests() {
f.requests = make(map[fakeRequest]struct{})
}

func (f *fakeAPI) recordRequest(r *http.Request) {
func (f *fakeAPI) recordRequest(r *http.Request, urlPath string) {
bodyBytes, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
f.requests[fakeRequest{
Path: r.URL.Path,
Path: urlPath,
Method: r.Method,
Body: string(bodyBytes),
}] = struct{}{}
Expand All @@ -67,10 +69,16 @@ func (f *fakeAPI) didRequestOccur(method, path, body string) bool {
}

func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.recordRequest(r)

w.Header().Set("Content-Type", "application/json")
urlPath := r.URL.Path

if !strings.HasPrefix(urlPath, "/"+apiVersion) {
http.Error(w, "not found", http.StatusNotFound)
return
}
urlPath = strings.TrimPrefix(urlPath, "/"+apiVersion)
f.recordRequest(r, urlPath)

switch r.Method {
case "GET":
whichAPI := strings.Split(urlPath[1:], "/")
Expand Down Expand Up @@ -99,7 +107,7 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rx, _ = regexp.Compile("/nodebalancers/[0-9]+/configs/[0-9]+/nodes")
if rx.MatchString(urlPath) {
res := 0
parts := strings.Split(r.URL.Path[1:], "/")
parts := strings.Split(urlPath[1:], "/")
nbcID, err := strconv.Atoi(parts[3])
if err != nil {
f.t.Fatal(err)
Expand Down Expand Up @@ -236,7 +244,7 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

case "POST":
tp := filepath.Base(r.URL.Path)
tp := filepath.Base(urlPath)
if tp == "nodebalancers" {
nbco := linodego.NodeBalancerCreateOptions{}
if err := json.NewDecoder(r.Body).Decode(&nbco); err != nil {
Expand Down Expand Up @@ -313,7 +321,7 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return

} else if tp == "rebuild" {
parts := strings.Split(r.URL.Path[1:], "/")
parts := strings.Split(urlPath[1:], "/")
nbcco := new(linodego.NodeBalancerConfigRebuildOptions)
if err := json.NewDecoder(r.Body).Decode(nbcco); err != nil {
f.t.Fatal(err)
Expand Down Expand Up @@ -382,7 +390,7 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(resp)
return
} else if tp == "configs" {
parts := strings.Split(r.URL.Path[1:], "/")
parts := strings.Split(urlPath[1:], "/")
nbcco := new(linodego.NodeBalancerConfigCreateOptions)
if err := json.NewDecoder(r.Body).Decode(nbcco); err != nil {
f.t.Fatal(err)
Expand Down Expand Up @@ -422,7 +430,7 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(resp)
return
} else if tp == "nodes" {
parts := strings.Split(r.URL.Path[1:], "/")
parts := strings.Split(urlPath[1:], "/")
nbnco := new(linodego.NodeBalancerNodeCreateOptions)
if err := json.NewDecoder(r.Body).Decode(nbnco); err != nil {
f.t.Fatal(err)
Expand Down Expand Up @@ -454,22 +462,22 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
case "DELETE":
idRaw := filepath.Base(r.URL.Path)
idRaw := filepath.Base(urlPath)
id, err := strconv.Atoi(idRaw)
if err != nil {
f.t.Fatal(err)
}
if strings.Contains(r.URL.Path, "nodes") {
if strings.Contains(urlPath, "nodes") {
delete(f.nbn, idRaw)
} else if strings.Contains(r.URL.Path, "configs") {
} else if strings.Contains(urlPath, "configs") {
delete(f.nbc, idRaw)

for k, n := range f.nbn {
if n.ConfigID == id {
delete(f.nbn, k)
}
}
} else if strings.Contains(r.URL.Path, "nodebalancers") {
} else if strings.Contains(urlPath, "nodebalancers") {
delete(f.nb, idRaw)

for k, c := range f.nbc {
Expand All @@ -485,10 +493,10 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
case "PUT":
if strings.Contains(r.URL.Path, "nodes") {
if strings.Contains(urlPath, "nodes") {
f.t.Fatal("PUT ...nodes is not supported by the mock API")
} else if strings.Contains(r.URL.Path, "configs") {
parts := strings.Split(r.URL.Path[1:], "/")
} else if strings.Contains(urlPath, "configs") {
parts := strings.Split(urlPath[1:], "/")
nbcco := new(linodego.NodeBalancerConfigUpdateOptions)
if err := json.NewDecoder(r.Body).Decode(nbcco); err != nil {
f.t.Fatal(err)
Expand Down Expand Up @@ -545,8 +553,8 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
_, _ = w.Write(resp)
return
} else if strings.Contains(r.URL.Path, "nodebalancer") {
parts := strings.Split(r.URL.Path[1:], "/")
} else if strings.Contains(urlPath, "nodebalancer") {
parts := strings.Split(urlPath[1:], "/")
nbuo := new(linodego.NodeBalancerUpdateOptions)
if err := json.NewDecoder(r.Body).Decode(nbuo); err != nil {
f.t.Fatal(err)
Expand Down
27 changes: 19 additions & 8 deletions cloud/linode/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const (

annLinodeHostnameOnlyIngress = "service.beta.kubernetes.io/linode-loadbalancer-hostname-only-ingress"
annLinodeLoadBalancerTags = "service.beta.kubernetes.io/linode-loadbalancer-tags"
annLinodeCloudFirewallID = "service.beta.kubernetes.io/linode-loadbalancer-firewall-id"

annLinodeNodePrivateIP = "node.k8s.linode.com/private-ip"
)
Expand Down Expand Up @@ -238,7 +239,7 @@ func (l *loadbalancers) EnsureLoadBalancer(ctx context.Context, clusterName stri
klog.Infof("created new NodeBalancer (%d) for service (%s)", nb.ID, serviceNn)

case nil:
if err = l.updateNodeBalancer(ctx, service, nodes, nb); err != nil {
if err = l.updateNodeBalancer(ctx, clusterName, service, nodes, nb); err != nil {
sentry.CaptureError(ctx, err)
return nil, err
}
Expand All @@ -262,7 +263,7 @@ func (l *loadbalancers) EnsureLoadBalancer(ctx context.Context, clusterName stri
}

//nolint:funlen
func (l *loadbalancers) updateNodeBalancer(ctx context.Context, service *v1.Service, nodes []*v1.Node, nb *linodego.NodeBalancer) (err error) {
func (l *loadbalancers) updateNodeBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node, nb *linodego.NodeBalancer) (err error) {
if len(nodes) == 0 {
return fmt.Errorf("%w: service %s", errNoNodesAvailable, getServiceNn(service))
}
Expand All @@ -279,7 +280,7 @@ func (l *loadbalancers) updateNodeBalancer(ctx context.Context, service *v1.Serv
}
}

tags := l.getLoadBalancerTags(ctx, service)
tags := l.getLoadBalancerTags(ctx, clusterName, service)
if !reflect.DeepEqual(nb.Tags, tags) {
update := nb.GetUpdateOptions()
update.Tags = &tags
Expand Down Expand Up @@ -391,7 +392,7 @@ func (l *loadbalancers) UpdateLoadBalancer(ctx context.Context, clusterName stri
}
}

return l.updateNodeBalancer(ctx, serviceWithStatus, nodes, nb)
return l.updateNodeBalancer(ctx, clusterName, serviceWithStatus, nodes, nb)
}

// Delete any NodeBalancer configs for ports that no longer exist on the Service
Expand Down Expand Up @@ -504,26 +505,36 @@ func (l *loadbalancers) getNodeBalancerByID(ctx context.Context, service *v1.Ser
return nb, nil
}

func (l *loadbalancers) getLoadBalancerTags(_ context.Context, service *v1.Service) []string {
func (l *loadbalancers) getLoadBalancerTags(_ context.Context, clusterName string, service *v1.Service) []string {
tags := []string{clusterName}
tagStr, ok := getServiceAnnotation(service, annLinodeLoadBalancerTags)
if ok {
return strings.Split(tagStr, ",")
return append(tags, strings.Split(tagStr, ",")...)
}
return []string{}
return tags
}

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)
tags := l.getLoadBalancerTags(ctx, clusterName, service)
createOpts := linodego.NodeBalancerCreateOptions{
Label: &label,
Region: l.zone,
ClientConnThrottle: &connThrottle,
Configs: configs,
Tags: tags,
}

fwid, ok := getServiceAnnotation(service, annLinodeCloudFirewallID)
if ok {
firewallID, err := strconv.Atoi(fwid)
if err != nil {
return nil, err
}
createOpts.FirewallID = firewallID
}
return l.client.CreateNodeBalancer(ctx, createOpts)
}

Expand Down
71 changes: 51 additions & 20 deletions cloud/linode/loadbalancers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,16 @@ func TestCCMLoadBalancers(t *testing.T) {
f: testGetLoadBalancer,
},
{
name: "Create Load Balancer",
f: testCreateNodeBalancer,
name: "Create Load Balancer Without Firewall",
f: testCreateNodeBalancerWithOutFirewall,
},
{
name: "Create Load Balancer With Valid Firewall ID",
f: testCreateNodeBalancerWithFirewall,
},
{
name: "Create Load Balancer With Invalid Firewall ID",
f: testCreateNodeBalancerWithInvalidFirewall,
},
{
name: "Update Load Balancer - Add Annotation",
Expand Down Expand Up @@ -205,7 +213,7 @@ func stubService(fake *fake.Clientset, service *v1.Service) {
fake.CoreV1().Services("").Create(context.TODO(), service, metav1.CreateOptions{})
}

func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI) {
func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI, firewallID *string) error {
svc := &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: randString(10),
Expand Down Expand Up @@ -233,14 +241,19 @@ func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI) {
},
}

if firewallID != nil {
svc.Annotations[annLinodeCloudFirewallID] = *firewallID
}

lb := &loadbalancers{client, "us-west", nil}
nodes := []*v1.Node{
{ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
}
nb, err := lb.buildLoadBalancerRequest(context.TODO(), "linodelb", svc, nodes)
if err != nil {
t.Fatal(err)
return err
}

if nb.Region != lb.zone {
t.Error("unexpected nodebalancer region")
t.Logf("expected: %s", lb.zone)
Expand All @@ -249,7 +262,7 @@ func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI) {

configs, err := client.ListNodeBalancerConfigs(context.TODO(), nb.ID, nil)
if err != nil {
t.Fatal(err)
return err
}

if len(configs) != len(svc.Spec.Ports) {
Expand All @@ -258,17 +271,9 @@ func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI) {
t.Logf("actual: %v", len(configs))
}

if !reflect.DeepEqual(err, nil) {
t.Error("unexpected error")
t.Logf("expected: %v", nil)
t.Logf("actual: %v", err)
}

nb, err = client.GetNodeBalancer(context.TODO(), nb.ID)
if !reflect.DeepEqual(err, nil) {
t.Error("unexpected error")
t.Logf("expected: %v", nil)
t.Logf("actual: %v", err)
if err != nil {
return err
}

if nb.ClientConnThrottle != 15 {
Expand All @@ -277,14 +282,39 @@ func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI) {
t.Logf("actual: %v", nb.ClientConnThrottle)
}

expectedTags := []string{"fake", "test", "yolo"}
expectedTags := []string{"linodelb", "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) }()
return nil
}

func testCreateNodeBalancerWithOutFirewall(t *testing.T, client *linodego.Client, f *fakeAPI) {
err := testCreateNodeBalancer(t, client, f, nil)
if err != nil {
t.Fatalf("expected a nil error, got %v", err)
}
}

func testCreateNodeBalancerWithFirewall(t *testing.T, client *linodego.Client, f *fakeAPI) {
firewallID := "123"
err := testCreateNodeBalancer(t, client, f, &firewallID)
if err != nil {
t.Fatalf("expected a nil error, got %v", err)
}
}

func testCreateNodeBalancerWithInvalidFirewall(t *testing.T, client *linodego.Client, f *fakeAPI) {
firewallID := "qwerty"
expectedError := "strconv.Atoi: parsing \"qwerty\": invalid syntax"
err := testCreateNodeBalancer(t, client, f, &firewallID)
if err.Error() != expectedError {
t.Fatalf("expected a %s error, got %v", expectedError, err)
}
}

func testUpdateLoadBalancerAddAnnotation(t *testing.T, client *linodego.Client, _ *fakeAPI) {
Expand Down Expand Up @@ -470,10 +500,11 @@ func testUpdateLoadBalancerAddTags(t *testing.T, client *linodego.Client, _ *fak
lb := &loadbalancers{client, "us-west", nil}
fakeClientset := fake.NewSimpleClientset()
lb.kubeClient = fakeClientset
clusterName := "linodelb"

defer lb.EnsureLoadBalancerDeleted(context.TODO(), "linodelb", svc)
defer lb.EnsureLoadBalancerDeleted(context.TODO(), clusterName, svc)

lbStatus, err := lb.EnsureLoadBalancer(context.TODO(), "linodelb", svc, nodes)
lbStatus, err := lb.EnsureLoadBalancer(context.TODO(), clusterName, svc, nodes)
if err != nil {
t.Errorf("EnsureLoadBalancer returned an error: %s", err)
}
Expand All @@ -485,7 +516,7 @@ func testUpdateLoadBalancerAddTags(t *testing.T, client *linodego.Client, _ *fak
annLinodeLoadBalancerTags: testTags,
})

err = lb.UpdateLoadBalancer(context.TODO(), "linodelb", svc, nodes)
err = lb.UpdateLoadBalancer(context.TODO(), clusterName, svc, nodes)
if err != nil {
t.Fatalf("UpdateLoadBalancer returned an error while updated annotations: %s", err)
}
Expand All @@ -495,7 +526,7 @@ func testUpdateLoadBalancerAddTags(t *testing.T, client *linodego.Client, _ *fak
t.Fatalf("failed to get NodeBalancer by status: %v", err)
}

expectedTags := strings.Split(testTags, ",")
expectedTags := append([]string{clusterName}, strings.Split(testTags, ",")...)
observedTags := nb.Tags

if !reflect.DeepEqual(expectedTags, observedTags) {
Expand Down
Loading

0 comments on commit f5f23c3

Please sign in to comment.