Skip to content
Open
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
6 changes: 3 additions & 3 deletions docs/reference/load_balancer_annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ This page contains all annotations, which can be specified at a Service of type
| `load-balancer.hetzner.cloud/node-selector` | `string` | `-` | `No` | Can be set to restrict which Nodes are added as targets to the Load Balancer. It accepts a Kubernetes label selector string, using either the set-based or equality-based formats. If the selector can not be parsed, the targets in the Load Balancer are not updated and an Event is created with the error message. Format: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors |
| `load-balancer.hetzner.cloud/uses-proxyprotocol` | `bool` | `false` | `No` | Specifies if the Load Balancer services should use the proxy protocol. |
| `load-balancer.hetzner.cloud/http-cookie-name` | `string` | `-` | `No` | Specifies the cookie name when using HTTP or HTTPS as protocol. |
| `load-balancer.hetzner.cloud/http-cookie-lifetime` | `int` | `-` | `No` | Specifies the lifetime of the HTTP cookie. |
| `load-balancer.hetzner.cloud/http-cookie-lifetime` | `duration` | `-` | `No` | Specifies the lifetime of the HTTP cookie. |
| `load-balancer.hetzner.cloud/http-timeout-idle` | `duration` | `-` | `No` | Specifies the idle timeout for the client and server side. Must be between 30s and 300s. |
| `load-balancer.hetzner.cloud/certificate-type` | `uploaded \| managed` | `uploaded` | `No` | Defines the type of certificate the Load Balancer should use. |
| `load-balancer.hetzner.cloud/http-certificates` | `string` | `-` | `No` | A comma separated list of IDs or Names of Certificates assigned to the service. |
Expand All @@ -38,8 +38,8 @@ This page contains all annotations, which can be specified at a Service of type
| `load-balancer.hetzner.cloud/http-sticky-sessions` | `bool` | `false` | `No` | Enables the sticky sessions feature of Hetzner Cloud HTTP Load Balancers. |
| `load-balancer.hetzner.cloud/health-check-protocol` | `tcp \| http \| https` | `tcp` | `No` | Sets the protocol the health check should be performed over. |
| `load-balancer.hetzner.cloud/health-check-port` | `int` | `-` | `No` | Specifies the port the health check is be performed on. |
| `load-balancer.hetzner.cloud/health-check-interval` | `int` | `-` | `No` | Specifies the interval in which time we perform a health check in seconds. |
| `load-balancer.hetzner.cloud/health-check-timeout` | `int` | `-` | `No` | Specifies the timeout of a single health check. |
| `load-balancer.hetzner.cloud/health-check-interval` | `duration` | `-` | `No` | Specifies the interval in which we perform a health check. |
| `load-balancer.hetzner.cloud/health-check-timeout` | `duration` | `-` | `No` | Specifies the timeout of a single health check. |
| `load-balancer.hetzner.cloud/health-check-retries` | `int` | `-` | `No` | Specifies the number of time a health check is retried until a target is marked as unhealthy. |
| `load-balancer.hetzner.cloud/health-check-http-domain` | `string` | `-` | `No` | Specifies the domain we try to access when performing the health check. |
| `load-balancer.hetzner.cloud/health-check-http-path` | `string` | `-` | `No` | Specifies the path we try to access when performing the health check. |
Expand Down
22 changes: 10 additions & 12 deletions hcloud/load_balancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,15 @@ func newLoadBalancers(lbOps LoadBalancerOps, lbCfg *config.LoadBalancerConfigura
}

