Repository: hetznercloud/hcloud-cloud-controller-manager (go)

Issue:
Changing the value of the `load-balancer.hetzner.cloud/disable-public-network` annotation for an existing service has no effect
This was originally reported in issue #120. It seems like sending the update to our backend is missing.

Pull request:
Update of disable-public-network annotation
Up until now the disable-public-network annotation only affected newly
created Load Balancers. This commit implements modifying existing Load
Balancers upon changing the annotation.

Closes #127

Patch:
diff --git a/internal/hcops/load_balancer.go b/internal/hcops/load_balancer.go
index 23da21ec..8b71ab4d 100644
--- a/internal/hcops/load_balancer.go
+++ b/internal/hcops/load_balancer.go
@@ -42,6 +42,13 @@ type HCloudLoadBalancerClient interface {
 
 	AttachToNetwork(ctx context.Context, lb *hcloud.LoadBalancer, opts hcloud.LoadBalancerAttachToNetworkOpts) (*hcloud.Action, *hcloud.Response, error)
 	DetachFromNetwork(ctx context.Context, lb *hcloud.LoadBalancer, opts hcloud.LoadBalancerDetachFromNetworkOpts) (*hcloud.Action, *hcloud.Response, error)
+
+	EnablePublicInterface(
+		ctx context.Context, loadBalancer *hcloud.LoadBalancer,
+	) (*hcloud.Action, *hcloud.Response, error)
+	DisablePublicInterface(
+		ctx context.Context, loadBalancer *hcloud.LoadBalancer,
+	) (*hcloud.Action, *hcloud.Response, error)
 }
 
 // LoadBalancerOps implements all operations regarding Hetzner Cloud Load Balancers.
@@ -193,6 +200,12 @@ func (l *LoadBalancerOps) ReconcileHCLB(ctx context.Context, lb *hcloud.LoadBala
 	}
 	changed = changed || networkAttached
 
+	pubIfaceToggled, err := l.togglePublicInterface(ctx, lb, svc)
+	if err != nil {
+		return changed, fmt.Errorf("%s: %w", op, err)
+	}
+	changed = changed || pubIfaceToggled
+
 	return changed, nil
 }
 
@@ -312,6 +325,37 @@ func (l *LoadBalancerOps) attachToNetwork(ctx context.Context, lb *hcloud.LoadBa
 	return true, nil
 }
 
+func (l *LoadBalancerOps) togglePublicInterface(ctx context.Context, lb *hcloud.LoadBalancer, svc *v1.Service) (bool, error) {
+	const op = "hcops/LoadBalancerOps.togglePublicInterface"
+	var a *hcloud.Action
+
+	disable, err := annotation.LBDisablePublicNetwork.BoolFromService(svc)
+	if errors.Is(err, annotation.ErrNotSet) {
+		return false, nil
+	}
+	if err != nil {
+		return false, fmt.Errorf("%s: %w", op, err)
+	}
+
+	if disable == !lb.PublicNet.Enabled {
+		return false, nil
+	}
+
+	if disable {
+		a, _, err = l.LBClient.DisablePublicInterface(ctx, lb)
+	} else {
+		a, _, err = l.LBClient.EnablePublicInterface(ctx, lb)
+	}
+	if err != nil {
+		return false, fmt.Errorf("%s: %w", op, err)
+	}
+
+	if err := WatchAction(ctx, l.ActionClient, a); err != nil {
+		return false, fmt.Errorf("%s: %w", op, err)
+	}
+	return true, nil
+}
+
 // ReconcileHCLBTargets adds or removes target nodes from the Hetzner Cloud
 // Load Balancer when nodes are added or removed to the K8S cluster.
 func (l *LoadBalancerOps) ReconcileHCLBTargets(
diff --git a/internal/mocks/loadbalancer.go b/internal/mocks/loadbalancer.go
index 7ff2b6f2..700ea1d1 100644
--- a/internal/mocks/loadbalancer.go
+++ b/internal/mocks/loadbalancer.go
@@ -95,3 +95,17 @@ func (m *LoadBalancerClient) DetachFromNetwork(
 	args := m.Called(ctx, lb, opts)
 	return getActionPtr(args, 0), getResponsePtr(args, 1), args.Error(2)
 }
+
+func (m *LoadBalancerClient) EnablePublicInterface(
+	ctx context.Context, lb *hcloud.LoadBalancer,
+) (*hcloud.Action, *hcloud.Response, error) {
+	args := m.Called(ctx, lb)
+	return getActionPtr(args, 0), getResponsePtr(args, 1), args.Error(2)
+}
+
+func (m *LoadBalancerClient) DisablePublicInterface(
+	ctx context.Context, lb *hcloud.LoadBalancer,
+) (*hcloud.Action, *hcloud.Response, error) {
+	args := m.Called(ctx, lb)
+	return getActionPtr(args, 0), getResponsePtr(args, 1), args.Error(2)
+}


Test patch:
diff --git a/internal/hcops/load_balancer_test.go b/internal/hcops/load_balancer_test.go
index af6ba178..8a2fad4f 100644
--- a/internal/hcops/load_balancer_test.go
+++ b/internal/hcops/load_balancer_test.go
@@ -641,6 +641,88 @@ func TestLoadBalancerOps_ReconcileHCLB(t *testing.T) {
 				assert.False(t, changed)
 			},
 		},
