From 2df65773878d3928f9d3e903ddbff31a91088056 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Mon, 23 Mar 2026 17:40:56 +0100 Subject: [PATCH 1/2] feat(resourcemanager): refactor wait handler to use helper struct --- CHANGELOG.md | 9 ++- services/resourcemanager/CHANGELOG.md | 5 ++ services/resourcemanager/VERSION | 2 +- services/resourcemanager/v0api/wait/wait.go | 58 +++++++++---------- .../resourcemanager/v0api/wait/wait_test.go | 9 ++- 5 files changed, 49 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beab4125d..3fc0e787e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,8 +107,13 @@ - **Dependencies:** Bump STACKIT SDK core module from `v0.22.0` to `v0.23.0` - `redis`: [v0.27.1](services/redis/CHANGELOG.md#v0271) - **Dependencies:** Bump STACKIT SDK core module from `v0.22.0` to `v0.23.0` -- `resourcemanager`: [v0.20.1](services/resourcemanager/CHANGELOG.md#v0201) - - **Dependencies:** Bump STACKIT SDK core module from `v0.22.0` to `v0.23.0` +- `resourcemanager`: + - [v0.20.1](services/resourcemanager/CHANGELOG.md#v0201) + - **Dependencies:** Bump STACKIT SDK core module from `v0.22.0` to `v0.23.0` + - [v0.21.0](services/resourcemanager/CHANGELOG.md#v0210) + - `v0api` + - **Improvement**: Use new `WaiterHelper` struct in the DNS WaitHandler + - **Breaking change:** Change return type of `wait.DeleteProjectWaitHandler()` to `*wait.AsyncActionHandler[resourcemanager.GetProjectResponse]` - `runcommand`: [v1.6.1](services/runcommand/CHANGELOG.md#v161) - **Dependencies:** Bump STACKIT SDK core module from `v0.22.0` to `v0.23.0` - `scf`: [v0.6.1](services/scf/CHANGELOG.md#v061) diff --git a/services/resourcemanager/CHANGELOG.md b/services/resourcemanager/CHANGELOG.md index c89f5af77..d4b5c8681 100644 --- a/services/resourcemanager/CHANGELOG.md +++ b/services/resourcemanager/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.21.0 +- `v0api` + - **Improvement**: Use new `WaiterHelper` struct in the DNS WaitHandler + - **Breaking change:** Change return type of `wait.DeleteProjectWaitHandler()` to `*wait.AsyncActionHandler[resourcemanager.GetProjectResponse]` + ## v0.20.1 - **Dependencies:** Bump STACKIT SDK core module from `v0.22.0` to `v0.23.0` diff --git a/services/resourcemanager/VERSION b/services/resourcemanager/VERSION index f78b7047d..fcc9d59a4 100644 --- a/services/resourcemanager/VERSION +++ b/services/resourcemanager/VERSION @@ -1 +1 @@ -v0.20.1 \ No newline at end of file +v0.21.0 \ No newline at end of file diff --git a/services/resourcemanager/v0api/wait/wait.go b/services/resourcemanager/v0api/wait/wait.go index 7b20de406..7674a7bdc 100644 --- a/services/resourcemanager/v0api/wait/wait.go +++ b/services/resourcemanager/v0api/wait/wait.go @@ -2,51 +2,49 @@ package wait import ( "context" - "fmt" + "errors" "net/http" "time" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/core/wait" resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" ) // CreateProjectWaitHandler will wait for project creation func CreateProjectWaitHandler(ctx context.Context, a resourcemanager.DefaultAPI, containerId string) *wait.AsyncActionHandler[resourcemanager.GetProjectResponse] { - handler := wait.New(func() (waitFinished bool, response *resourcemanager.GetProjectResponse, err error) { - p, err := a.GetProject(ctx, containerId).Execute() - if err != nil { - return false, nil, err - } - if p.ContainerId == containerId && p.LifecycleState == resourcemanager.LIFECYCLESTATE_ACTIVE { - return true, p, nil - } - if p.ContainerId == containerId && p.LifecycleState == resourcemanager.LIFECYCLESTATE_CREATING { - return false, nil, nil - } - return true, p, fmt.Errorf("creation failed: received project state '%s'", p.LifecycleState) - }) + waitConfig := wait.WaiterHelper[resourcemanager.GetProjectResponse, resourcemanager.LifecycleState]{ + FetchInstance: a.GetProject(ctx, containerId).Execute, + GetState: func(project *resourcemanager.GetProjectResponse) (resourcemanager.LifecycleState, error) { + if project == nil { + return "", errors.New("empty response") + } + return project.LifecycleState, nil + }, + ActiveState: []resourcemanager.LifecycleState{resourcemanager.LIFECYCLESTATE_ACTIVE}, + ErrorState: []resourcemanager.LifecycleState{resourcemanager.LIFECYCLESTATE_INACTIVE}, + } + + handler := wait.New(waitConfig.Wait()) handler.SetSleepBeforeWait(1 * time.Minute) handler.SetTimeout(45 * time.Minute) return handler } // DeleteProjectWaitHandler will wait for project deletion -func DeleteProjectWaitHandler(ctx context.Context, a resourcemanager.DefaultAPI, containerId string) *wait.AsyncActionHandler[struct{}] { - handler := wait.New(func() (waitFinished bool, response *struct{}, err error) { - _, err = a.GetProject(ctx, containerId).Execute() - 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, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError") - } - if oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusForbidden { - return true, nil, nil - } - return false, nil, err - }) +func DeleteProjectWaitHandler(ctx context.Context, a resourcemanager.DefaultAPI, containerId string) *wait.AsyncActionHandler[resourcemanager.GetProjectResponse] { + waitConfig := wait.WaiterHelper[resourcemanager.GetProjectResponse, resourcemanager.LifecycleState]{ + FetchInstance: a.GetProject(ctx, containerId).Execute, + GetState: func(project *resourcemanager.GetProjectResponse) (resourcemanager.LifecycleState, error) { + if project == nil { + return "", errors.New("empty response") + } + return project.LifecycleState, nil + }, + ErrorState: []resourcemanager.LifecycleState{resourcemanager.LIFECYCLESTATE_INACTIVE}, + DeleteHttpErrorStatusCodes: []int{http.StatusNotFound, http.StatusForbidden}, + } + + handler := wait.New(waitConfig.Wait()) handler.SetTimeout(15 * time.Minute) return handler } diff --git a/services/resourcemanager/v0api/wait/wait_test.go b/services/resourcemanager/v0api/wait/wait_test.go index ad5b8d021..112b6be04 100644 --- a/services/resourcemanager/v0api/wait/wait_test.go +++ b/services/resourcemanager/v0api/wait/wait_test.go @@ -75,7 +75,7 @@ func TestCreateProjectWaitHandler(t *testing.T) { getFails: false, projectState: resourcemanager.LifecycleState("ANOTHER STATE"), wantErr: true, - wantResp: true, + wantResp: false, }, } for _, tt := range tests { @@ -122,6 +122,13 @@ func TestDeleteProjectWaitHandler(t *testing.T) { projectState: resourcemanager.LifecycleState(""), wantErr: false, }, + { + desc: "delete_pending", + getFails: false, + getNotFound: false, + projectState: resourcemanager.LIFECYCLESTATE_DELETING, + wantErr: true, + }, { desc: "get_fails", getFails: true, From a6c8ce229d9eafa766aa4008dd202d32f5d28da8 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Tue, 24 Mar 2026 09:45:50 +0100 Subject: [PATCH 2/2] adjust waithandler tests to cover state transitions --- .../resourcemanager/v0api/wait/wait_test.go | 170 +++++++++++------- 1 file changed, 108 insertions(+), 62 deletions(-) diff --git a/services/resourcemanager/v0api/wait/wait_test.go b/services/resourcemanager/v0api/wait/wait_test.go index 112b6be04..98e50948f 100644 --- a/services/resourcemanager/v0api/wait/wait_test.go +++ b/services/resourcemanager/v0api/wait/wait_test.go @@ -18,23 +18,27 @@ type mockSettings struct { projectState resourcemanager.LifecycleState } -func newAPIMock(settings mockSettings) resourcemanager.DefaultAPI { +func newAPIMock(settings []mockSettings) resourcemanager.DefaultAPI { + count := 0 return &resourcemanager.DefaultAPIServiceMock{ GetProjectExecuteMock: utils.Ptr(func(_ resourcemanager.ApiGetProjectRequest) (*resourcemanager.GetProjectResponse, error) { - if settings.getFails { + setting := settings[count%len(settings)] + count++ + + if setting.getFails { return nil, &oapierror.GenericOpenAPIError{ StatusCode: http.StatusInternalServerError, } } - if settings.getNotFound { + if setting.getNotFound { return nil, &oapierror.GenericOpenAPIError{ StatusCode: http.StatusNotFound, } } return &resourcemanager.GetProjectResponse{ - LifecycleState: settings.projectState, + LifecycleState: setting.projectState, ContainerId: "cid", }, nil }), @@ -43,59 +47,81 @@ func newAPIMock(settings mockSettings) resourcemanager.DefaultAPI { func TestCreateProjectWaitHandler(t *testing.T) { tests := []struct { - desc string - getFails bool - projectState resourcemanager.LifecycleState - wantErr bool - wantResp bool + desc string + mockSettings []mockSettings + wantProjectState resourcemanager.LifecycleState + wantErr bool + wantResp bool }{ { - desc: "create_succeeded", - getFails: false, - projectState: resourcemanager.LIFECYCLESTATE_ACTIVE, - wantErr: false, - wantResp: true, + desc: "create_succeeded", + mockSettings: []mockSettings{ + {projectState: resourcemanager.LIFECYCLESTATE_ACTIVE}, + }, + wantProjectState: resourcemanager.LIFECYCLESTATE_ACTIVE, + wantErr: false, + wantResp: true, }, { - desc: "creating", - getFails: false, - projectState: resourcemanager.LIFECYCLESTATE_CREATING, - wantErr: true, - wantResp: false, + desc: "creating", + mockSettings: []mockSettings{ + { + projectState: resourcemanager.LIFECYCLESTATE_CREATING, + }, + { + projectState: resourcemanager.LIFECYCLESTATE_CREATING, + }, + { + projectState: resourcemanager.LIFECYCLESTATE_ACTIVE, + }, + }, + wantProjectState: resourcemanager.LIFECYCLESTATE_ACTIVE, + wantErr: false, + wantResp: true, }, { - desc: "get_fails", - getFails: true, - projectState: resourcemanager.LifecycleState(""), - wantErr: true, - wantResp: false, + desc: "get_fails", + mockSettings: []mockSettings{ + { + projectState: resourcemanager.LIFECYCLESTATE_CREATING, + }, + { + projectState: resourcemanager.LIFECYCLESTATE_CREATING, + }, + { + getFails: true, + }, + }, + wantErr: true, + wantResp: false, }, { - desc: "unknown_state", - getFails: false, - projectState: resourcemanager.LifecycleState("ANOTHER STATE"), - wantErr: true, - wantResp: false, + desc: "unknown_state", + mockSettings: []mockSettings{ + { + getFails: false, + projectState: resourcemanager.LifecycleState("ANOTHER STATE"), + }, + }, + wantErr: true, + wantResp: false, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - apiClient := newAPIMock(mockSettings{ - getFails: tt.getFails, - projectState: tt.projectState, - }) + apiClient := newAPIMock(tt.mockSettings) var wantRes *resourcemanager.GetProjectResponse if tt.wantResp { wantRes = &resourcemanager.GetProjectResponse{ - LifecycleState: tt.projectState, + LifecycleState: tt.wantProjectState, ContainerId: "cid", } } handler := CreateProjectWaitHandler(context.Background(), apiClient, "cid") - gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(10 * time.Millisecond).WaitWithContext(context.Background()) + gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(0).SetThrottle(1).WaitWithContext(context.Background()) if (err != nil) != tt.wantErr { t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) @@ -110,49 +136,69 @@ func TestCreateProjectWaitHandler(t *testing.T) { func TestDeleteProjectWaitHandler(t *testing.T) { tests := []struct { desc string - getFails bool - getNotFound bool - projectState resourcemanager.LifecycleState + mockSettings []mockSettings wantErr bool }{ { - desc: "delete_succeeded", - getFails: false, - getNotFound: true, - projectState: resourcemanager.LifecycleState(""), - wantErr: false, + desc: "delete_succeeded", + mockSettings: []mockSettings{ + { + projectState: resourcemanager.LifecycleState(""), + getFails: false, + getNotFound: true, + }, + }, + wantErr: false, }, { - desc: "delete_pending", - getFails: false, - getNotFound: false, - projectState: resourcemanager.LIFECYCLESTATE_DELETING, - wantErr: true, + desc: "delete_pending", + mockSettings: []mockSettings{ + { + getFails: false, + getNotFound: false, + projectState: resourcemanager.LIFECYCLESTATE_DELETING, + }, + { + getFails: false, + getNotFound: false, + projectState: resourcemanager.LIFECYCLESTATE_DELETING, + }, + { + getFails: false, + getNotFound: true, + projectState: resourcemanager.LifecycleState(""), + }, + }, + wantErr: false, }, { - desc: "get_fails", - getFails: true, - projectState: "", - wantErr: true, + desc: "get_fails", + mockSettings: []mockSettings{ + { + projectState: resourcemanager.LifecycleState(""), + getFails: true, + }, + }, + wantErr: true, }, { - desc: "timeout", - getFails: false, - projectState: "ANOTHER STATE", - wantErr: true, + desc: "timeout", + mockSettings: []mockSettings{ + { + getFails: false, + projectState: "ANOTHER STATE", + }, + }, + wantErr: true, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - apiClient := newAPIMock(mockSettings{ - getFails: tt.getFails, - getNotFound: tt.getNotFound, - projectState: tt.projectState, - }) + apiClient := newAPIMock(tt.mockSettings) handler := DeleteProjectWaitHandler(context.Background(), apiClient, "cid") - _, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background()) + _, err := handler.SetTimeout(10 * time.Millisecond).SetThrottle(1).WaitWithContext(context.Background()) if (err != nil) != tt.wantErr { t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)