diff --git a/pkg/kloudlb/controller/controller.go b/pkg/kloudlb/controller/controller.go index 3526955..fe51697 100644 --- a/pkg/kloudlb/controller/controller.go +++ b/pkg/kloudlb/controller/controller.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "time" corev1 "k8s.io/api/core/v1" @@ -163,10 +164,15 @@ func (c *Controller) sync(ctx context.Context, key string) error { if ingressIP(svc) != "" { return nil } + ports, err := servicePortsToAllocateRules(svc) + if err != nil { + return err + } lb, err := c.lbClient.Allocate(ctx, provisioner.AllocateRequest{ Namespace: namespace, Name: name, + Ports: ports, }) if err != nil { return err @@ -217,6 +223,29 @@ func ingressIP(svc *corev1.Service) string { return "" } +func servicePortsToAllocateRules(svc *corev1.Service) ([]provisioner.AllocatePortRule, error) { + if svc == nil { + return nil, fmt.Errorf("service is required") + } + if len(svc.Spec.Ports) == 0 { + return nil, fmt.Errorf("service %s/%s has no ports", svc.Namespace, svc.Name) + } + out := make([]provisioner.AllocatePortRule, 0, len(svc.Spec.Ports)) + for _, p := range svc.Spec.Ports { + protocol := string(p.Protocol) + if protocol == "" { + protocol = string(corev1.ProtocolTCP) + } + switch protocol { + case string(corev1.ProtocolTCP), string(corev1.ProtocolUDP): + out = append(out, provisioner.AllocatePortRule{Protocol: strings.ToLower(protocol), PortFrom: int(p.Port)}) + default: + return nil, fmt.Errorf("service %s/%s has unsupported protocol %q for load balancer firewall", svc.Namespace, svc.Name, protocol) + } + } + return out, nil +} + func containsString(items []string, target string) bool { for _, item := range items { if item == target { diff --git a/pkg/kloudlb/controller/ports_test.go b/pkg/kloudlb/controller/ports_test.go new file mode 100644 index 0000000..15b6364 --- /dev/null +++ b/pkg/kloudlb/controller/ports_test.go @@ -0,0 +1,47 @@ +package controller + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestServicePortsToAllocateRules(t *testing.T) { + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "web"}, + Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 80, Protocol: corev1.ProtocolTCP}, {Port: 53, Protocol: corev1.ProtocolUDP}}}, + } + + rules, err := servicePortsToAllocateRules(svc) + if err != nil { + t.Fatalf("servicePortsToAllocateRules: %v", err) + } + if len(rules) != 2 { + t.Fatalf("rules count = %d, want 2", len(rules)) + } + if rules[0].Protocol != "tcp" || rules[0].PortFrom != 80 { + t.Fatalf("unexpected first rule: %+v", rules[0]) + } + if rules[1].Protocol != "udp" || rules[1].PortFrom != 53 { + t.Fatalf("unexpected second rule: %+v", rules[1]) + } +} + +func TestServicePortsToAllocateRulesRejectsUnsupportedProtocol(t *testing.T) { + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "web"}, + Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 80, Protocol: corev1.ProtocolSCTP}}}, + } + + if _, err := servicePortsToAllocateRules(svc); err == nil { + t.Fatal("expected error for unsupported protocol") + } +} + +func TestServicePortsToAllocateRulesRejectsNoPorts(t *testing.T) { + svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "web"}} + if _, err := servicePortsToAllocateRules(svc); err == nil { + t.Fatal("expected error for empty ports") + } +} diff --git a/pkg/lb/provisioner/types.go b/pkg/lb/provisioner/types.go index 0672f20..01bb48d 100644 --- a/pkg/lb/provisioner/types.go +++ b/pkg/lb/provisioner/types.go @@ -10,4 +10,11 @@ type LoadBalancer struct { type AllocateRequest struct { Namespace string `json:"namespace"` Name string `json:"name"` + Ports []AllocatePortRule `json:"ports,omitempty"` +} + +type AllocatePortRule struct { + Protocol string `json:"protocol"` + PortFrom int `json:"portFrom"` + PortTo *int `json:"portTo,omitempty"` }