func matchNodeSelector(svc *corev1.Service, nodes []*corev1.Node) ([]*corev1.Node, error) {
var (
err error
selectedNodes []*corev1.Node
)
var selectedNodes []*corev1.Node

selector := labels.Everything()
if v, ok := annotation.LBNodeSelector.StringFromService(svc); ok {
selector, err = labels.Parse(v)
if v, err := annotation.LBNodeSelector.FromService(svc); err == nil {
parsed, err := labels.Parse(v)
if err != nil {
return nil, fmt.Errorf("unable to parse the node-selector annotation: %w", err)
}
selector = parsed
}

for _, n := range nodes {
Expand All @@ -79,7 +77,7 @@ func (l *loadBalancers) GetLoadBalancer(
return nil, false, fmt.Errorf("%s: %w", op, err)
}

if v, ok := annotation.LBHostname.StringFromService(service); ok {
if v, err := annotation.LBHostname.FromService(service); err == nil {
return &corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{{Hostname: v}},
}, true, nil
Expand All @@ -94,7 +92,7 @@ func (l *loadBalancers) GetLoadBalancer(
}

func (l *loadBalancers) GetLoadBalancerName(_ context.Context, _ string, service *corev1.Service) string {
if v, ok := annotation.LBName.StringFromService(service); ok {
if v, err := annotation.LBName.FromService(service); err == nil {
return v
}
return cloudprovider.DefaultLoadBalancerName(service)
Expand Down Expand Up @@ -196,7 +194,7 @@ func (l *loadBalancers) EnsureLoadBalancer(

// Either set the Hostname or the IPs (below).
// See: https://github.com/kubernetes/kubernetes/issues/66607
if v, ok := annotation.LBHostname.StringFromService(svc); ok {
if v, err := annotation.LBHostname.FromService(svc); err == nil {
return &corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{{Hostname: v}},
}, nil
Expand Down Expand Up @@ -262,7 +260,7 @@ func (l *loadBalancers) buildLoadBalancerStatusIngress(lb *hcloud.LoadBalancer,
}

func (l *loadBalancers) getPrivateIngressEnabled(svc *corev1.Service) (bool, error) {
disable, err := annotation.LBDisablePrivateIngress.BoolFromService(svc)
disable, err := annotation.LBDisablePrivateIngress.FromService(svc)
if err == nil {
return !disable, nil
}
Expand All @@ -273,7 +271,7 @@ func (l *loadBalancers) getPrivateIngressEnabled(svc *corev1.Service) (bool, err
}

func (l *loadBalancers) getProxyProtocolEnabled(svc *corev1.Service) (bool, error) {
enable, err := annotation.LBSvcProxyProtocol.BoolFromService(svc)
enable, err := annotation.LBSvcProxyProtocol.FromService(svc)
if err == nil {
return enable, nil
}
Expand All @@ -287,7 +285,7 @@ func (l *loadBalancers) getProxyProtocolEnabled(svc *corev1.Service) (bool, erro
}

func (l *loadBalancers) getIPv6Enabled(svc *corev1.Service) (bool, error) {
disable, err := annotation.LBIPv6Disabled.BoolFromService(svc)
disable, err := annotation.LBIPv6Disabled.FromService(svc)
if err == nil {
return !disable, nil
}
Expand Down
161 changes: 161 additions & 0 deletions internal/annotation/annotation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Package annotation defines the Kubernetes annotations that configure the
// resources managed by the cloud controller manager.
//
// Every annotation is declared with the type of its value, so reading one
// yields that type and nothing else:
//
// const LBUsePrivateIP Bool = "load-balancer.hetzner.cloud/use-private-ip"
//
// usePrivateIP, err := LBUsePrivateIP.FromService(svc)
//
// Annotations are declared as constants of a named string type. The reference
// documentation in docs/reference is generated from those declarations by
// tools/doc_generation.go, which expects constants with a string literal.
package annotation

import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"

corev1 "k8s.io/api/core/v1"

"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

var ErrNotSet = errors.New("not set")

type Annotation[T any] interface {
FromService(svc *corev1.Service) (T, error)
}

type (
String string
Bool string
Int string
Duration string // e.g. "30s" or "1h"
Strings string // comma separated list of strings
IP string
Protocol string // Load Balancer service protocol
AlgorithmType string // Load Balancer algorithm type
// Certificates is an annotation holding a comma separated list of Certificates,
// referenced either by ID or by name.
Certificates string
)

func (a String) FromService(svc *corev1.Service) (string, error) {
return value(string(a), svc)
}

func (a Bool) FromService(svc *corev1.Service) (bool, error) {
return parse(string(a), svc, strconv.ParseBool)
}

func (a Int) FromService(svc *corev1.Service) (int, error) {
return parse(string(a), svc, strconv.Atoi)
}

func (a Duration) FromService(svc *corev1.Service) (time.Duration, error) {
return parse(string(a), svc, time.ParseDuration)
}

func (a Strings) FromService(svc *corev1.Service) ([]string, error) {
return parse(string(a), svc, func(v string) ([]string, error) {
return strings.Split(v, ","), nil
})
}

func (a IP) FromService(svc *corev1.Service) (net.IP, error) {
return parse(string(a), svc, parseIP)
}

func (a Protocol) FromService(svc *corev1.Service) (hcloud.LoadBalancerServiceProtocol, error) {
return parse(string(a), svc, parseServiceProtocol)
}

func (a AlgorithmType) FromService(svc *corev1.Service) (hcloud.LoadBalancerAlgorithmType, error) {
return parse(string(a), svc, parseAlgorithmType)
}

func (a Certificates) FromService(svc *corev1.Service) ([]*hcloud.Certificate, error) {
return parse(string(a), svc, parseCertificates)
}

// value returns the raw value of the annotation with name from svc.
func value(name string, svc *corev1.Service) (string, error) {
v, ok := svc.Annotations[name]
if !ok {
return "", fmt.Errorf("%s: %w", name, ErrNotSet)
}
return v, nil
}

func parse[T any](name string, svc *corev1.Service, convert func(string) (T, error)) (T, error) {
var zero T

v, err := value(name, svc)
if err != nil {
return zero, err
}

converted, err := convert(v)
if err != nil {
return zero, fmt.Errorf("%s: %w", name, err)
}

return converted, nil
}

func parseIP(v string) (net.IP, error) {
ip := net.ParseIP(v)
if ip == nil {
return nil, fmt.Errorf("invalid ip address: %s", v)
}
return ip, nil
}

func parseAlgorithmType(v string) (hcloud.LoadBalancerAlgorithmType, error) {
algorithm := hcloud.LoadBalancerAlgorithmType(strings.ToLower(v))

switch algorithm {
case hcloud.LoadBalancerAlgorithmTypeLeastConnections,
hcloud.LoadBalancerAlgorithmTypeRoundRobin:
return algorithm, nil
default:
return "", fmt.Errorf("invalid: %s", v)
}
}

func parseServiceProtocol(v string) (hcloud.LoadBalancerServiceProtocol, error) {
protocol := hcloud.LoadBalancerServiceProtocol(strings.ToLower(v))

switch protocol {
case hcloud.LoadBalancerServiceProtocolTCP,
hcloud.LoadBalancerServiceProtocolHTTP,
hcloud.LoadBalancerServiceProtocolHTTPS:
return protocol, nil
default:
return "", fmt.Errorf("invalid: %s", v)
}
}

func parseCertificates(v string) ([]*hcloud.Certificate, error) {
values := strings.Split(v, ",")
certificates := make([]*hcloud.Certificate, len(values))

for i, value := range values {
id, err := strconv.ParseInt(value, 10, 64)
if err != nil {
// If we could not parse the string as an integer we assume it is a
// name, not an id.
certificates[i] = &hcloud.Certificate{Name: value}
continue
}
certificates[i] = &hcloud.Certificate{ID: id}
}

return certificates, nil
}
Loading
Loading