From 4c0abf4edca3e7844d1cfbf6f2f3192a964cc155 Mon Sep 17 00:00:00 2001 From: Vicente Pinto Date: Thu, 26 Sep 2024 17:41:45 +0100 Subject: [PATCH 1/3] Add waiters for iaas volumes --- services/iaasalpha/go.mod | 5 +- services/iaasalpha/go.sum | 1 + services/iaasalpha/wait/wait.go | 69 ++++++++++++ services/iaasalpha/wait/wait_test.go | 159 +++++++++++++++++++++++++++ 4 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 services/iaasalpha/wait/wait.go create mode 100644 services/iaasalpha/wait/wait_test.go 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..9863936fa --- /dev/null +++ b/services/iaasalpha/wait/wait.go @@ -0,0 +1,69 @@ +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" +) + +// 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 + } + 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 { + 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 { + if volume != nil { + if volume.Id == nil || volume.Status == nil { + return false, nil, 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, nil, nil + } + } + 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..17998d06d --- /dev/null +++ b/services/iaasalpha/wait/wait_test.go @@ -0,0 +1,159 @@ +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 + getNetworkFails bool + getProjectRequestFails 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: "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) + } + }) + } +} From 1a7072b7db1e4850f18537a4d09357e5304f6c9b Mon Sep 17 00:00:00 2001 From: Vicente Pinto Date: Thu, 26 Sep 2024 17:55:08 +0100 Subject: [PATCH 2/3] fix lint --- services/iaasalpha/wait/wait_test.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/services/iaasalpha/wait/wait_test.go b/services/iaasalpha/wait/wait_test.go index 17998d06d..1c0e9a79b 100644 --- a/services/iaasalpha/wait/wait_test.go +++ b/services/iaasalpha/wait/wait_test.go @@ -12,11 +12,9 @@ import ( ) type apiClientMocked struct { - getVolumeFails bool - getNetworkFails bool - getProjectRequestFails bool - isDeleted bool - resourceState string + getVolumeFails bool + isDeleted bool + resourceState string } func (a *apiClientMocked) GetVolumeExecute(_ context.Context, _, _ string) (*iaasalpha.Volume, error) { From 2179680f0bbc9c98aa08d919fcb7793a33080632 Mon Sep 17 00:00:00 2001 From: Vicente Pinto Date: Fri, 27 Sep 2024 14:57:15 +0100 Subject: [PATCH 3/3] Check for error status on Create --- services/iaasalpha/wait/wait.go | 20 ++++++++++++-------- services/iaasalpha/wait/wait_test.go | 7 +++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/services/iaasalpha/wait/wait.go b/services/iaasalpha/wait/wait.go index 9863936fa..db9e0a44c 100644 --- a/services/iaasalpha/wait/wait.go +++ b/services/iaasalpha/wait/wait.go @@ -14,6 +14,7 @@ import ( const ( AvailableStatus = "AVAILABLE" DeleteSuccess = "DELETED" + ErrorStatus = "ERROR" ) // Interfaces needed for tests @@ -34,6 +35,9 @@ func CreateVolumeWaitHandler(ctx context.Context, a APIClientInterface, projectI 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) @@ -45,6 +49,14 @@ func DeleteVolumeWaitHandler(ctx context.Context, a APIClientInterface, projectI 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 @@ -52,14 +64,6 @@ func DeleteVolumeWaitHandler(ctx context.Context, a APIClientInterface, projectI return false, volume, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError: %w", err) } if oapiErr.StatusCode != http.StatusNotFound { - if volume != nil { - if volume.Id == nil || volume.Status == nil { - return false, nil, 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, nil, nil - } - } return false, volume, err } return true, nil, nil diff --git a/services/iaasalpha/wait/wait_test.go b/services/iaasalpha/wait/wait_test.go index 1c0e9a79b..51852b7d3 100644 --- a/services/iaasalpha/wait/wait_test.go +++ b/services/iaasalpha/wait/wait_test.go @@ -51,6 +51,13 @@ func TestCreateVolumeWaitHandler(t *testing.T) { wantErr: false, wantResp: true, }, + { + desc: "error_status", + getFails: false, + resourceState: ErrorStatus, + wantErr: true, + wantResp: true, + }, { desc: "get_fails", getFails: true,