Skip to content

Commit 32cb59f

Browse files
committed
feat(load-balancer): improve logging on invalid input
1 parent d62fb43 commit 32cb59f

5 files changed

Lines changed: 144 additions & 11 deletions

File tree

internal/hcops/certificates.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func (co *CertificateOps) CreateManagedCertificate(
7777
return fmt.Errorf("%s: %w", op, ErrAlreadyExists)
7878
}
7979
if err != nil {
80-
return fmt.Errorf("%s: %w", op, err)
80+
return fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
8181
}
8282

8383
err = co.ActionClient.WaitFor(ctx, result.Action)

internal/hcops/errors.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package hcops
22

3-
import "errors"
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/hetznercloud/hcloud-go/v2/hcloud"
9+
)
410

511
var (
612
// ErrNotFound signals that an item was not found by the Hetzner Cloud
@@ -16,3 +22,23 @@ var (
1622
// resource already exists.
1723
ErrAlreadyExists = errors.New("already exists")
1824
)
25+
26+
// withInvalidInputFields adds the validation errors of an 'invalid_input' API
27+
// error to the error message.
28+
func withInvalidInputFields(err error) error {
29+
apiErr, ok := errors.AsType[hcloud.Error](err)
30+
if !ok {
31+
return err
32+
}
33+
details, ok := apiErr.Details.(hcloud.ErrorDetailsInvalidInput)
34+
if !ok || len(details.Fields) == 0 {
35+
return err
36+
}
37+
38+
fields := make([]string, 0, len(details.Fields))
39+
for _, field := range details.Fields {
40+
fields = append(fields, fmt.Sprintf("%s (%s)", field.Name, strings.Join(field.Messages, ", ")))
41+
}
42+
43+
return fmt.Errorf("%w: invalid fields: %s", err, strings.Join(fields, ", "))
44+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package hcops
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
10+
"github.com/hetznercloud/hcloud-go/v2/hcloud"
11+
)
12+
13+
func TestWithInvalidInputFields(t *testing.T) {
14+
invalidInput := hcloud.Error{
15+
Code: hcloud.ErrorCodeInvalidInput,
16+
Message: "invalid input in field 'http'",
17+
Details: hcloud.ErrorDetailsInvalidInput{
18+
Fields: []hcloud.ErrorDetailsInvalidInputField{
19+
{Name: "http.timeout_idle", Messages: []string{"must be between 5 and 3600"}},
20+
},
21+
},
22+
}
23+
24+
tests := []struct {
25+
name string
26+
err error
27+
expected string
28+
}{
29+
{
30+
name: "invalid input error",
31+
err: invalidInput,
32+
expected: "invalid input in field 'http' (invalid_input): invalid fields: http.timeout_idle (must be between 5 and 3600)",
33+
},
34+
{
35+
name: "wrapped invalid input error",
36+
err: fmt.Errorf("hcops/LoadBalancerOps.ReconcileHCLBServices: %w", invalidInput),
37+
expected: "hcops/LoadBalancerOps.ReconcileHCLBServices: invalid input in field 'http' (invalid_input): invalid fields: http.timeout_idle (must be between 5 and 3600)",
38+
},
39+
{
40+
name: "invalid input error without details",
41+
err: hcloud.Error{
42+
Code: hcloud.ErrorCodeInvalidInput,
43+
Message: "invalid input",
44+
},
45+
expected: "invalid input (invalid_input)",
46+
},
47+
{
48+
name: "other api error",
49+
err: hcloud.Error{Code: hcloud.ErrorCodeNotFound, Message: "not found"},
50+
expected: "not found (not_found)",
51+
},
52+
{
53+
name: "other error",
54+
err: errors.New("something went wrong"),
55+
expected: "something went wrong",
56+
},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
err := withInvalidInputFields(tt.err)
62+
63+
assert.EqualError(t, err, tt.expected)
64+
// The API error must stay accessible for the callers.
65+
assert.Equal(t, hcloud.IsError(tt.err, hcloud.ErrorCodeInvalidInput), hcloud.IsError(err, hcloud.ErrorCodeInvalidInput))
66+
})
67+
}
68+
}

