diff --git a/pkg/stackit/client/helper.go b/pkg/stackit/client/helper.go index 82cb2640..981b553d 100644 --- a/pkg/stackit/client/helper.go +++ b/pkg/stackit/client/helper.go @@ -9,16 +9,17 @@ import ( sdkWait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" ) -func withResponseID[T any](ctx context.Context, call func(context.Context) (T, error)) (T, error) { +func execute[T any](ctx context.Context, call func(context.Context) (T, error)) (T, error) { var httpResp *http.Response ctx = runtime.WithCaptureHTTPResponse(ctx, &httpResp) resp, err := call(ctx) if err != nil { var zero T + err = stackiterrors.WrapError(err, "X-Trace-Id", runtime.GetTraceId(ctx)) if httpResp != nil { reqID := httpResp.Header.Get(sdkWait.XRequestIDHeader) - return zero, stackiterrors.WrapErrorWithResponseID(err, reqID) + err = stackiterrors.WrapErrorWithResponseID(err, reqID) } return zero, err } diff --git a/pkg/stackit/client/helper_test.go b/pkg/stackit/client/helper_test.go new file mode 100644 index 00000000..5b2ff4cf --- /dev/null +++ b/pkg/stackit/client/helper_test.go @@ -0,0 +1,27 @@ +package client + +import ( + "context" + "errors" + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" +) + +var _ = Describe("execute", func() { + It("wraps API errors with trace and request IDs", func() { + _, err := execute(context.Background(), func(ctx context.Context) (int, error) { + response, ok := ctx.Value(sdkconfig.ContextHTTPResponse).(**http.Response) + Expect(ok).To(BeTrue()) + *response = &http.Response{Header: http.Header{ + "X-Trace-Id": {"trace-123"}, + "X-Request-Id": {"request-456"}, + }} + return 0, errors.New("api error") + }) + + Expect(err).To(MatchError("[X-Request-Id:request-456]: [X-Trace-Id:trace-123]: api error")) + }) +}) diff --git a/pkg/stackit/client/iaas.go b/pkg/stackit/client/iaas.go index 56ed1588..d11e05e9 100644 --- a/pkg/stackit/client/iaas.go +++ b/pkg/stackit/client/iaas.go @@ -109,19 +109,19 @@ func NewIaaSClient(region, projectID string, options []sdkconfig.ConfigurationOp } func (i *iaasClient) GetServer(ctx context.Context, serverID string) (*iaas.Server, error) { - return withResponseID(ctx, func(ctx context.Context) (*iaas.Server, error) { + return execute(ctx, func(ctx context.Context) (*iaas.Server, error) { return i.Client.GetServer(ctx, i.projectID, i.region, serverID).Execute() }) } func (i *iaasClient) GetServerWithDetails(ctx context.Context, serverID string) (*iaas.Server, error) { - return withResponseID(ctx, func(ctx context.Context) (*iaas.Server, error) { + return execute(ctx, func(ctx context.Context) (*iaas.Server, error) { return i.Client.GetServer(ctx, i.projectID, i.region, serverID).Details(true).Execute() }) } func (i *iaasClient) ListServers(ctx context.Context) (*[]iaas.Server, error) { - return withResponseID(ctx, func(ctx context.Context) (*[]iaas.Server, error) { + return execute(ctx, func(ctx context.Context) (*[]iaas.Server, error) { resp, err := i.Client.ListServers(ctx, i.projectID, i.region).Details(true).Execute() if err != nil { return nil, err @@ -133,7 +133,7 @@ func (i *iaasClient) ListServers(ctx context.Context) (*[]iaas.Server, error) { //nolint:gocritic // Payload is passed by value to match the shared IaaSClient interface. func (i *iaasClient) CreateSnapshot(ctx context.Context, payload iaas.CreateSnapshotPayload) (*iaas.Snapshot, error) { - return withResponseID(ctx, func(ctx context.Context) (*iaas.Snapshot, error) { + return execute(ctx, func(ctx context.Context) (*iaas.Snapshot, error) { return i.Client. CreateSnapshot(ctx, i.projectID, i.region). CreateSnapshotPayload(payload). @@ -142,7 +142,7 @@ func (i *iaasClient) CreateSnapshot(ctx context.Context, payload iaas.CreateSnap } func (i *iaasClient) ListSnapshots(ctx context.Context, filters map[string]string) ([]iaas.Snapshot, string, error) { - resp, err := withResponseID(ctx, func(ctx context.Context) (*iaas.SnapshotListResponse, error) { + resp, err := execute(ctx, func(ctx context.Context) (*iaas.SnapshotListResponse, error) { return i.Client.ListSnapshotsInProject(ctx, i.projectID, i.region).Execute() }) if err != nil { @@ -155,14 +155,14 @@ func (i *iaasClient) ListSnapshots(ctx context.Context, filters map[string]strin } func (i *iaasClient) DeleteSnapshot(ctx context.Context, snapshotID string) error { - _, err := withResponseID(ctx, func(ctx context.Context) (any, error) { + _, err := execute(ctx, func(ctx context.Context) (any, error) { return nil, i.Client.DeleteSnapshot(ctx, i.projectID, i.region, snapshotID).Execute() }) return err } func (i *iaasClient) GetSnapshot(ctx context.Context, snapshotID string) (*iaas.Snapshot, error) { - return withResponseID(ctx, func(ctx context.Context) (*iaas.Snapshot, error) { + return execute(ctx, func(ctx context.Context) (*iaas.Snapshot, error) { return i.Client.GetSnapshot(ctx, i.projectID, i.region, snapshotID).Execute() }) } @@ -197,7 +197,7 @@ func (i *iaasClient) WaitSnapshotReady(ctx context.Context, snapshotID string) ( } func (i *iaasClient) snapshotIsReady(ctx context.Context, snapshotID string) (bool, error) { - snapshot, err := withResponseID(ctx, func(ctx context.Context) (*iaas.Snapshot, error) { + snapshot, err := execute(ctx, func(ctx context.Context) (*iaas.Snapshot, error) { return i.Client.GetSnapshot(ctx, i.projectID, i.region, snapshotID).Execute() }) if err != nil { @@ -213,7 +213,7 @@ func (i *iaasClient) CreateBackup(ctx context.Context, name, volID, snapshotID s return nil, err } - return withResponseID(ctx, func(ctx context.Context) (*iaas.Backup, error) { + return execute(ctx, func(ctx context.Context) (*iaas.Backup, error) { return i.Client. CreateBackup(ctx, i.projectID, i.region). CreateBackupPayload(payload). @@ -257,7 +257,7 @@ func BuildCreateBackupPayload(name, volID, snapshotID string, tags map[string]st } func (i *iaasClient) ListBackups(ctx context.Context, filters map[string]string) ([]iaas.Backup, error) { - resp, err := withResponseID(ctx, func(ctx context.Context) (*iaas.BackupListResponse, error) { + resp, err := execute(ctx, func(ctx context.Context) (*iaas.BackupListResponse, error) { return i.Client.ListBackups(ctx, i.projectID, i.region).Execute() }) if err != nil { @@ -270,14 +270,14 @@ func (i *iaasClient) ListBackups(ctx context.Context, filters map[string]string) } func (i *iaasClient) DeleteBackup(ctx context.Context, backupID string) error { - _, err := withResponseID(ctx, func(ctx context.Context) (any, error) { + _, err := execute(ctx, func(ctx context.Context) (any, error) { return nil, i.Client.DeleteBackup(ctx, i.projectID, i.region, backupID).Execute() }) return err } func (i *iaasClient) GetBackup(ctx context.Context, backupID string) (*iaas.Backup, error) { - return withResponseID(ctx, func(ctx context.Context) (*iaas.Backup, error) { + return execute(ctx, func(ctx context.Context) (*iaas.Backup, error) { return i.Client.GetBackup(ctx, i.projectID, i.region, backupID).Execute() }) } @@ -341,7 +341,7 @@ func (i *iaasClient) backupIsReady(ctx context.Context, backupID string) (bool, func (i *iaasClient) CreateVolume(ctx context.Context, payload iaas.CreateVolumePayload) (*iaas.Volume, error) { payload.Description = new(VolumeDescription) - return withResponseID(ctx, func(ctx context.Context) (*iaas.Volume, error) { + return execute(ctx, func(ctx context.Context) (*iaas.Volume, error) { return i.Client.CreateVolume(ctx, i.projectID, i.region).CreateVolumePayload(payload).Execute() }) } @@ -355,7 +355,7 @@ func (i *iaasClient) DeleteVolume(ctx context.Context, volumeID string) error { return fmt.Errorf("cannot delete the volume %q, it's still attached to a node", volumeID) } - _, err = withResponseID(ctx, func(ctx context.Context) (any, error) { + _, err = execute(ctx, func(ctx context.Context) (any, error) { return nil, i.Client.DeleteVolume(ctx, i.projectID, i.region, volumeID).Execute() }) return err @@ -372,7 +372,7 @@ func (i *iaasClient) AttachVolume(ctx context.Context, serverID, volumeID string return *volume.Id, nil } - _, err = withResponseID(ctx, func(ctx context.Context) (any, error) { + _, err = execute(ctx, func(ctx context.Context) (any, error) { return i.Client. AddVolumeToServer(ctx, i.projectID, i.region, serverID, volumeID). AddVolumeToServerPayload(payload). @@ -386,13 +386,13 @@ func (i *iaasClient) AttachVolume(ctx context.Context, serverID, volumeID string } func (i *iaasClient) GetVolume(ctx context.Context, volumeID string) (*iaas.Volume, error) { - return withResponseID(ctx, func(ctx context.Context) (*iaas.Volume, error) { + return execute(ctx, func(ctx context.Context) (*iaas.Volume, error) { return i.Client.GetVolume(ctx, i.projectID, i.region, volumeID).Execute() }) } func (i *iaasClient) GetVolumesByName(ctx context.Context, volName string) ([]iaas.Volume, error) { - resp, err := withResponseID(ctx, func(ctx context.Context) (*iaas.VolumeListResponse, error) { + resp, err := execute(ctx, func(ctx context.Context) (*iaas.VolumeListResponse, error) { return i.Client.ListVolumes(ctx, i.projectID, i.region).Execute() }) if err != nil { @@ -407,7 +407,7 @@ func (i *iaasClient) GetVolumesByName(ctx context.Context, volName string) ([]ia func (i *iaasClient) ListVolumes(ctx context.Context, _ int, _ string) ([]iaas.Volume, string, error) { // TODO: Add support for pagination when IaaS adds it - resp, err := withResponseID(ctx, func(ctx context.Context) (*iaas.VolumeListResponse, error) { + resp, err := execute(ctx, func(ctx context.Context) (*iaas.VolumeListResponse, error) { return i.Client.ListVolumes(ctx, i.projectID, i.region).Execute() }) if err != nil { @@ -420,7 +420,7 @@ func (i *iaasClient) ListVolumes(ctx context.Context, _ int, _ string) ([]iaas.V func (i *iaasClient) ExpandVolume(ctx context.Context, volumeID, volumeStatus string, payload iaas.ResizeVolumePayload) error { switch volumeStatus { case VolumeAttachedStatus, VolumeAvailableStatus: - _, err := withResponseID(ctx, func(ctx context.Context) (any, error) { + _, err := execute(ctx, func(ctx context.Context) (any, error) { return nil, i.Client. ResizeVolume(ctx, i.projectID, i.region, volumeID). ResizeVolumePayload(payload). @@ -525,7 +525,7 @@ func (i *iaasClient) DetachVolume(ctx context.Context, serverID, volumeID string } if volume.ServerId != nil && *volume.ServerId == serverID { - _, err := withResponseID(ctx, func(ctx context.Context) (any, error) { + _, err := execute(ctx, func(ctx context.Context) (any, error) { err := i.Client.RemoveVolumeFromServer(ctx, i.projectID, i.region, serverID, volumeID).Execute() if err != nil { return nil, fmt.Errorf("failed to detach volume %s from compute %s : %w", *volume.Name, serverID, err) diff --git a/pkg/stackit/client/loadbalancer.go b/pkg/stackit/client/loadbalancer.go index 965df796..8b2b8bd2 100644 --- a/pkg/stackit/client/loadbalancer.go +++ b/pkg/stackit/client/loadbalancer.go @@ -40,7 +40,7 @@ func NewLoadBalancingClient(region, projectID string, options []sdkconfig.Config } func (l *loadBalancingClient) CreateLoadBalancer(ctx context.Context, payload *loadbalancer.CreateLoadBalancerPayload) (*loadbalancer.LoadBalancer, error) { - return withResponseID(ctx, func(ctx context.Context) (*loadbalancer.LoadBalancer, error) { + return execute(ctx, func(ctx context.Context) (*loadbalancer.LoadBalancer, error) { return l.Client. CreateLoadBalancer(ctx, l.projectID, l.region). CreateLoadBalancerPayload(*payload). @@ -50,7 +50,7 @@ func (l *loadBalancingClient) CreateLoadBalancer(ctx context.Context, payload *l } func (l *loadBalancingClient) DeleteLoadBalancer(ctx context.Context, lbName string) error { - _, err := withResponseID(ctx, func(ctx context.Context) (map[string]any, error) { + _, err := execute(ctx, func(ctx context.Context) (map[string]any, error) { return l.Client. DeleteLoadBalancer(ctx, l.projectID, l.region, lbName). Execute() @@ -59,7 +59,7 @@ func (l *loadBalancingClient) DeleteLoadBalancer(ctx context.Context, lbName str } func (l *loadBalancingClient) GetLoadBalancer(ctx context.Context, lbName string) (*loadbalancer.LoadBalancer, error) { - return withResponseID(ctx, func(ctx context.Context) (*loadbalancer.LoadBalancer, error) { + return execute(ctx, func(ctx context.Context) (*loadbalancer.LoadBalancer, error) { return l.Client. GetLoadBalancer(ctx, l.projectID, l.region, lbName). Execute() @@ -67,7 +67,7 @@ func (l *loadBalancingClient) GetLoadBalancer(ctx context.Context, lbName string } func (l *loadBalancingClient) UpdateLoadBalancer(ctx context.Context, lbName string, updates *loadbalancer.UpdateLoadBalancerPayload) (*loadbalancer.LoadBalancer, error) { - return withResponseID(ctx, func(ctx context.Context) (*loadbalancer.LoadBalancer, error) { + return execute(ctx, func(ctx context.Context) (*loadbalancer.LoadBalancer, error) { return l.Client. UpdateLoadBalancer(ctx, l.projectID, l.region, lbName). UpdateLoadBalancerPayload(*updates). @@ -76,7 +76,7 @@ func (l *loadBalancingClient) UpdateLoadBalancer(ctx context.Context, lbName str } func (l *loadBalancingClient) UpdateTargetPool(ctx context.Context, name, targetPoolName string, payload loadbalancer.UpdateTargetPoolPayload) error { - _, err := withResponseID(ctx, func(ctx context.Context) (*loadbalancer.TargetPool, error) { + _, err := execute(ctx, func(ctx context.Context) (*loadbalancer.TargetPool, error) { return l.Client. UpdateTargetPool(ctx, l.projectID, l.region, name, targetPoolName). UpdateTargetPoolPayload(payload). @@ -86,7 +86,7 @@ func (l *loadBalancingClient) UpdateTargetPool(ctx context.Context, name, target } func (l *loadBalancingClient) CreateCredentials(ctx context.Context, payload loadbalancer.CreateCredentialsPayload) (*loadbalancer.CreateCredentialsResponse, error) { - return withResponseID(ctx, func(ctx context.Context) (*loadbalancer.CreateCredentialsResponse, error) { + return execute(ctx, func(ctx context.Context) (*loadbalancer.CreateCredentialsResponse, error) { return l.Client. CreateCredentials(ctx, l.projectID, l.region). CreateCredentialsPayload(payload). @@ -96,7 +96,7 @@ func (l *loadBalancingClient) CreateCredentials(ctx context.Context, payload loa } func (l *loadBalancingClient) ListCredentials(ctx context.Context) (*loadbalancer.ListCredentialsResponse, error) { - return withResponseID(ctx, func(ctx context.Context) (*loadbalancer.ListCredentialsResponse, error) { + return execute(ctx, func(ctx context.Context) (*loadbalancer.ListCredentialsResponse, error) { return l.Client. ListCredentials(ctx, l.projectID, l.region). Execute() @@ -104,7 +104,7 @@ func (l *loadBalancingClient) ListCredentials(ctx context.Context) (*loadbalance } func (l *loadBalancingClient) UpdateCredentials(ctx context.Context, credentialsRef string, payload loadbalancer.UpdateCredentialsPayload) error { - _, err := withResponseID(ctx, func(ctx context.Context) (*loadbalancer.UpdateCredentialsResponse, error) { + _, err := execute(ctx, func(ctx context.Context) (*loadbalancer.UpdateCredentialsResponse, error) { return l.Client. UpdateCredentials(ctx, l.projectID, l.region, credentialsRef). UpdateCredentialsPayload(payload). @@ -114,7 +114,7 @@ func (l *loadBalancingClient) UpdateCredentials(ctx context.Context, credentials } func (l *loadBalancingClient) DeleteCredentials(ctx context.Context, credentialsRef string) error { - _, err := withResponseID(ctx, func(ctx context.Context) (map[string]any, error) { + _, err := execute(ctx, func(ctx context.Context) (map[string]any, error) { return l.Client. DeleteCredentials(ctx, l.projectID, l.region, credentialsRef). Execute() diff --git a/pkg/stackit/stackiterrors/errors.go b/pkg/stackit/stackiterrors/errors.go index 1b1f127a..2dc30772 100644 --- a/pkg/stackit/stackiterrors/errors.go +++ b/pkg/stackit/stackiterrors/errors.go @@ -40,16 +40,20 @@ func IgnoreNotFound(err error) error { return err } -// WrapErrorWithResponseID wraps the error with the X-Request-Id but only if the error is not nil -func WrapErrorWithResponseID(err error, reqID string) error { +// WrapError wraps the error with an identifier but only if the error is not nil. +func WrapError(err error, name, id string) error { if err == nil { return nil } - // if the request id is empty we don't wrap the error - if reqID == "" { + if id == "" { return err } - return fmt.Errorf("[%s:%s]: %w", wait.XRequestIDHeader, reqID, err) + return fmt.Errorf("[%s:%s]: %w", name, id, err) +} + +// WrapErrorWithResponseID wraps the error with the X-Request-Id. +func WrapErrorWithResponseID(err error, reqID string) error { + return WrapError(err, wait.XRequestIDHeader, reqID) } func IsInvalidError(err error) bool { diff --git a/pkg/stackit/stackiterrors/errors_test.go b/pkg/stackit/stackiterrors/errors_test.go index e3be05b8..f603b71d 100644 --- a/pkg/stackit/stackiterrors/errors_test.go +++ b/pkg/stackit/stackiterrors/errors_test.go @@ -95,6 +95,23 @@ var _ = Describe("Errors", func() { }) }) + Describe("WrapError", func() { + It("wraps the error with the provided identifier", func() { + err := errors.New("test error") + expected := fmt.Errorf("[X-Trace-Id:12345]: %w", err) + Expect(WrapError(err, "X-Trace-Id", "12345")).To(Equal(expected)) + }) + + It("returns the original error when the identifier is empty", func() { + err := errors.New("test error") + Expect(WrapError(err, "trace-id", "")).To(Equal(err)) + }) + + It("returns nil when the error is nil", func() { + Expect(WrapError(nil, "trace-id", "12345")).To(Succeed()) + }) + }) + Describe("IsInvalidError", func() { Context("when error is a BadRequest error", func() { It("should return true", func() {