Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 11 additions & 2 deletions docs/guides/load-balancer/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@ Load Balancers are configured via Kubernetes [annotations](https://kubernetes.io

For convenience, you can set the following environment variables as cluster-wide defaults, so you don't have to set them on each load balancer service. If a load balancer service has the corresponding annotation set, it overrides the default.

- `HCLOUD_LOAD_BALANCERS_ALGORITHM_TYPE`
- `HCLOUD_LOAD_BALANCERS_DISABLE_IPV6`
- `HCLOUD_LOAD_BALANCERS_DISABLE_PRIVATE_INGRESS`
- `HCLOUD_LOAD_BALANCERS_DISABLE_PUBLIC_NETWORK`
- `HCLOUD_LOAD_BALANCERS_ENABLED`
- `HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_INTERVAL`
- `HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_RETRIES`
- `HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_TIMEOUT`
- `HCLOUD_LOAD_BALANCERS_LOCATION` (mutually exclusive with `HCLOUD_LOAD_BALANCERS_NETWORK_ZONE`)
- `HCLOUD_LOAD_BALANCERS_NETWORK_ZONE` (mutually exclusive with `HCLOUD_LOAD_BALANCERS_LOCATION`)
- `HCLOUD_LOAD_BALANCERS_DISABLE_PRIVATE_INGRESS`
- `HCLOUD_LOAD_BALANCERS_PRIVATE_SUBNET_IP_RANGE`
- `HCLOUD_LOAD_BALANCERS_TYPE`
- `HCLOUD_LOAD_BALANCERS_USE_PRIVATE_IP`
- `HCLOUD_LOAD_BALANCERS_ENABLED`
- `HCLOUD_LOAD_BALANCERS_USES_PROXYPROTOCOL`
102 changes: 94 additions & 8 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"fmt"
"os"
"strconv"
"strings"
"time"

"k8s.io/klog/v2"

"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/exp/kit/envutil"
)

