Repository: hetznercloud/hcloud-cloud-controller-manager
Pull request #143 (2021-01-08 14:02:25)

Loadbalancer: Support setting defaults
Implements #125.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f870b82..2724667b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@ Changelog
 Unreleased
 ----------
 
-* Fix: allow referencing certificates by name (GH-116)
+* Add support for setting load balancer values via cluster-wide defaults: `HCLOUD_LOAD_BALANCERS_LOCATION`, `HCLOUD_LOAD_BALANCERS_NETWORK_ZONE`, `HCLOUD_LOAD_BALANCERS_DISABLE_PRIVATE_INGRESS`, `HCLOUD_LOAD_BALANCERS_USE_PRIVATE_IP` (#125)
+* Fix: allow referencing certificates by name (#116)
 
 v1.8.1
 ------
diff --git a/docs/load_balancers.md b/docs/load_balancers.md
index 076ad1e7..dcd622ba 100644
--- a/docs/load_balancers.md
+++ b/docs/load_balancers.md
@@ -53,4 +53,11 @@ spec:
   type: LoadBalancer
 ```
 
+## Cluster-wide Defaults
 
+For convenvience, 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_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_USE_PRIVATE_IP`
diff --git a/hcloud/cloud.go b/hcloud/cloud.go
index f4770145..c1a69bb8 100644
--- a/hcloud/cloud.go
+++ b/hcloud/cloud.go
@@ -18,9 +18,11 @@ package hcloud
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"os"
+	"strconv"
 
 	"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/hcops"
 	"github.com/hetznercloud/hcloud-go/hcloud"
@@ -29,14 +31,18 @@ import (
 )
 
 const (
-	hcloudTokenENVVar                = "HCLOUD_TOKEN"
-	hcloudEndpointENVVar             = "HCLOUD_ENDPOINT"
-	hcloudNetworkENVVar              = "HCLOUD_NETWORK"
-	hcloudDebugENVVar                = "HCLOUD_DEBUG"
-	hcloudLoadBalancersEnabledENVVar = "HCLOUD_LOAD_BALANCERS_ENABLED"
-	nodeNameENVVar                   = "NODE_NAME"
-	providerName                     = "hcloud"
-	providerVersion                  = "v1.8.1"
+	hcloudTokenENVVar                        = "HCLOUD_TOKEN"
+	hcloudEndpointENVVar                     = "HCLOUD_ENDPOINT"
+	hcloudNetworkENVVar                      = "HCLOUD_NETWORK"
+	hcloudDebugENVVar                        = "HCLOUD_DEBUG"
+	hcloudLoadBalancersEnabledENVVar         = "HCLOUD_LOAD_BALANCERS_ENABLED"
+	hcloudLoadBalancersLocation              = "HCLOUD_LOAD_BALANCERS_LOCATION"
+	hcloudLoadBalancersNetworkZone           = "HCLOUD_LOAD_BALANCERS_NETWORK_ZONE"
+	hcloudLoadBalancersDisablePrivateIngress = "HCLOUD_LOAD_BALANCERS_DISABLE_PRIVATE_INGRESS"
+	hcloudLoadBalancersUsePrivateIP          = "HCLOUD_LOAD_BALANCERS_USE_PRIVATE_IP"
+	nodeNameENVVar                           = "NODE_NAME"
+	providerName                             = "hcloud"
+	providerVersion                          = "v1.8.1"
 )
 
 type cloud struct {
@@ -94,6 +100,12 @@ func newCloud(config io.Reader) (cloudprovider.Interface, error) {
 	if err != nil {
 		return nil, fmt.Errorf("%s: %w", op, err)
 	}
+
+	lbOpsDefaults, lbDisablePrivateIngress, err := loadBalancerDefaultsFromEnv()
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", op, err)
+	}
+
 	fmt.Printf("Hetzner Cloud k8s cloud controller %s started\n", providerVersion)
 
 	lbOps := &hcops.LoadBalancerOps{
@@ -102,9 +114,10 @@ func newCloud(config io.Reader) (cloudprovider.Interface, error) {
 		ActionClient:  &client.Action,
 		NetworkClient: &client.Network,
 		NetworkID:     networkID,
+		Defaults:      lbOpsDefaults,
 	}
 
-	loadBalancers := newLoadBalancers(lbOps, &client.Action)
+	loadBalancers := newLoadBalancers(lbOps, &client.Action, lbDisablePrivateIngress)
 	if os.Getenv(hcloudLoadBalancersEnabledENVVar) == "false" {
 		loadBalancers = nil
 	}
@@ -164,6 +177,46 @@ func (c *cloud) HasClusterID() bool {
 	return false
 }
 
+func loadBalancerDefaultsFromEnv() (hcops.LoadBalancerDefaults, bool, error) {
+	defaults := hcops.LoadBalancerDefaults{
+		Location:    os.Getenv(hcloudLoadBalancersLocation),
+		NetworkZone: os.Getenv(hcloudLoadBalancersNetworkZone),
+	}
+
+	if defaults.Location != "" && defaults.NetworkZone != "" {
+		return defaults, false, errors.New(
+			"HCLOUD_LOAD_BALANCERS_LOCATION/HCLOUD_LOAD_BALANCERS_NETWORK_ZONE: Only one of these can be set")
+	}
+
+	disablePrivateIngress, err := getEnvBool(hcloudLoadBalancersDisablePrivateIngress)
+	if err != nil {
+		return defaults, false, err
+	}
+
+	defaults.UsePrivateIP, err = getEnvBool(hcloudLoadBalancersUsePrivateIP)
+	if err != nil {
+		return defaults, false, err
+	}
+
+	return defaults, disablePrivateIngress, nil
+}
+
+// getEnvBool returns the boolean 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 getEnvBool(key string) (bool, error) {
+	v, ok := os.LookupEnv(key)
+	if !ok {
+		return false, nil
+	}
+
+	b, err := strconv.ParseBool(v)
+	if err != nil {
+		return false, fmt.Errorf("%s: %v", key, err)
+	}
+
+	return b, nil
+}
+
 func init() {
 	cloudprovider.RegisterCloudProvider(providerName, func(config io.Reader) (cloudprovider.Interface, error) {
 		return newCloud(config)
diff --git a/hcloud/load_balancers.go b/hcloud/load_balancers.go
index 1e7dfb48..fc9b9c57 100644
--- a/hcloud/load_balancers.go
+++ b/hcloud/load_balancers.go
@@ -27,12 +27,17 @@ type LoadBalancerOps interface {
 }
 
 type loadBalancers struct {
-	lbOps LoadBalancerOps
-	ac    hcops.HCloudActionClient // Deprecated: should only be referenced by hcops types
+	lbOps                        LoadBalancerOps
+	ac                           hcops.HCloudActionClient // Deprecated: should only be referenced by hcops types
+	disablePrivateIngressDefault bool
 }
 
-func newLoadBalancers(lbOps LoadBalancerOps, ac hcops.HCloudActionClient) *loadBalancers {
-	return &loadBalancers{lbOps: lbOps, ac: ac}
+func newLoadBalancers(lbOps LoadBalancerOps, ac hcops.HCloudActionClient, disablePrivateIngressDefault bool) *loadBalancers {
+	return &loadBalancers{
+		lbOps:                        lbOps,
+		ac:                           ac,
+		disablePrivateIngressDefault: disablePrivateIngressDefault,
+	}
 }
 
 func (l *loadBalancers) GetLoadBalancer(
@@ -171,8 +176,8 @@ func (l *loadBalancers) EnsureLoadBalancer(
 		)
 	}
 
-	disablePrivIngress, err := annotation.LBDisablePrivateIngress.BoolFromService(svc)
-	if err != nil && !errors.Is(err, annotation.ErrNotSet) {
+	disablePrivIngress, err := l.getDisablePrivateIngress(svc)
+	if err != nil {
 		return nil, fmt.Errorf("%s: %w", op, err)
 	}
 	if !disablePrivIngress {
@@ -184,6 +189,17 @@ func (l *loadBalancers) EnsureLoadBalancer(
 	return &v1.LoadBalancerStatus{Ingress: ingress}, nil
 }
 
+func (l *loadBalancers) getDisablePrivateIngress(svc *v1.Service) (bool, error) {
+	disable, err := annotation.LBDisablePrivateIngress.BoolFromService(svc)
+	if err == nil {
+		return disable, nil
+	}
+	if errors.Is(err, annotation.ErrNotSet) {
+		return l.disablePrivateIngressDefault, nil
+	}
+	return false, err
+}
+
 func (l *loadBalancers) UpdateLoadBalancer(
 	ctx context.Context, clusterName string, svc *v1.Service, nodes []*v1.Node,
 ) error {
diff --git a/internal/hcops/load_balancer.go b/internal/hcops/load_balancer.go
index b5f53604..661b1102 100644
--- a/internal/hcops/load_balancer.go
+++ b/internal/hcops/load_balancer.go
@@ -68,6 +68,14 @@ type LoadBalancerOps struct {
 	CertClient    HCloudCertificateClient
 	RetryDelay    time.Duration
 	NetworkID     int
+	Defaults      LoadBalancerDefaults
+}
+
+// LoadBalancerDefaults stores cluster-wide default values for load balancers.
+type LoadBalancerDefaults struct {
+	Location     string
+	NetworkZone  string
+	UsePrivateIP bool
 }
 
 // GetByK8SServiceUID tries to find a Load Balancer by its Kubernetes service
@@ -156,9 +164,19 @@ func (l *LoadBalancerOps) Create(
 	if v, ok := annotation.LBType.StringFromService(svc); ok {
 		opts.LoadBalancerType.Name = v
 	}
+	if l.Defaults.Location != "" {
+		opts.Location = &hcloud.Location{Name: l.Defaults.Location}
+	}
 	if v, ok := annotation.LBLocation.StringFromService(svc); ok {
-		opts.Location = &hcloud.Location{Name: v}
+		if v == "" {
+			// Allow resetting the location in case someone wants to specify a network zone in an annotation
+			// and a location as default.
+			opts.Location = nil
+		} else {
+			opts.Location = &hcloud.Location{Name: v}
+		}
 	}
+	opts.NetworkZone = hcloud.NetworkZone(l.Defaults.NetworkZone)
 	if v, ok := annotation.LBNetworkZone.StringFromService(svc); ok {
 		opts.NetworkZone = hcloud.NetworkZone(v)
 	}
@@ -481,8 +499,8 @@ func (l *LoadBalancerOps) ReconcileHCLBTargets(
 		changed bool
 	)
 
-	usePrivateIP, err := annotation.LBUsePrivateIP.BoolFromService(svc)
-	if err != nil && !errors.Is(err, annotation.ErrNotSet) {
+	usePrivateIP, err := l.getUsePrivateIP(svc)
+	if err != nil {
 		return changed, fmt.Errorf("%s: %w", op, err)
 	}
 	if usePrivateIP && l.NetworkID == 0 {
@@ -554,6 +572,17 @@ func (l *LoadBalancerOps) ReconcileHCLBTargets(
 	return changed, nil
 }
 
+func (l *LoadBalancerOps) getUsePrivateIP(svc *v1.Service) (bool, error) {
+	usePrivateIP, err := annotation.LBUsePrivateIP.BoolFromService(svc)
+	if err != nil {
+		if errors.Is(err, annotation.ErrNotSet) {
+			return l.Defaults.UsePrivateIP, nil
+		}
+		return false, err
+	}
+	return usePrivateIP, nil
+}
+
 // ReconcileHCLBServices synchronizes services exposed by the Hetzner Cloud
 // Load Balancer with the kubernetes cluster.
 func (l *LoadBalancerOps) ReconcileHCLBServices(


Test patch:
diff --git a/hcloud/cloud_test.go b/hcloud/cloud_test.go
index 1dc63b2f..04fbb861 100644
--- a/hcloud/cloud_test.go
+++ b/hcloud/cloud_test.go
@@ -25,6 +25,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/hcops"
 	"github.com/hetznercloud/hcloud-go/hcloud"
 	"github.com/hetznercloud/hcloud-go/hcloud/schema"
 	"github.com/stretchr/testify/assert"
@@ -254,3 +255,104 @@ func TestCloud(t *testing.T) {
 		}
 	})
 }
+
+func TestLoadBalancerDefaultsFromEnv(t *testing.T) {
+	cases := []struct {
+		name                     string
+		env                      map[string]string
+		expDefaults              hcops.LoadBalancerDefaults
+		expDisablePrivateIngress bool
+		expErr                   string
+	}{
+		{
+			name:        "None set",
+			env:         map[string]string{},
+			expDefaults: hcops.LoadBalancerDefaults{
+				// strings should be empty (zero value)
+				// bools should be false (zero value)
+			},
+		},
+		{
+			name: "All set (except network zone)",
+			env: map[string]string{
+				"HCLOUD_LOAD_BALANCERS_LOCATION":                "hel1",
+				"HCLOUD_LOAD_BALANCERS_DISABLE_PRIVATE_INGRESS": "true",
+				"HCLOUD_LOAD_BALANCERS_USE_PRIVATE_IP":          "true",
+			},
+			expDefaults: hcops.LoadBalancerDefaults{
+				Location:     "hel1",
+				UsePrivateIP: true,
+			},
+			expDisablePrivateIngress: true,
+		},
+		{
+			name: "Network zone set",
+			env: map[string]string{
+				"HCLOUD_LOAD_BALANCERS_NETWORK_ZONE": "eu-central",
+			},
+			expDefaults: hcops.LoadBalancerDefaults{
+				NetworkZone: "eu-central",
+			},
+		},
+		{
+			name: "Both location and network zone set (error)",
+			env: map[string]string{
+				"HCLOUD_LOAD_BALANCERS_LOCATION":     "hel1",
+				"HCLOUD_LOAD_BALANCERS_NETWORK_ZONE": "eu-central",
+			},
+			expErr: "HCLOUD_LOAD_BALANCERS_LOCATION/HCLOUD_LOAD_BALANCERS_NETWORK_ZONE: Only one of these can be set",
+		},
+		{
+			name: "Invalid DISABLE_PRIVATE_INGRESS",
+			env: map[string]string{
+				"HCLOUD_LOAD_BALANCERS_DISABLE_PRIVATE_INGRESS": "invalid",
+			},
+			expErr: `HCLOUD_LOAD_BALANCERS_DISABLE_PRIVATE_INGRESS: strconv.ParseBool: parsing "invalid": invalid syntax`,
+		},
+		{
+			name: "Invalid USE_PRIVATE_IP",
+			env: map[string]string{
+				"HCLOUD_LOAD_BALANCERS_USE_PRIVATE_IP": "invalid",
+			},
+			expErr: `HCLOUD_LOAD_BALANCERS_USE_PRIVATE_IP: strconv.ParseBool: parsing "invalid": invalid syntax`,
+		},
+	}
+
+	for _, c := range cases {
+		c := c // prevent scopelint from complaining
+		t.Run(c.name, func(t *testing.T) {
+			previousEnvVars := map[string]string{}
+			unsetEnvVars := []string{}
+
+			for k, v := range c.env {
+				// Store previous value, so we can later restore it and not affect other tests in this package.
+				if v, ok := os.LookupEnv(k); ok {
+					previousEnvVars[k] = v
+				} else if !ok {
+					unsetEnvVars = append(unsetEnvVars, k)
+				}
+				os.Setenv(k, v)
+			}
+
+			// Make sure this is always executed, even on panic
+			defer func() {
+				for k, v := range previousEnvVars {
+					os.Setenv(k, v)
+				}
+				for _, k := range unsetEnvVars {
+					os.Unsetenv(k)
+				}
+			}()
+
+			defaults, disablePrivateIngress, err := loadBalancerDefaultsFromEnv()
+
+			if c.expErr != "" {
+				assert.EqualError(t, err, c.expErr)
+				return
+			}
+			assert.NoError(t, err)
+			assert.Equal(t, c.expDefaults, defaults)
+			assert.Equal(t, c.expDisablePrivateIngress, disablePrivateIngress)
+		})
+	}
+}
diff --git a/hcloud/load_balancers_test.go b/hcloud/load_balancers_test.go
index 4cff3ac7..88612430 100644
--- a/hcloud/load_balancers_test.go
+++ b/hcloud/load_balancers_test.go
@@ -97,6 +97,27 @@ func TestLoadBalancers_GetLoadBalancer(t *testing.T) {
 }
 
 func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
+	setupSuccessMocks := func(tt *LoadBalancerTestCase, lbName string) {
+		tt.LBOps.
+			On("GetByK8SServiceUID", tt.Ctx, tt.Service).
+			Return(nil, hcops.ErrNotFound)
+		tt.LBOps.
+			On("GetByName", tt.Ctx, lbName).
+			Return(nil, hcops.ErrNotFound)
+		tt.LBOps.
+			On("Create", tt.Ctx, tt.LB.Name, tt.Service).
+			Return(tt.LB, nil)
+		tt.LBOps.
+			On("ReconcileHCLB", tt.Ctx, tt.LB, tt.Service).
+			Return(false, nil)
+		tt.LBOps.
+			On("ReconcileHCLBTargets", tt.Ctx, tt.LB, tt.Service, tt.Nodes).
+			Return(false, nil)
+		tt.LBOps.
+			On("ReconcileHCLBServices", tt.Ctx, tt.LB, tt.Service).
+			Return(false, nil)
+	}
+
 	tests := []LoadBalancerTestCase{
 		{
 			Name:       "check for existing Load Balancer fails",
@@ -129,24 +150,7 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 				},
 			},
 			Mock: func(t *testing.T, tt *LoadBalancerTestCase) {
-				tt.LBOps.
-					On("GetByK8SServiceUID", tt.Ctx, tt.Service).
-					Return(nil, hcops.ErrNotFound)
-				tt.LBOps.
-					On("GetByName", tt.Ctx, "pub-net-only").
-					Return(nil, hcops.ErrNotFound)
-				tt.LBOps.
-					On("Create", tt.Ctx, tt.LB.Name, tt.Service).
-					Return(tt.LB, nil)
-				tt.LBOps.
-					On("ReconcileHCLB", tt.Ctx, tt.LB, tt.Service).
-					Return(false, nil)
-				tt.LBOps.
-					On("ReconcileHCLBTargets", tt.Ctx, tt.LB, tt.Service, tt.Nodes).
-					Return(false, nil)
-				tt.LBOps.
-					On("ReconcileHCLBServices", tt.Ctx, tt.LB, tt.Service).
-					Return(false, nil)
+				setupSuccessMocks(tt, "pub-net-only")
 			},
 			Perform: func(t *testing.T, tt *LoadBalancerTestCase) {
 				expected := &v1.LoadBalancerStatus{
@@ -188,24 +192,7 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 				},
 			},
 			Mock: func(t *testing.T, tt *LoadBalancerTestCase) {
-				tt.LBOps.
-					On("GetByK8SServiceUID", tt.Ctx, tt.Service).
-					Return(nil, hcops.ErrNotFound)
-				tt.LBOps.
-					On("GetByName", tt.Ctx, "with-priv-net").
-					Return(nil, hcops.ErrNotFound)
-				tt.LBOps.
-					On("Create", tt.Ctx, tt.LB.Name, tt.Service).
-					Return(tt.LB, nil)
-				tt.LBOps.
-					On("ReconcileHCLB", tt.Ctx, tt.LB, tt.Service).
-					Return(false, nil)
-				tt.LBOps.
-					On("ReconcileHCLBTargets", tt.Ctx, tt.LB, tt.Service, tt.Nodes).
-					Return(false, nil)
-				tt.LBOps.
-					On("ReconcileHCLBServices", tt.Ctx, tt.LB, tt.Service).
-					Return(false, nil)
+				setupSuccessMocks(tt, "with-priv-net")
 			},
 			Perform: func(t *testing.T, tt *LoadBalancerTestCase) {
 				expected := &v1.LoadBalancerStatus{
@@ -221,7 +208,50 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 			},
 		},
 		{
-			Name:       "disable private ingress",
+			Name:       "disable private ingress via default",
+			NetworkID:  4711,
+			ServiceUID: "5",
+			ServiceAnnotations: map[annotation.Name]interface{}{
+				annotation.LBName: "with-priv-net-no-priv-ingress",
+			},
+			DisablePrivateIngressDefault: true,
+			LB: &hcloud.LoadBalancer{
+				ID:               1,
+				Name:             "with-priv-net-no-priv-ingress",
+				LoadBalancerType: &hcloud.LoadBalancerType{Name: "lb11"},
+				Location:         &hcloud.Location{Name: "nbg1", NetworkZone: hcloud.NetworkZoneEUCentral},
+				PublicNet: hcloud.LoadBalancerPublicNet{
+					Enabled: true,
+					IPv4:    hcloud.LoadBalancerPublicNetIPv4{IP: net.ParseIP("1.2.3.4")},
+					// IPv6:    hcloud.LoadBalancerPublicNetIPv6{IP: net.ParseIP("fe80::1")},
+				},
+				PrivateNet: []hcloud.LoadBalancerPrivateNet{
+					{
+						Network: &hcloud.Network{
+							ID:   4711,
+							Name: "priv-net",
+						},
+						IP: net.ParseIP("10.10.10.2"),
+					},
+				},
+			},
+			Mock: func(t *testing.T, tt *LoadBalancerTestCase) {
+				setupSuccessMocks(tt, "with-priv-net-no-priv-ingress")
+			},
+			Perform: func(t *testing.T, tt *LoadBalancerTestCase) {
+				expected := &v1.LoadBalancerStatus{
+					Ingress: []v1.LoadBalancerIngress{
+						{IP: tt.LB.PublicNet.IPv4.IP.String()},
+						{IP: tt.LB.PublicNet.IPv6.IP.String()},
+					},
+				}
+				lbStat, err := tt.LoadBalancers.EnsureLoadBalancer(tt.Ctx, tt.ClusterName, tt.Service, tt.Nodes)
+				assert.NoError(t, err)
+				assert.Equal(t, expected, lbStat)
+			},
+		},
+		{
+			Name:       "disable private ingress via annotation",
 			NetworkID:  4711,
 			ServiceUID: "5",
 			ServiceAnnotations: map[annotation.Name]interface{}{
@@ -249,24 +279,7 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 				},
 			},
 			Mock: func(t *testing.T, tt *LoadBalancerTestCase) {
-				tt.LBOps.
-					On("GetByK8SServiceUID", tt.Ctx, tt.Service).
-					Return(nil, hcops.ErrNotFound)
-				tt.LBOps.
-					On("GetByName", tt.Ctx, "with-priv-net-no-priv-ingress").
-					Return(nil, hcops.ErrNotFound)
-				tt.LBOps.
-					On("Create", tt.Ctx, tt.LB.Name, tt.Service).
-					Return(tt.LB, nil)
-				tt.LBOps.
-					On("ReconcileHCLB", tt.Ctx, tt.LB, tt.Service).
-					Return(false, nil)
-				tt.LBOps.
-					On("ReconcileHCLBTargets", tt.Ctx, tt.LB, tt.Service, tt.Nodes).
-					Return(false, nil)
-				tt.LBOps.
-					On("ReconcileHCLBServices", tt.Ctx, tt.LB, tt.Service).
-					Return(false, nil)
+				setupSuccessMocks(tt, "with-priv-net-no-priv-ingress")
 			},
 			Perform: func(t *testing.T, tt *LoadBalancerTestCase) {
 				expected := &v1.LoadBalancerStatus{
diff --git a/hcloud/testing.go b/hcloud/testing.go
index 4fa5bc50..cd8bc55f 100644
--- a/hcloud/testing.go
+++ b/hcloud/testing.go
@@ -69,14 +69,15 @@ type LoadBalancerTestCase struct {
 	Name string
 
 	// Defined in test case as needed
-	ClusterName        string
-	NetworkID          int
-	ServiceUID         string
-	ServiceAnnotations map[annotation.Name]interface{}
-	Nodes              []*v1.Node
-	LB                 *hcloud.LoadBalancer
-	LBCreateResult     *hcloud.LoadBalancerCreateResult
-	Mock               func(t *testing.T, tt *LoadBalancerTestCase)
+	ClusterName                  string
+	NetworkID                    int
+	ServiceUID                   string
+	ServiceAnnotations           map[annotation.Name]interface{}
+	DisablePrivateIngressDefault bool
+	Nodes                        []*v1.Node
+	LB                           *hcloud.LoadBalancer
+	LBCreateResult               *hcloud.LoadBalancerCreateResult
+	Mock                         func(t *testing.T, tt *LoadBalancerTestCase)
 
 	// Defines the actual test
 	Perform func(t *testing.T, tt *LoadBalancerTestCase)
@@ -122,7 +123,7 @@ func (tt *LoadBalancerTestCase) run(t *testing.T) {
 		tt.Mock(t, tt)
 	}
 
-	tt.LoadBalancers = newLoadBalancers(tt.LBOps, tt.ActionClient)
+	tt.LoadBalancers = newLoadBalancers(tt.LBOps, tt.ActionClient, tt.DisablePrivateIngressDefault)
 	tt.Perform(t, tt)
 
 	tt.LBOps.AssertExpectations(t)
diff --git a/internal/hcops/load_balancer_test.go b/internal/hcops/load_balancer_test.go
index 5f1ef1a6..78b5ba82 100644
--- a/internal/hcops/load_balancer_test.go
+++ b/internal/hcops/load_balancer_test.go
@@ -241,6 +241,7 @@ func TestGetByK8SServiceUID(t *testing.T) {
 func TestLoadBalancerOps_Create(t *testing.T) {
 	type testCase struct {
 		name               string
+		defaults           hcops.LoadBalancerDefaults
 		serviceAnnotations map[annotation.Name]interface{}
 		createOpts         hcloud.LoadBalancerCreateOpts
 		mock               func(t *testing.T, tt *testCase, fx *hcops.LoadBalancerOpsFixture)
@@ -249,7 +250,10 @@ func TestLoadBalancerOps_Create(t *testing.T) {
 	}
 	tests := []testCase{
 		{
-			name: "create with with location name only",
+			name: "create with with location name (and default set)",
+			defaults: hcops.LoadBalancerDefaults{
+				Location: "hel1",
+			},
 			serviceAnnotations: map[annotation.Name]interface{}{
 				annotation.LBLocation: "fsn1",
 			},
@@ -266,7 +270,10 @@ func TestLoadBalancerOps_Create(t *testing.T) {
 			lb: &hcloud.LoadBalancer{ID: 1},
 		},
 		{
-			name: "create with network zone name only",
+			name: "create with network zone name only (and default set)",
+			defaults: hcops.LoadBalancerDefaults{
+				NetworkZone: "eu-central",
+			},
 			serviceAnnotations: map[annotation.Name]interface{}{
 				annotation.LBNetworkZone: "eu-central",
 			},
@@ -280,6 +287,78 @@ func TestLoadBalancerOps_Create(t *testing.T) {
 			},
 			lb: &hcloud.LoadBalancer{ID: 2},
 		},
+		{
+			name: "create with location as default only",
+			defaults: hcops.LoadBalancerDefaults{
+				Location: "fsn1",
+			},
+			createOpts: hcloud.LoadBalancerCreateOpts{
+				Name:             "some-lb",
+				LoadBalancerType: &hcloud.LoadBalancerType{Name: "lb11"},
+				Location: &hcloud.Location{
+					Name: "fsn1",
+				},
+				Labels: map[string]string{
+					hcops.LabelServiceUID: "some-lb-uid",
+				},
+			},
+			lb: &hcloud.LoadBalancer{ID: 3},
+		},
+		{
+			name: "create with network zone as default only",
+			defaults: hcops.LoadBalancerDefaults{
+				NetworkZone: "eu-central",
+			},
+			createOpts: hcloud.LoadBalancerCreateOpts{
+				Name:             "some-lb",
+				LoadBalancerType: &hcloud.LoadBalancerType{Name: "lb11"},
+				NetworkZone:      hcloud.NetworkZoneEUCentral,
+				Labels: map[string]string{
+					hcops.LabelServiceUID: "some-lb-uid",
+				},
+			},
+			lb: &hcloud.LoadBalancer{ID: 4},
+		},
+		{
+			name: "create with network zone and reset default location",
+			defaults: hcops.LoadBalancerDefaults{
+				Location: "hel1",
+			},
+			serviceAnnotations: map[annotation.Name]interface{}{
+				annotation.LBLocation:    "",
+				annotation.LBNetworkZone: "eu-central",
+			},
+			createOpts: hcloud.LoadBalancerCreateOpts{
+				Name:             "another-lb",
+				LoadBalancerType: &hcloud.LoadBalancerType{Name: "lb11"},
+				NetworkZone:      hcloud.NetworkZoneEUCentral,
+				Labels: map[string]string{
+					hcops.LabelServiceUID: "another-lb-uid",
+				},
+			},
+			lb: &hcloud.LoadBalancer{ID: 2},
+		},
+		{
+			name: "create with location and reset default network zone",
+			defaults: hcops.LoadBalancerDefaults{
+				NetworkZone: "eu-central",
+			},
+			serviceAnnotations: map[annotation.Name]interface{}{
+				annotation.LBLocation:    "fsn1",
+				annotation.LBNetworkZone: "",
+			},
+			createOpts: hcloud.LoadBalancerCreateOpts{
+				Name:             "another-lb",
+				LoadBalancerType: &hcloud.LoadBalancerType{Name: "lb11"},
+				Location: &hcloud.Location{
+					Name: "fsn1",
+				},
+				Labels: map[string]string{
+					hcops.LabelServiceUID: "another-lb-uid",
+				},
+			},
+			lb: &hcloud.LoadBalancer{ID: 2},
+		},
 		{
 			name:               "fails if location and network zone missing",
 			serviceAnnotations: map[annotation.Name]interface{}{},
@@ -430,6 +509,8 @@ func TestLoadBalancerOps_Create(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			fx := hcops.NewLoadBalancerOpsFixture(t)
 
+			fx.LBOps.Defaults = tt.defaults
+
 			if tt.mock == nil {
 				tt.mock = func(t *testing.T, tt *testCase, fx *hcops.LoadBalancerOpsFixture) {
 					if tt.createOpts.Name == "" {
@@ -506,6 +587,7 @@ func TestLoadBalancerOps_Delete(t *testing.T) {
 
 type LBReconcilementTestCase struct {
 	name               string
+	defaults           hcops.LoadBalancerDefaults
 	serviceUID         string
 	serviceAnnotations map[annotation.Name]interface{}
 	servicePorts       []v1.ServicePort
@@ -523,6 +605,8 @@ func (tt *LBReconcilementTestCase) run(t *testing.T) {
 	t.Helper()
 
 	tt.fx = hcops.NewLoadBalancerOpsFixture(t)
+	tt.fx.LBOps.Defaults = tt.defaults
+
 	if tt.service == nil {
 		tt.service = &v1.Service{
 			Spec:       v1.ServiceSpec{Ports: tt.servicePorts},
@@ -1007,7 +1091,41 @@ func TestLoadBalancerOps_ReconcileHCLBTargets(t *testing.T) {
 			},
 		},
 		{
-			name: "enable use of private network",
+			name: "enable use of private network via default",
+			defaults: hcops.LoadBalancerDefaults{
+				// Make sure the annotation overrides the default
+				UsePrivateIP: true,
+			},
+			k8sNodes: []*v1.Node{
+				{Spec: v1.NodeSpec{ProviderID: "hcloud://1"}},
+				{Spec: v1.NodeSpec{ProviderID: "hcloud://2"}},
+			},
+			initialLB: &hcloud.LoadBalancer{
+				ID: 3,
+			},
+			mock: func(t *testing.T, tt *LBReconcilementTestCase) {
+				tt.fx.LBOps.NetworkID = 4711
+
+				opts := hcloud.LoadBalancerAddServerTargetOpts{Server: &hcloud.Server{ID: 1}, UsePrivateIP: hcloud.Bool(true)}
+				action := tt.fx.MockAddServerTarget(tt.initialLB, opts, nil)
+				tt.fx.MockWatchProgress(action, nil)
+
+				opts = hcloud.LoadBalancerAddServerTargetOpts{Server: &hcloud.Server{ID: 2}, UsePrivateIP: hcloud.Bool(true)}
+				action = tt.fx.MockAddServerTarget(tt.initialLB, opts, nil)
+				tt.fx.MockWatchProgress(action, nil)
+			},
+			perform: func(t *testing.T, tt *LBReconcilementTestCase) {
+				changed, err := tt.fx.LBOps.ReconcileHCLBTargets(tt.fx.Ctx, tt.initialLB, tt.service, tt.k8sNodes)
+				assert.NoError(t, err)
+				assert.True(t, changed)
+			},
+		},
+		{
+			name: "enable use of private network via annotation",
+			defaults: hcops.LoadBalancerDefaults{
+				// Make sure the annotation overrides the default
+				UsePrivateIP: false,
+			},
 			k8sNodes: []*v1.Node{
 				{Spec: v1.NodeSpec{ProviderID: "hcloud://1"}},
 				{Spec: v1.NodeSpec{ProviderID: "hcloud://2"}},
@@ -1036,7 +1154,11 @@ func TestLoadBalancerOps_ReconcileHCLBTargets(t *testing.T) {
 			},
 		},
 		{
-			name: "disable use of private network",
+			name: "disable use of private network via annotation",
+			defaults: hcops.LoadBalancerDefaults{
+				// Make sure the annotation overrides the default
+				UsePrivateIP: true,
+			},
 			k8sNodes: []*v1.Node{
 				{Spec: v1.NodeSpec{ProviderID: "hcloud://1"}},
 			},