internal/hcops/load_balancer.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func (l *LoadBalancerOps) Create(
179179

180180
result, _, err := l.LBClient.Create(ctx, opts)
181181
if err != nil {
182-
return nil, fmt.Errorf("%s: %w", op, err)
182+
return nil, fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
183183
}
184184
if err := l.ActionClient.WaitFor(ctx, result.Action); err != nil {
185185
return nil, fmt.Errorf("%s: %w", op, err)
@@ -302,7 +302,7 @@ func (l *LoadBalancerOps) changeHCLBInfo(ctx context.Context, lb *hcloud.LoadBal
302302

303303
updated, _, err := l.LBClient.Update(ctx, lb, opts)
304304
if err != nil {
305-
return false, fmt.Errorf("%s: %w", op, err)
305+
return false, fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
306306
}
307307
lb.Name = updated.Name
308308
lb.Labels = updated.Labels
@@ -326,7 +326,7 @@ func (l *LoadBalancerOps) changeIPv4RDNS(ctx context.Context, lb *hcloud.LoadBal
326326

327327
action, _, err := l.LBClient.ChangeDNSPtr(ctx, lb, lb.PublicNet.IPv4.IP.String(), &rdns)
328328
if err != nil {
329-
return false, fmt.Errorf("%s: %w", op, err)
329+
return false, fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
330330
}
331331
err = l.ActionClient.WaitFor(ctx, action)
332332
if err != nil {
@@ -351,7 +351,7 @@ func (l *LoadBalancerOps) changeIPv6RDNS(ctx context.Context, lb *hcloud.LoadBal
351351

352352
action, _, err := l.LBClient.ChangeDNSPtr(ctx, lb, lb.PublicNet.IPv6.IP.String(), &rdns)
353353
if err != nil {
354-
return false, fmt.Errorf("%s: %w", op, err)
354+
return false, fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
355355
}
356356
err = l.ActionClient.WaitFor(ctx, action)
357357
if err != nil {
@@ -382,7 +382,7 @@ func (l *LoadBalancerOps) changeAlgorithm(ctx context.Context, lb *hcloud.LoadBa
382382
opts := hcloud.LoadBalancerChangeAlgorithmOpts{Type: at}
383383
action, _, err := l.LBClient.ChangeAlgorithm(ctx, lb, opts)
384384
if err != nil {
385-
return false, fmt.Errorf("%s: %w", op, err)
385+
return false, fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
386386
}
387387
err = l.ActionClient.WaitFor(ctx, action)
388388
if err != nil {
@@ -409,7 +409,7 @@ func (l *LoadBalancerOps) changeType(ctx context.Context, lb *hcloud.LoadBalance
409409
opts := hcloud.LoadBalancerChangeTypeOpts{LoadBalancerType: &hcloud.LoadBalancerType{Name: lt}}
410410
action, _, err := l.LBClient.ChangeType(ctx, lb, opts)
411411
if err != nil {
412-
return false, fmt.Errorf("%s: %w", op, err)
412+
return false, fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
413413
}
414414
err = l.ActionClient.WaitFor(ctx, action)
415415
if err != nil {
@@ -515,7 +515,7 @@ func (l *LoadBalancerOps) attachToNetwork(ctx context.Context, lb *hcloud.LoadBa
515515
a, _, err = l.LBClient.AttachToNetwork(ctx, lb, opts)
516516
}
517517
if err != nil {
518-
return false, fmt.Errorf("%s: %w", op, err)
518+
return false, fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
519519
}
520520

521521
if err := l.ActionClient.WaitFor(ctx, a); err != nil {
@@ -954,7 +954,7 @@ func (l *LoadBalancerOps) ReconcileHCLBServices(
954954
}
955955
action, _, err = l.LBClient.UpdateService(ctx, lb, b.listenPort, updOpts)
956956
if err != nil {
957-
return changed, fmt.Errorf("%s: %w", op, err)
957+
return changed, fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
958958
}
959959
} else {
960960
klog.InfoS("add service", "op", op, "port", portNo, "loadBalancerID", lb.ID)
@@ -965,7 +965,7 @@ func (l *LoadBalancerOps) ReconcileHCLBServices(
965965
}
966966
action, _, err = l.LBClient.AddService(ctx, lb, addOpts)
967967
if err != nil {
968-
return changed, fmt.Errorf("%s: %w", op, err)
968+
return changed, fmt.Errorf("%s: %w", op, withInvalidInputFields(err))
969969
}
970970
}
971971

internal/hcops/load_balancer_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,6 +1749,45 @@ func TestLoadBalancerOps_ReconcileHCLBServices(t *testing.T) {
17491749
assert.True(t, changed)
17501750
},
17511751
},
1752+
{
1753+
name: "add service rejected by the API",
1754+
servicePorts: []corev1.ServicePort{
1755+
{Port: 80, NodePort: 8080},
1756+
},
1757+
initialLB: &hcloud.LoadBalancer{
1758+
ID: 4,
1759+
LoadBalancerType: &hcloud.LoadBalancerType{
1760+
MaxTargets: 25,
1761+
},
1762+
},
1763+
mock: func(_ *testing.T, tt *LBReconcilementTestCase) {
1764+
opts := hcloud.LoadBalancerAddServiceOpts{
1765+
Protocol: hcloud.LoadBalancerServiceProtocolTCP,
1766+
ListenPort: new(80),
1767+
DestinationPort: new(8080),
1768+
HealthCheck: &hcloud.LoadBalancerAddServiceOptsHealthCheck{
1769+
Protocol: hcloud.LoadBalancerServiceProtocolTCP,
1770+
Port: new(8080),
1771+
},
1772+
}
1773+
tt.fx.MockAddService(opts, tt.initialLB, hcloud.Error{
1774+
Code: hcloud.ErrorCodeInvalidInput,
1775+
Message: "invalid input in field 'http'",
1776+
Details: hcloud.ErrorDetailsInvalidInput{
1777+
Fields: []hcloud.ErrorDetailsInvalidInputField{
1778+
{Name: "http.timeout_idle", Messages: []string{"must be between 5 and 3600"}},
1779+
},
1780+
},
1781+
})
1782+
},
1783+
perform: func(t *testing.T, tt *LBReconcilementTestCase) {
1784+
changed, err := tt.fx.LBOps.ReconcileHCLBServices(tt.fx.Ctx, tt.initialLB, tt.service)
1785+
assert.EqualError(t, err,
1786+
"hcops/LoadBalancerOps.ReconcileHCLBServices: invalid input in field 'http' (invalid_input): "+
1787+
"invalid fields: http.timeout_idle (must be between 5 and 3600)")
1788+
assert.False(t, changed)
1789+
},
1790+
},
17521791
{
17531792
name: "reference TLS certificate by id",
17541793
servicePorts: []corev1.ServicePort{

0 commit comments

Comments
 (0)