Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cloud/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (

AnnLinodeLoadBalancerPreserve = "service.beta.kubernetes.io/linode-loadbalancer-preserve"
AnnLinodeNodeBalancerID = "service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-id"
AnnLinodeNodeBalancerType = "service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-type"

AnnLinodeHostnameOnlyIngress = "service.beta.kubernetes.io/linode-loadbalancer-hostname-only-ingress"
AnnLinodeLoadBalancerTags = "service.beta.kubernetes.io/linode-loadbalancer-tags"
Expand Down
1 change: 1 addition & 0 deletions cloud/linode/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var Options struct {
IpHolderSuffix string
LinodeExternalNetwork *net.IPNet
NodeBalancerTags []string
DefaultNBType string
GlobalStopChannel chan<- struct{}
}

Expand Down
12 changes: 12 additions & 0 deletions cloud/linode/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -627,17 +627,29 @@ func (l *loadbalancers) GetLoadBalancerTags(_ context.Context, clusterName strin
return tags
}

// GetLinodeNBType returns the NodeBalancer type for the service.
func (l *loadbalancers) GetLinodeNBType(service *v1.Service) linodego.NodeBalancerPlanType {
typeStr, ok := service.GetAnnotations()[annotations.AnnLinodeNodeBalancerType]
if ok && linodego.NodeBalancerPlanType(typeStr) == linodego.NBTypePremium {
return linodego.NBTypePremium
}

return linodego.NodeBalancerPlanType(Options.DefaultNBType)
}

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

fwid, ok := service.GetAnnotations()[annotations.AnnLinodeCloudFirewallID]
Expand Down
100 changes: 100 additions & 0 deletions cloud/linode/loadbalancers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"strings"
"testing"

ciliumclient "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1"
"github.com/linode/linodego"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -27,6 +28,7 @@ import (
"k8s.io/client-go/kubernetes/fake"

"github.com/linode/linode-cloud-controller-manager/cloud/annotations"
"github.com/linode/linode-cloud-controller-manager/cloud/linode/client"
"github.com/linode/linode-cloud-controller-manager/cloud/linode/firewall"
)

Expand Down Expand Up @@ -3675,3 +3677,101 @@ func Test_LoadbalNodeNameCoercion(t *testing.T) {
}
}
}