Expand All @@ -31,12 +33,20 @@ const (
hcloudNetworkDisableAttachedCheck = "HCLOUD_NETWORK_DISABLE_ATTACHED_CHECK"
hcloudNetworkRoutesEnabled = "HCLOUD_NETWORK_ROUTES_ENABLED"

hcloudLoadBalancersAlgorithmType = "HCLOUD_LOAD_BALANCERS_ALGORITHM_TYPE"
hcloudLoadBalancersDisableIPv6 = "HCLOUD_LOAD_BALANCERS_DISABLE_IPV6"
hcloudLoadBalancersDisablePrivateIngress = "HCLOUD_LOAD_BALANCERS_DISABLE_PRIVATE_INGRESS"
hcloudLoadBalancersDisablePublicNetwork = "HCLOUD_LOAD_BALANCERS_DISABLE_PUBLIC_NETWORK"
hcloudLoadBalancersEnabled = "HCLOUD_LOAD_BALANCERS_ENABLED"
hcloudLoadBalancersHealthCheckInterval = "HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_INTERVAL"
hcloudLoadBalancersHealthCheckRetries = "HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_RETRIES"
hcloudLoadBalancersHealthCheckTimeout = "HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_TIMEOUT"
hcloudLoadBalancersLocation = "HCLOUD_LOAD_BALANCERS_LOCATION"
hcloudLoadBalancersNetworkZone = "HCLOUD_LOAD_BALANCERS_NETWORK_ZONE"
hcloudLoadBalancersDisablePrivateIngress = "HCLOUD_LOAD_BALANCERS_DISABLE_PRIVATE_INGRESS"
hcloudLoadBalancersPrivateSubnetIPRange = "HCLOUD_LOAD_BALANCERS_PRIVATE_SUBNET_IP_RANGE"
hcloudLoadBalancersType = "HCLOUD_LOAD_BALANCERS_TYPE"
hcloudLoadBalancersUsePrivateIP = "HCLOUD_LOAD_BALANCERS_USE_PRIVATE_IP"
hcloudLoadBalancersDisableIPv6 = "HCLOUD_LOAD_BALANCERS_DISABLE_IPV6"
hcloudLoadBalancersUsesProxyProtocol = "HCLOUD_LOAD_BALANCERS_USES_PROXYPROTOCOL"

hcloudMetricsEnabled = "HCLOUD_METRICS_ENABLED"
hcloudMetricsAddress = ":8233"
Expand Down Expand Up @@ -76,12 +86,21 @@ type InstanceConfiguration struct {
}

type LoadBalancerConfiguration struct {
Enabled bool
Location string
NetworkZone string
PrivateIngressEnabled bool
PrivateIPEnabled bool
IPv6Enabled bool
AlgorithmType hcloud.LoadBalancerAlgorithmType
DisablePublicNetwork bool
Enabled bool
HealthCheckInterval time.Duration
HealthCheckRetries int
HealthCheckTimeout time.Duration
IPv6Enabled bool
Location string
NetworkZone string
PrivateIngressEnabled bool
PrivateIPEnabled bool
PrivateSubnetIPRange string
ProxyProtocolEnabled bool
ProxyProtocolEnabledSet bool
Type string
}

type NetworkConfiguration struct {
Expand Down Expand Up @@ -183,12 +202,70 @@ func Read() (HCCMConfiguration, error) {
errs = append(errs, err)
}

cfg.LoadBalancer.ProxyProtocolEnabled, err = getEnvBool(hcloudLoadBalancersUsesProxyProtocol, false)
if err != nil {
errs = append(errs, err)
}
// Workaround to keep bug https://github.com/hetznercloud/hcloud-cloud-controller-manager/issues/876
if _, exists := os.LookupEnv(hcloudLoadBalancersUsesProxyProtocol); exists {
cfg.LoadBalancer.ProxyProtocolEnabledSet = true
}

disableIPv6, err := getEnvBool(hcloudLoadBalancersDisableIPv6, false)
if err != nil {
errs = append(errs, err)
}
cfg.LoadBalancer.IPv6Enabled = !disableIPv6 // Invert the logic, as the env var is prefixed with DISABLE_.

if subnetRange, ok := os.LookupEnv(hcloudLoadBalancersPrivateSubnetIPRange); ok {
cfg.LoadBalancer.PrivateSubnetIPRange = subnetRange
}
Comment on lines +220 to +222
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use net.ParseCIDR in the Validate function to check if the format is correct.


if algorithmType, ok := os.LookupEnv(hcloudLoadBalancersAlgorithmType); ok {
alg, parseErr := parseLoadBalancerAlgorithmType(algorithmType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are mixing Read and Validate in this call. Could you please move the arg type validation into Validate.

if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersAlgorithmType, parseErr))
} else {
cfg.LoadBalancer.AlgorithmType = alg
}
}

if interval, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckInterval); ok {
d, parseErr := time.ParseDuration(interval)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckInterval, parseErr))
} else {
cfg.LoadBalancer.HealthCheckInterval = d
}
}
Comment on lines +233 to +240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if interval, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckInterval); ok {
d, parseErr := time.ParseDuration(interval)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckInterval, parseErr))
} else {
cfg.LoadBalancer.HealthCheckInterval = d
}
}
cfg.LoadBalancer.HealthCheckInterval, err = getEnvDuration(hcloudLoadBalancersHealthCheckInterval)
if err != nil {
errs = append(errs, err)
}

We already have a util function for this. If errs is not empty, HCCM won't continue and throw an error. So the if-else is not necessary here.


if timeout, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckTimeout); ok {
d, parseErr := time.ParseDuration(timeout)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckTimeout, parseErr))
} else {
cfg.LoadBalancer.HealthCheckTimeout = d
}
}
Comment on lines +242 to +249
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if timeout, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckTimeout); ok {
d, parseErr := time.ParseDuration(timeout)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckTimeout, parseErr))
} else {
cfg.LoadBalancer.HealthCheckTimeout = d
}
}
cfg.LoadBalancer.HealthCheckTimeout, err = getEnvDuration(hcloudLoadBalancersHealthCheckTimeout)
if err != nil {
errs = append(errs, err)
}

Same here