+		{
+			name: "disable enabled public network",
+			serviceAnnotations: map[annotation.Name]interface{}{
+				annotation.LBDisablePublicNetwork: true,
+			},
+			initialLB: &hcloud.LoadBalancer{
+				ID: 6,
+				PublicNet: hcloud.LoadBalancerPublicNet{
+					Enabled: true,
+				},
+			},
+			mock: func(t *testing.T, tt *LBReconcilementTestCase) {
+				action := &hcloud.Action{ID: rand.Int()}
+				tt.fx.LBClient.
+					On("DisablePublicInterface", tt.fx.Ctx, tt.initialLB).
+					Return(action, nil, nil)
+				tt.fx.MockWatchProgress(action, nil)
+			},
+			perform: func(t *testing.T, tt *LBReconcilementTestCase) {
+				changed, err := tt.fx.LBOps.ReconcileHCLB(tt.fx.Ctx, tt.initialLB, tt.service)
+				assert.NoError(t, err)
+				assert.True(t, changed)
+			},
+		},
+		{
+			name: "keep disabled public interface",
+			serviceAnnotations: map[annotation.Name]interface{}{
+				annotation.LBDisablePublicNetwork: true,
+			},
+			initialLB: &hcloud.LoadBalancer{
+				ID: 7,
+				PublicNet: hcloud.LoadBalancerPublicNet{
+					Enabled: false,
+				},
+			},
+			perform: func(t *testing.T, tt *LBReconcilementTestCase) {
+				changed, err := tt.fx.LBOps.ReconcileHCLB(tt.fx.Ctx, tt.initialLB, tt.service)
+				assert.NoError(t, err)
+				assert.False(t, changed)
+			},
+		},
+		{
+			name: "enable disabled public interface",
+			serviceAnnotations: map[annotation.Name]interface{}{
+				annotation.LBDisablePublicNetwork: false,
+			},
+			initialLB: &hcloud.LoadBalancer{
+				ID: 8,
+				PublicNet: hcloud.LoadBalancerPublicNet{
+					Enabled: false,
+				},
+			},
+			mock: func(t *testing.T, tt *LBReconcilementTestCase) {
+				action := &hcloud.Action{ID: rand.Int()}
+				tt.fx.LBClient.
+					On("EnablePublicInterface", tt.fx.Ctx, tt.initialLB).
+					Return(action, nil, nil)
+				tt.fx.MockWatchProgress(action, nil)
+			},
+			perform: func(t *testing.T, tt *LBReconcilementTestCase) {
+				changed, err := tt.fx.LBOps.ReconcileHCLB(tt.fx.Ctx, tt.initialLB, tt.service)
+				assert.NoError(t, err)
+				assert.True(t, changed)
+			},
+		},
+		{
+			name: "keep enabled public interface",
+			serviceAnnotations: map[annotation.Name]interface{}{
+				annotation.LBDisablePublicNetwork: false,
+			},
+			initialLB: &hcloud.LoadBalancer{
+				ID: 9,
+				PublicNet: hcloud.LoadBalancerPublicNet{
+					Enabled: true,
+				},
+			},
+			perform: func(t *testing.T, tt *LBReconcilementTestCase) {
+				changed, err := tt.fx.LBOps.ReconcileHCLB(tt.fx.Ctx, tt.initialLB, tt.service)
+				assert.NoError(t, err)
+				assert.False(t, changed)
+			},
+		},
 	}
 
 	for _, tt := range tests {