func Test_loadbalancers_GetLinodeNBType(t *testing.T) {
type fields struct {
client client.Client
zone string
kubeClient kubernetes.Interface
ciliumClient ciliumclient.CiliumV2alpha1Interface
loadBalancerType string
}
type args struct {
service *v1.Service
}
tests := []struct {
name string
fields fields
args args
defaultNB linodego.NodeBalancerPlanType
want linodego.NodeBalancerPlanType
}{
{
name: "No annotation in service and common as default",
fields: fields{
client: nil,
zone: "",
kubeClient: nil,
ciliumClient: nil,
loadBalancerType: "nodebalancer",
},
args: args{
service: &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Annotations: map[string]string{},
},
},
},
defaultNB: linodego.NBTypeCommon,
want: linodego.NBTypeCommon,
},
{
name: "No annotation in service and premium as default",
fields: fields{
client: nil,
zone: "",
kubeClient: nil,
ciliumClient: nil,
loadBalancerType: "nodebalancer",
},
args: args{
service: &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Annotations: map[string]string{},
},
},
},
defaultNB: linodego.NBTypePremium,
want: linodego.NBTypePremium,
},
{
name: "Nodebalancer type annotation in service",
fields: fields{
client: nil,
zone: "",
kubeClient: nil,
ciliumClient: nil,
loadBalancerType: "nodebalancer",
},
args: args{
service: &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Annotations: map[string]string{
annotations.AnnLinodeNodeBalancerType: string(linodego.NBTypePremium),
},
},
},
},
defaultNB: linodego.NBTypeCommon,
want: linodego.NBTypePremium,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &loadbalancers{
client: tt.fields.client,
zone: tt.fields.zone,
kubeClient: tt.fields.kubeClient,
ciliumClient: tt.fields.ciliumClient,
loadBalancerType: tt.fields.loadBalancerType,
}
Options.DefaultNBType = string(tt.defaultNB)
if got := l.GetLinodeNBType(tt.args.service); !reflect.DeepEqual(got, tt.want) {
t.Errorf("loadbalancers.GetLinodeNBType() = %v, want %v", got, tt.want)
}
})
}
}
3 changes: 3 additions & 0 deletions deploy/chart/templates/daemonset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ spec:
{{- if .Values.allowUnauthorizedMetrics }}
- --authorization-always-allow-paths="/metrics"
{{- end }}
{{- if .Values.defaultNBType }}
- --default-nodebalancer-type={{ .Values.defaultNBType }}
{{- end }}
{{- with .Values.containerSecurityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
Expand Down
3 changes: 3 additions & 0 deletions deploy/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ tolerations:
# Enable Linode token health checker
# tokenHealthChecker: true

# Default NodeBalancer type to create("common" or "premium"). Default is "common"
# defaultNBType: "common"

# This section adds the ability to pass environment variables to adjust CCM defaults
# https://github.com/linode/linode-cloud-controller-manager/blob/master/cloud/linode/loadbalancers.go
# LINODE_HOSTNAME_ONLY_INGRESS type bool is supported
Expand Down
9 changes: 9 additions & 0 deletions docs/configuration/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ For implementation details, see:
| `tags` | string | | A comma separated list of tags to be applied to the NodeBalancer instance |
| `firewall-id` | string | | An existing Cloud Firewall ID to be attached to the NodeBalancer instance. See [Firewall Setup](firewall.md) |
| `firewall-acl` | string | | The Firewall rules to be applied to the NodeBalancer. See [Firewall Configuration](#firewall-configuration) |
| `nodebalancer-type` | string | | The type of NodeBalancer to create (options: common, premium). See [NodeBalancer Types](#nodebalancer-type) |

### Port Specific Configuration

Expand Down Expand Up @@ -104,6 +105,14 @@ metadata:
}
```

### NodeBalancer Type
Linode supports nodebalancers of different types: common and premium. By default, nodebalancers of type common are provisioned. If an account is allowed to provision premium nodebalancers and one wants to use them, it can be achieved by specifying the annotation:
```yaml
metadata:
annotations:
service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-type: premium
```

For more examples and detailed configuration options, see:
- [LoadBalancer Configuration](loadbalancer.md)
- [Firewall Configuration](firewall.md)
Expand Down
93 changes: 93 additions & 0 deletions e2e/test/lb-premium-nb/chainsaw-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
name: lb-premium-nb
labels:
all:
spec:
namespace: "lb-premium-nb"
steps:
- name: Create pods and services
try:
- apply:
file: create-pods-services.yaml
catch:
- describe:
apiVersion: v1
kind: Pod
- describe:
apiVersion: v1
kind: Service
- name: Check that loadbalancer ip is assigned
try:
- assert:
resource:
apiVersion: v1
kind: Service
metadata:
name: svc-test
status:
(loadBalancer.ingress[0].ip != null): true
- name: Fetch loadbalancer ip and check both pods reachable
try:
- script:
content: |
set -e
IP=$(kubectl get svc svc-test -n $NAMESPACE -o json | jq -r .status.loadBalancer.ingress[0].ip)

podnames=()

for i in {1..10}; do
if [[ ${#podnames[@]} -lt 2 ]]; then
output=$(curl -s $IP:80 | jq -e .podName || true)

if [[ "$output" == *"test-"* ]]; then
unique=true
for i in "${array[@]}"; do
if [[ "$i" == "$output" ]]; then
unique=false
break
fi
done
if [[ "$unique" == true ]]; then
podnames+=($output)
fi
fi
else
break
fi
sleep 10
done

if [[ ${#podnames[@]} -lt 2 ]]; then
echo "all pods failed to respond"
else
echo "all pods responded"
fi
check:
($error == null): true
(contains($stdout, 'all pods responded')): true
- name: Check nodebalancer type
try:
- script:
content: |
set -e

nbid=$(KUBECONFIG=$KUBECONFIG NAMESPACE=$NAMESPACE LINODE_TOKEN=$LINODE_TOKEN ../scripts/get-nb-id.sh)
for i in {1..10}; do
type=$(curl -s --request GET \
-H "Authorization: Bearer $LINODE_TOKEN" \
-H "Content-Type: application/json" \
-H "accept: application/json" \
"https://api.linode.com/v4/nodebalancers/${nbid}" | jq -r '.type')

if [[ $type == "premium" ]]; then
echo "nodebalancer type is premium"
break
fi
sleep 5
done
check:
($error == null): true
(contains($stdout, 'nodebalancer type is premium')): true
61 changes: 61 additions & 0 deletions e2e/test/lb-premium-nb/create-pods-services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: lb-premium-nb
name: test
spec:
replicas: 2
selector:
matchLabels:
app: lb-premium-nb
template:
metadata:
labels:
app: lb-premium-nb
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- simple-lb
topologyKey: kubernetes.io/hostname
weight: 100
containers:
- image: appscode/test-server:2.3
name: test
ports:
- name: http-1
containerPort: 8080
protocol: TCP
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
---
apiVersion: v1
kind: Service
metadata:
name: svc-test
annotations:
service.beta.kubernetes.io/linode-loadbalancer-nodebalancer-type: premium
labels:
app: lb-premium-nb
spec:
type: LoadBalancer
selector:
app: lb-premium-nb
ports:
- name: http-1
protocol: TCP
port: 80
targetPort: 8080
sessionAffinity: None
Loading
Loading