if retries, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckRetries); ok {
v, parseErr := strconv.Atoi(retries)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckRetries, parseErr))
} else {
cfg.LoadBalancer.HealthCheckRetries = v
}
}
Comment on lines +251 to +258
Copy link
Contributor

@lukasmetzner lukasmetzner Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if retries, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckRetries); ok {
v, parseErr := strconv.Atoi(retries)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckRetries, parseErr))
} else {
cfg.LoadBalancer.HealthCheckRetries = v
}
}
cfg.LoadBalancer.HealthCheckRetries, err = strconv.Atoi(os.Getenv(hcloudLoadBalancersHealthCheckRetries))
if err != nil {
errs = append(errs, err)
}

Same here


cfg.LoadBalancer.DisablePublicNetwork, err = getEnvBool(hcloudLoadBalancersDisablePublicNetwork, false)
if err != nil {
errs = append(errs, err)
}

if lbType, ok := os.LookupEnv(hcloudLoadBalancersType); ok {
cfg.LoadBalancer.Type = lbType
}
Comment on lines +265 to +267
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if lbType, ok := os.LookupEnv(hcloudLoadBalancersType); ok {
cfg.LoadBalancer.Type = lbType
}
cfg.LoadBalancer.Type = os.Getenv(hcloudLoadBalancersType)

cfg.LoadBalancer.Type is of type string and therefore will be initialized as an empty string. os.Getenv will return an empty string when the env is not set.


cfg.Network.NameOrID = os.Getenv(hcloudNetwork)
disableAttachedCheck, err := getEnvBool(hcloudNetworkDisableAttachedCheck, false)
if err != nil {
Expand Down Expand Up @@ -280,3 +357,12 @@ func getEnvDuration(key string) (time.Duration, error) {

return b, nil
}

func parseLoadBalancerAlgorithmType(value string) (hcloud.LoadBalancerAlgorithmType, error) {
v := strings.ToLower(strings.TrimSpace(value))
alg := hcloud.LoadBalancerAlgorithmType(v)
if alg == hcloud.LoadBalancerAlgorithmTypeRoundRobin || alg == hcloud.LoadBalancerAlgorithmTypeLeastConnections {
return alg, nil
}
return "", fmt.Errorf("unsupported value %q", value)
}
24 changes: 21 additions & 3 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert"

"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/testsupport"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

func TestRead(t *testing.T) {
Expand Down Expand Up @@ -123,6 +124,14 @@ failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or
"HCLOUD_TOKEN", "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq",
"HCLOUD_ENDPOINT", "https://api.example.com",
"HCLOUD_DEBUG", "true",
"HCLOUD_LOAD_BALANCERS_PRIVATE_SUBNET_IP_RANGE", "10.1.0.0/24",
"HCLOUD_LOAD_BALANCERS_USES_PROXYPROTOCOL", "true",
"HCLOUD_LOAD_BALANCERS_ALGORITHM_TYPE", "least_connections",
"HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_INTERVAL", "30s",
"HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_TIMEOUT", "5s",
"HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_RETRIES", "5",
"HCLOUD_LOAD_BALANCERS_DISABLE_PUBLIC_NETWORK", "true",
"HCLOUD_LOAD_BALANCERS_TYPE", "lb21",
},
want: HCCMConfiguration{
HCloudClient: HCloudClientConfiguration{
Expand All @@ -137,9 +146,18 @@ failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or
AttachedCheckEnabled: true,
},
LoadBalancer: LoadBalancerConfiguration{
Enabled: true,
PrivateIngressEnabled: true,
IPv6Enabled: true,
Enabled: true,
PrivateIngressEnabled: true,
IPv6Enabled: true,
PrivateSubnetIPRange: "10.1.0.0/24",
ProxyProtocolEnabled: true,
ProxyProtocolEnabledSet: true,
AlgorithmType: hcloud.LoadBalancerAlgorithmTypeLeastConnections,
HealthCheckInterval: 30 * time.Second,
HealthCheckTimeout: 5 * time.Second,
HealthCheckRetries: 5,
DisablePublicNetwork: true,
Type: "lb21",
},
},
wantErr: nil,
Expand Down
Loading