diff --git a/services/iaasalpha/go.mod b/services/iaasalpha/go.mod index 51875d650..53dee80e3 100644 --- a/services/iaasalpha/go.mod +++ b/services/iaasalpha/go.mod @@ -2,7 +2,10 @@ module github.com/stackitcloud/stackit-sdk-go/services/iaasalpha go 1.18 -require github.com/stackitcloud/stackit-sdk-go/core v0.13.0 +require ( + github.com/google/go-cmp v0.6.0 + github.com/stackitcloud/stackit-sdk-go/core v0.13.0 +) require ( github.com/golang-jwt/jwt/v5 v5.2.1 // indirect diff --git a/services/iaasalpha/go.sum b/services/iaasalpha/go.sum index d29515edb..62f3cf6f4 100644 --- a/services/iaasalpha/go.sum +++ b/services/iaasalpha/go.sum @@ -1,6 +1,7 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/stackitcloud/stackit-sdk-go/core v0.13.0 h1:BtJT2WXqZdexPPQi/HPUIr8g4JUPOCheh6J9dxiCQ4Q= diff --git a/services/iaasalpha/wait/wait.go b/services/iaasalpha/wait/wait.go new file mode 100644 index 000000000..db9e0a44c --- /dev/null +++ b/services/iaasalpha/wait/wait.go @@ -0,0 +1,73 @@ +package wait + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/core/wait" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + AvailableStatus = "AVAILABLE" + DeleteSuccess = "DELETED" + ErrorStatus = "ERROR" +) + +// Interfaces needed for tests +type APIClientInterface interface { + GetVolumeExecute(ctx context.Context, projectId string, volumeId string) (*iaasalpha.Volume, error) +} + +// CreateVolumeWaitHandler will wait for volume creation +func CreateVolumeWaitHandler(ctx context.Context, a APIClientInterface, projectId, volumeId string) *wait.AsyncActionHandler[iaasalpha.Volume] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.Volume, err error) { + volume, err := a.GetVolumeExecute(ctx, projectId, volumeId) + if err != nil { + return false, volume, err + } + if volume.Id == nil || volume.Status == nil { + return false, volume, fmt.Errorf("create failed for volume with id %s, the response is not valid: the id or the status are missing", volumeId) + } + if *volume.Id == volumeId && *volume.Status == AvailableStatus { + return true, volume, nil + } + if *volume.Id == volumeId && *volume.Status == ErrorStatus { + return true, volume, fmt.Errorf("create failed for volume with id %s", volumeId) + } + return false, volume, nil + }) + handler.SetTimeout(10 * time.Minute) + return handler +} + +// DeleteVolumeWaitHandler will wait for volume deletion +func DeleteVolumeWaitHandler(ctx context.Context, a APIClientInterface, projectId, volumeId string) *wait.AsyncActionHandler[iaasalpha.Volume] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.Volume, err error) { + volume, err := a.GetVolumeExecute(ctx, projectId, volumeId) + if err == nil { + if volume != nil { + if volume.Id == nil || volume.Status == nil { + return false, volume, fmt.Errorf("delete failed for volume with id %s, the response is not valid: the id or the status are missing", volumeId) + } + if *volume.Id == volumeId && *volume.Status == DeleteSuccess { + return true, volume, nil + } + } + return false, nil, nil + } + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if !ok { + return false, volume, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError: %w", err) + } + if oapiErr.StatusCode != http.StatusNotFound { + return false, volume, err + } + return true, nil, nil + }) + handler.SetTimeout(10 * time.Minute) + return handler +} diff --git a/services/iaasalpha/wait/wait_test.go b/services/iaasalpha/wait/wait_test.go new file mode 100644 index 000000000..51852b7d3 --- /dev/null +++ b/services/iaasalpha/wait/wait_test.go @@ -0,0 +1,164 @@ +package wait + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +type apiClientMocked struct { + getVolumeFails bool + isDeleted bool + resourceState string +} + +func (a *apiClientMocked) GetVolumeExecute(_ context.Context, _, _ string) (*iaasalpha.Volume, error) { + if a.isDeleted { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 404, + } + } + + if a.getVolumeFails { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 500, + } + } + + return &iaasalpha.Volume{ + Id: utils.Ptr("vid"), + Status: &a.resourceState, + }, nil +} + +func TestCreateVolumeWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + resourceState string + wantErr bool + wantResp bool + }{ + { + desc: "create_succeeded", + getFails: false, + resourceState: AvailableStatus, + wantErr: false, + wantResp: true, + }, + { + desc: "error_status", + getFails: false, + resourceState: ErrorStatus, + wantErr: true, + wantResp: true, + }, + { + desc: "get_fails", + getFails: true, + resourceState: "", + wantErr: true, + wantResp: false, + }, + { + desc: "timeout", + getFails: false, + resourceState: "ANOTHER Status", + wantErr: true, + wantResp: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getVolumeFails: tt.getFails, + resourceState: tt.resourceState, + } + + var wantRes *iaasalpha.Volume + if tt.wantResp { + wantRes = &iaasalpha.Volume{ + Id: utils.Ptr("vid"), + Status: &tt.resourceState, + } + } + + handler := CreateVolumeWaitHandler(context.Background(), apiClient, "pid", "vid") + + gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background()) + + if (err != nil) != tt.wantErr { + t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) + } + if !cmp.Equal(gotRes, wantRes) { + t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes) + } + }) + } +} + +func TestDeleteVolumeWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + isDeleted bool + resourceState string + wantErr bool + wantResp bool + }{ + { + desc: "delete_succeeded", + getFails: false, + isDeleted: true, + wantErr: false, + wantResp: false, + }, + { + desc: "get_fails", + getFails: true, + resourceState: "", + wantErr: true, + wantResp: false, + }, + { + desc: "timeout", + getFails: false, + resourceState: "ANOTHER Status", + wantErr: true, + wantResp: false, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getVolumeFails: tt.getFails, + isDeleted: tt.isDeleted, + resourceState: tt.resourceState, + } + + var wantRes *iaasalpha.Volume + if tt.wantResp { + wantRes = &iaasalpha.Volume{ + Id: utils.Ptr("vid"), + Status: &tt.resourceState, + } + } + + handler := DeleteVolumeWaitHandler(context.Background(), apiClient, "pid", "vid") + + gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background()) + + if (err != nil) != tt.wantErr { + t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) + } + if !cmp.Equal(gotRes, wantRes) { + t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes) + } + }) + } +}