From 43f0bfaba452278f5991526f7fec13b8e07ed613 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Wed, 2 Jul 2025 09:56:32 +0200 Subject: [PATCH 1/2] add iaasalpha wait handler --- services/iaasalpha/go.mod | 5 +- services/iaasalpha/wait/wait.go | 83 ++++++++++ services/iaasalpha/wait/wait_test.go | 216 +++++++++++++++++++++++++++ 3 files changed, 303 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 e0b39a0a6..47070d132 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.21 -require github.com/stackitcloud/stackit-sdk-go/core v0.17.2 +require ( + github.com/google/go-cmp v0.7.0 + github.com/stackitcloud/stackit-sdk-go/core v0.17.2 +) require ( github.com/golang-jwt/jwt/v5 v5.2.2 // indirect diff --git a/services/iaasalpha/wait/wait.go b/services/iaasalpha/wait/wait.go new file mode 100644 index 000000000..2fb42f78f --- /dev/null +++ b/services/iaasalpha/wait/wait.go @@ -0,0 +1,83 @@ +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 ( + CreateSuccess = "CREATED" +) + +// Interfaces needed for tests +type APIClientInterface interface { + GetNetworkExecute(ctx context.Context, projectId, region, networkId string) (*iaasalpha.Network, error) +} + +// CreateNetworkWaitHandler will wait for network creation using network id +func CreateNetworkWaitHandler(ctx context.Context, a APIClientInterface, projectId, region, networkId string) *wait.AsyncActionHandler[iaasalpha.Network] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.Network, err error) { + network, err := a.GetNetworkExecute(ctx, projectId, region, networkId) + if err != nil { + return false, network, err + } + if network.Id == nil || network.Status == nil { + return false, network, fmt.Errorf("crate failed for network with id %s, the response is not valid: the id or the state are missing", networkId) + } + // The state returns to "CREATED" after a successful creation is completed + if *network.Id == networkId && *network.Status == CreateSuccess { + return true, network, nil + } + return false, network, nil + }) + handler.SetSleepBeforeWait(2 * time.Second) + handler.SetTimeout(15 * time.Minute) + return handler +} + +// UpdateNetworkWaitHandler will wait for network update +func UpdateNetworkWaitHandler(ctx context.Context, a APIClientInterface, projectId, region, networkId string) *wait.AsyncActionHandler[iaasalpha.Network] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.Network, err error) { + network, err := a.GetNetworkExecute(ctx, projectId, region, networkId) + if err != nil { + return false, network, err + } + if network.Id == nil || network.Status == nil { + return false, network, fmt.Errorf("update failed for network with id %s, the response is not valid: the id or the state are missing", networkId) + } + // The state returns to "CREATED" after a successful update is completed + if *network.Id == networkId && *network.Status == CreateSuccess { + return true, network, nil + } + return false, network, nil + }) + handler.SetSleepBeforeWait(2 * time.Second) + handler.SetTimeout(15 * time.Minute) + return handler +} + +// DeleteNetworkWaitHandler will wait for network deletion +func DeleteNetworkWaitHandler(ctx context.Context, a APIClientInterface, projectId, region, networkId string) *wait.AsyncActionHandler[iaasalpha.Network] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.Network, err error) { + network, err := a.GetNetworkExecute(ctx, projectId, region, networkId) + 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, network, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError: %w", err) + } + if oapiErr.StatusCode != http.StatusNotFound { + return false, network, err + } + return true, nil, nil + }) + handler.SetTimeout(15 * 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..a9aba5b11 --- /dev/null +++ b/services/iaasalpha/wait/wait_test.go @@ -0,0 +1,216 @@ +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 { + getNetworkFails bool + isDeleted bool + resourceState string +} + +func (a *apiClientMocked) GetNetworkExecute(_ context.Context, _, _, _ string) (*iaasalpha.Network, error) { + if a.isDeleted { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 404, + } + } + + if a.getNetworkFails { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 500, + } + } + + return &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Status: &a.resourceState, + }, nil +} + +func TestCreateNetworkWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + resourceState string + wantErr bool + wantResp bool + }{ + { + desc: "create_succeeded", + getFails: false, + resourceState: CreateSuccess, + wantErr: false, + wantResp: true, + }, + { + desc: "get_fails", + getFails: true, + resourceState: "", + wantErr: true, + wantResp: false, + }, + { + desc: "timeout", + getFails: false, + resourceState: "ANOTHER STATE", + wantErr: true, + wantResp: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getNetworkFails: tt.getFails, + resourceState: tt.resourceState, + } + + var wantRes *iaasalpha.Network + if tt.wantResp { + wantRes = &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Status: utils.Ptr(tt.resourceState), + } + } + + handler := CreateNetworkWaitHandler(context.Background(), apiClient, "pid", "eu01", "nid") + + gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(1 * 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 TestUpdateNetworkWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + resourceState string + wantErr bool + wantResp bool + }{ + { + desc: "update_succeeded", + getFails: false, + resourceState: CreateSuccess, + wantErr: false, + wantResp: true, + }, + { + desc: "get_fails", + getFails: true, + resourceState: "", + wantErr: true, + wantResp: false, + }, + { + desc: "timeout", + getFails: false, + resourceState: "ANOTHER STATE", + wantErr: true, + wantResp: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getNetworkFails: tt.getFails, + resourceState: tt.resourceState, + } + + var wantRes *iaasalpha.Network + if tt.wantResp { + wantRes = &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Status: utils.Ptr(tt.resourceState), + } + } + + handler := UpdateNetworkWaitHandler(context.Background(), apiClient, "pid", "eu01", "nid") + + gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(1 * 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 TestDeleteNetworkWaitHandler(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 STATE", + wantErr: true, + wantResp: false, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getNetworkFails: tt.getFails, + isDeleted: tt.isDeleted, + resourceState: tt.resourceState, + } + + var wantRes *iaasalpha.Network + if tt.wantResp { + wantRes = &iaasalpha.Network{ + Id: utils.Ptr("nid"), + Status: utils.Ptr(tt.resourceState), + } + } + + handler := DeleteNetworkWaitHandler(context.Background(), apiClient, "pid", "eu01", "nid") + + 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 8eb25b5b42b6d789f96ecd18b84fb6b122dbd31e Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Wed, 2 Jul 2025 10:52:01 +0200 Subject: [PATCH 2/2] implement review feedback --- services/iaasalpha/wait/wait.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/iaasalpha/wait/wait.go b/services/iaasalpha/wait/wait.go index 2fb42f78f..6293355aa 100644 --- a/services/iaasalpha/wait/wait.go +++ b/services/iaasalpha/wait/wait.go @@ -2,6 +2,7 @@ package wait import ( "context" + "errors" "fmt" "net/http" "time" @@ -28,7 +29,7 @@ func CreateNetworkWaitHandler(ctx context.Context, a APIClientInterface, project return false, network, err } if network.Id == nil || network.Status == nil { - return false, network, fmt.Errorf("crate failed for network with id %s, the response is not valid: the id or the state are missing", networkId) + return false, network, fmt.Errorf("create failed for network with id %s, the response is not valid: the id or the state are missing", networkId) } // The state returns to "CREATED" after a successful creation is completed if *network.Id == networkId && *network.Status == CreateSuccess { @@ -69,7 +70,8 @@ func DeleteNetworkWaitHandler(ctx context.Context, a APIClientInterface, project 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 + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if !ok { return false, network, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError: %w", err) }