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
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`
103 changes: 100 additions & 3 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 = "HCLOUD_METRICS_ADDRESS"
Expand Down Expand Up @@ -76,12 +86,20 @@ type InstanceConfiguration struct {
}

type LoadBalancerConfiguration struct {
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
IPv6Enabled bool
PrivateSubnetIPRange string
ProxyProtocolEnabled *bool
Type string
}

type NetworkConfiguration struct {
Expand Down Expand Up @@ -188,12 +206,66 @@ func Read() (HCCMConfiguration, error) {
errs = append(errs, err)
}

cfg.LoadBalancer.ProxyProtocolEnabled, err = getEnvBoolPtr(hcloudLoadBalancersUsesProxyProtocol)
if err != nil {
errs = append(errs, err)
}

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 = getEnvBoolPtr(hcloudLoadBalancersDisablePublicNetwork)
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 @@ -270,6 +342,22 @@ func getEnvBool(key string, defaultValue bool) (bool, error) {
return b, nil
}

// getEnvBoolPtr returns a pointer to the boolean parsed from the environment variable with the given key.
// Returns nil if the env var is unset.
func getEnvBoolPtr(key string) (*bool, error) {
v, ok := os.LookupEnv(key)
if !ok {
return nil, nil
}

b, err := strconv.ParseBool(v)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", key, err)
}

return &b, nil
}

// getEnvDuration returns the duration parsed from the environment variable with the given key and a potential error
// parsing the var. Returns false if the env var is unset.
func getEnvDuration(key string) (time.Duration, error) {
Expand All @@ -285,3 +373,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)
}
17 changes: 17 additions & 0 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 @@ -140,6 +149,14 @@ failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or
Enabled: true,
PrivateIngressEnabled: true,
IPv6Enabled: true,
PrivateSubnetIPRange: "10.1.0.0/24",
ProxyProtocolEnabled: hcloud.Ptr(true),
AlgorithmType: hcloud.LoadBalancerAlgorithmTypeLeastConnections,
HealthCheckInterval: 30 * time.Second,
HealthCheckTimeout: 5 * time.Second,
HealthCheckRetries: 5,
DisablePublicNetwork: hcloud.Ptr(true),
Type: "lb21",
},
},
wantErr: nil,
Expand Down
Loading
Loading