From 50f79ffa0aef4e18fad3fdd9796e167477fedb62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Schmitz?= <152157960+bahkauv70@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:38:47 +0100 Subject: [PATCH 1/6] feat: implement waiter for alb api --- services/alb/go.mod | 2 - services/alb/wait/wait.go | 68 +++++++++++ services/alb/wait/wait_test.go | 210 +++++++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 services/alb/wait/wait.go create mode 100644 services/alb/wait/wait_test.go diff --git a/services/alb/go.mod b/services/alb/go.mod index 4176ff213..8075789f2 100644 --- a/services/alb/go.mod +++ b/services/alb/go.mod @@ -2,8 +2,6 @@ module github.com/stackitcloud/stackit-sdk-go/services/alb go 1.21 -require github.com/stackitcloud/stackit-sdk-go/core v0.16.0 - require ( github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect diff --git a/services/alb/wait/wait.go b/services/alb/wait/wait.go new file mode 100644 index 000000000..e25619775 --- /dev/null +++ b/services/alb/wait/wait.go @@ -0,0 +1,68 @@ +package wait + +import ( + "context" + "errors" + "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/alb" +) + +const ( + StatusUnspecified = "STATUS_UNSPECIFIED" + StatusPending = "STATUS_PENDING" + StatusReady = "STATUS_READY" + StatusError = "STATUS_ERROR" + StatusTerminating = "STATUS_TERMINATING" +) + +type APIClientLoadbalancerInterface interface { + GetLoadBalancerExecute(ctx context.Context, projectId string, region string, name string) (*alb.LoadBalancer, error) +} + +func CreateOrUpdateLoadbalancerWaitHandler(ctx context.Context, client APIClientLoadbalancerInterface, projectId, region, name string) *wait.AsyncActionHandler[alb.LoadBalancer] { + handler := wait.New(func() (bool, *alb.LoadBalancer, error) { + response, err := client.GetLoadBalancerExecute(ctx, projectId, region, name) + if err != nil { + return false, nil, err + } + if response.HasStatus() { + switch *response.Status { + case StatusPending: + return false, nil, nil + case StatusUnspecified: + return true, response, nil + case StatusError: + return true, response, fmt.Errorf("loadbalancer in error: %s", *response.Status) + default: + return true, response, nil + } + } + + return false, nil, nil + }) + handler.SetTimeout(10 * time.Minute) + return handler +} + +func DeleteLoadbalancerWaitHandler(ctx context.Context, client APIClientLoadbalancerInterface, projectId, region, name string) *wait.AsyncActionHandler[alb.LoadBalancer] { + handler := wait.New(func() (bool, *alb.LoadBalancer, error) { + loadBalancer, err := client.GetLoadBalancerExecute(ctx, projectId, region, name) + if err != nil { + var apiErr *oapierror.GenericOpenAPIError + if errors.As(err, &apiErr) { + if statusCode := apiErr.StatusCode; statusCode == http.StatusNotFound || statusCode == http.StatusGone { + return true, loadBalancer, nil + } + } + return true, loadBalancer, err + } + return false, nil, nil + }) + handler.SetTimeout(10 * time.Minute) + return handler +} diff --git a/services/alb/wait/wait_test.go b/services/alb/wait/wait_test.go new file mode 100644 index 000000000..fbd44eb22 --- /dev/null +++ b/services/alb/wait/wait_test.go @@ -0,0 +1,210 @@ +package wait + +import ( + "context" + "errors" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/alb" +) + +var ( + testProject = uuid.NewString() + testRegion = "eu01" + testName = "testlb" +) + +var _ APIClientLoadbalancerInterface = (*apiClientLoadbalancerMocked)(nil) + +type response struct { + loadbalancer *alb.LoadBalancer + err error +} + +type apiClientLoadbalancerMocked struct { + n int + responses []response +} + +// GetLoadBalancerExecute implements APIClientLoadbalancerInterface. +func (a *apiClientLoadbalancerMocked) GetLoadBalancerExecute(_ context.Context, _, _, _ string) (*alb.LoadBalancer, error) { + resp := a.responses[a.n] + a.n++ + a.n %= len(a.responses) + return resp.loadbalancer, resp.err +} + +func TestCreateOrUpdateLoadbalancerWaitHandler(t *testing.T) { + tests := []struct { + name string + responses []response + want *alb.LoadBalancer + wantErr bool + }{ + { + "create succeeded immediately", + []response{ + {&alb.LoadBalancer{Status: alb.PtrString(StatusReady)}, nil}, + }, + &alb.LoadBalancer{Status: utils.Ptr(StatusReady)}, + false, + }, + { + "create succeeded delayed", + []response{ + {&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: alb.PtrString(StatusReady)}, nil}, + }, + &alb.LoadBalancer{Status: utils.Ptr(StatusReady)}, + false, + }, + { + "create failed delayed", + []response{ + {&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: alb.PtrString(StatusError)}, nil}, + }, + &alb.LoadBalancer{Status: utils.Ptr(StatusError)}, + true, + }, + { + "timeout", + []response{ + {&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil}, + }, + nil, + true, + }, + { + "broken state", + []response{ + {&alb.LoadBalancer{Status: alb.PtrString("bogus")}, nil}, + }, + &alb.LoadBalancer{Status: alb.PtrString("bogus")}, + false, + }, + // no special update tests needed as the states are the same + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + client := &apiClientLoadbalancerMocked{ + responses: tt.responses, + } + handler := CreateOrUpdateLoadbalancerWaitHandler(ctx, client, testProject, testRegion, testName) + got, err := handler.SetTimeout(1 * time.Second). + SetThrottle(250 * time.Millisecond). + WaitWithContext(ctx) + + if (err != nil) != tt.wantErr { + t.Fatalf("unexpected error response. want %v but got %qe ", tt.wantErr, err) + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("differing loadbalancer %s", diff) + } + }) + } +} +func httpStatus(code int, status string) *oapierror.GenericOpenAPIError { + return &oapierror.GenericOpenAPIError{ + StatusCode: code, + ErrorMessage: status, + Model: map[string]any{}, + } +} + +func TestDeleteLoadbalancerWaitHandler(t *testing.T) { + tests := []struct { + name string + responses []response + wantErr bool + }{ + { + "Delete with '404' succeeded immediately", + []response{ + {nil, httpStatus(http.StatusNotFound, "not found")}, + }, + false, + }, + { + "Delete with '404' delayed", + []response{ + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {nil, httpStatus(http.StatusNotFound, "not found")}, + }, + false, + }, + { + "Delete with 'gone' succeeded immediately", + []response{ + {nil, httpStatus(http.StatusGone, "gone")}, + }, + false, + }, + { + "Delete with 'gone' delayed", + []response{ + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {nil, httpStatus(http.StatusGone, "not found")}, + }, + false, + }, + { + "Delete with error delayed", + []response{ + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(string(StatusError))}, httpStatus(http.StatusInternalServerError, "kapow")}, + }, + true, + }, + { + "Cannot delete", + []response{ + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, + {&alb.LoadBalancer{Status: utils.Ptr(string(StatusError))}, httpStatus(http.StatusOK, "ok")}, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + client := &apiClientLoadbalancerMocked{ + responses: tt.responses, + } + handler := DeleteLoadbalancerWaitHandler(ctx, client, testProject, testRegion, testName) + _, err := handler.SetTimeout(1 * time.Second). + SetThrottle(250 * time.Millisecond). + WaitWithContext(ctx) + + if tt.wantErr != (err != nil) { + t.Fatalf("wrong error result. want err: %v got %v", tt.wantErr, err) + } + if tt.wantErr { + var apiErr *oapierror.GenericOpenAPIError + if !errors.As(err, &apiErr) { + t.Fatalf("expected openapi error, got %v", err) + } + } + }) + } +} From 984ea705ddc31849dd51a99ccc316a01937b74ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Schmitz?= <152157960+bahkauv70@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:31:29 +0100 Subject: [PATCH 2/6] feat: updated changelogs --- CHANGELOG.md | 2 +- services/alb/CHANGELOG.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 789968aab..ad150a047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Release (2025-XX-YY) -- `alb`: [v0.1.0](services/alb/CHANGELOG.md#v010-2025-03-19) +- `alb`: [v0.1.1](services/alb/CHANGELOG.md#v011-2025-03-20) - **New:** API for application load balancer - `cdn`: [v0.1.0](services/cdn/CHANGELOG.md#v010-2025-03-19) - **New:** Introduce new API for content delivery diff --git a/services/alb/CHANGELOG.md b/services/alb/CHANGELOG.md index f56e2cbf5..491aded5d 100644 --- a/services/alb/CHANGELOG.md +++ b/services/alb/CHANGELOG.md @@ -1,2 +1,5 @@ +## v0.1.1 (2025-03-20) +- **Minor enhancement:** Provider waiter for loadbalancer api + ## v0.1.0 (2025-03-19) - **New:** API for application load balancer From 275f956e9ffe258ebe847060d5009dfd104503ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Schmitz?= <152157960+bahkauv70@users.noreply.github.com> Date: Wed, 19 Mar 2025 17:43:35 +0100 Subject: [PATCH 3/6] fix: set version to 0.2.0 --- CHANGELOG.md | 2 +- services/alb/CHANGELOG.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad150a047..366ca4182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Release (2025-XX-YY) -- `alb`: [v0.1.1](services/alb/CHANGELOG.md#v011-2025-03-20) +- `alb`: [v0.2.0](services/alb/CHANGELOG.md#v011-2025-03-20) - **New:** API for application load balancer - `cdn`: [v0.1.0](services/cdn/CHANGELOG.md#v010-2025-03-19) - **New:** Introduce new API for content delivery diff --git a/services/alb/CHANGELOG.md b/services/alb/CHANGELOG.md index 491aded5d..d39d9f186 100644 --- a/services/alb/CHANGELOG.md +++ b/services/alb/CHANGELOG.md @@ -1,5 +1,5 @@ -## v0.1.1 (2025-03-20) -- **Minor enhancement:** Provider waiter for loadbalancer api +## v0.2.0 (2025-03-20) +- **Enhancement:** Provider waiter for loadbalancer api ## v0.1.0 (2025-03-19) - **New:** API for application load balancer From e7c5d55eb76110f9f6c4c6b911866dbd2dd88520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Schmitz?= <152157960+bahkauv70@users.noreply.github.com> Date: Wed, 19 Mar 2025 17:58:36 +0100 Subject: [PATCH 4/6] feat: generalized openapi error constructors, which introduces new core version --- CHANGELOG.md | 4 +++- core/CHANGELOG.md | 3 +++ core/oapierror/oapierror.go | 17 +++++++++++++++++ services/alb/wait/wait_test.go | 19 ++++++------------- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 366ca4182..4a620e23a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Release (2025-XX-YY) -- `alb`: [v0.2.0](services/alb/CHANGELOG.md#v011-2025-03-20) +- `core`: [0.17.0](core/CHANGELOG.md#v0170-2025-03-20) + - **New:** Helper functions for generic openapi error codes +- `alb`: [v0.2.0](services/alb/CHANGELOG.md#v020-2025-03-20) - **New:** API for application load balancer - `cdn`: [v0.1.0](services/cdn/CHANGELOG.md#v010-2025-03-19) - **New:** Introduce new API for content delivery diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index cd48cbe6d..882bb5159 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -1,3 +1,6 @@ +## v0.17.0 (2025-03-25) +- **New:** Helper functions for generic openapi error codes + ## v0.16.2 (2025-03-21) - **New:** If a custom http.Client is provided, the http.Transport is respected. This allows customizing the http.Client with custom timeouts or instrumentation. diff --git a/core/oapierror/oapierror.go b/core/oapierror/oapierror.go index c40770f5c..205abc17f 100644 --- a/core/oapierror/oapierror.go +++ b/core/oapierror/oapierror.go @@ -19,6 +19,23 @@ type GenericOpenAPIError struct { Model interface{} } +func NewError(code int, status string) *GenericOpenAPIError { + return &GenericOpenAPIError{ + StatusCode: code, + ErrorMessage: status, + Model: map[string]any{}, + } +} + +func NewErrorWithBody(code int, status string, body []byte, model any) *GenericOpenAPIError { + return &GenericOpenAPIError{ + StatusCode: code, + ErrorMessage: status, + Body: body, + Model: model, + } +} + // Error returns non-empty string if there was an errorMessage. func (e GenericOpenAPIError) Error() string { // Prevent panic in case of negative value diff --git a/services/alb/wait/wait_test.go b/services/alb/wait/wait_test.go index fbd44eb22..ae918aa35 100644 --- a/services/alb/wait/wait_test.go +++ b/services/alb/wait/wait_test.go @@ -116,13 +116,6 @@ func TestCreateOrUpdateLoadbalancerWaitHandler(t *testing.T) { }) } } -func httpStatus(code int, status string) *oapierror.GenericOpenAPIError { - return &oapierror.GenericOpenAPIError{ - StatusCode: code, - ErrorMessage: status, - Model: map[string]any{}, - } -} func TestDeleteLoadbalancerWaitHandler(t *testing.T) { tests := []struct { @@ -133,7 +126,7 @@ func TestDeleteLoadbalancerWaitHandler(t *testing.T) { { "Delete with '404' succeeded immediately", []response{ - {nil, httpStatus(http.StatusNotFound, "not found")}, + {nil, oapierror.NewError(http.StatusNotFound, "not found")}, }, false, }, @@ -143,14 +136,14 @@ func TestDeleteLoadbalancerWaitHandler(t *testing.T) { {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, - {nil, httpStatus(http.StatusNotFound, "not found")}, + {nil, oapierror.NewError(http.StatusNotFound, "not found")}, }, false, }, { "Delete with 'gone' succeeded immediately", []response{ - {nil, httpStatus(http.StatusGone, "gone")}, + {nil, oapierror.NewError(http.StatusGone, "gone")}, }, false, }, @@ -160,7 +153,7 @@ func TestDeleteLoadbalancerWaitHandler(t *testing.T) { {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, - {nil, httpStatus(http.StatusGone, "not found")}, + {nil, oapierror.NewError(http.StatusGone, "not found")}, }, false, }, @@ -170,7 +163,7 @@ func TestDeleteLoadbalancerWaitHandler(t *testing.T) { {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, - {&alb.LoadBalancer{Status: utils.Ptr(string(StatusError))}, httpStatus(http.StatusInternalServerError, "kapow")}, + {&alb.LoadBalancer{Status: utils.Ptr(string(StatusError))}, oapierror.NewError(http.StatusInternalServerError, "kapow")}, }, true, }, @@ -180,7 +173,7 @@ func TestDeleteLoadbalancerWaitHandler(t *testing.T) { {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, {&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil}, - {&alb.LoadBalancer{Status: utils.Ptr(string(StatusError))}, httpStatus(http.StatusOK, "ok")}, + {&alb.LoadBalancer{Status: utils.Ptr(string(StatusError))}, oapierror.NewError(http.StatusOK, "ok")}, }, true, }, From 2b2a5eb7ac74136c22aae833dfa4614579a6dab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Schmitz?= <152157960+bahkauv70@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:18:55 +0100 Subject: [PATCH 5/6] fix: integrated review findings --- services/alb/wait/wait.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/alb/wait/wait.go b/services/alb/wait/wait.go index e25619775..f46162d0d 100644 --- a/services/alb/wait/wait.go +++ b/services/alb/wait/wait.go @@ -35,7 +35,7 @@ func CreateOrUpdateLoadbalancerWaitHandler(ctx context.Context, client APIClient case StatusPending: return false, nil, nil case StatusUnspecified: - return true, response, nil + return false, nil, nil case StatusError: return true, response, fmt.Errorf("loadbalancer in error: %s", *response.Status) default: @@ -51,15 +51,15 @@ func CreateOrUpdateLoadbalancerWaitHandler(ctx context.Context, client APIClient func DeleteLoadbalancerWaitHandler(ctx context.Context, client APIClientLoadbalancerInterface, projectId, region, name string) *wait.AsyncActionHandler[alb.LoadBalancer] { handler := wait.New(func() (bool, *alb.LoadBalancer, error) { - loadBalancer, err := client.GetLoadBalancerExecute(ctx, projectId, region, name) + _, err := client.GetLoadBalancerExecute(ctx, projectId, region, name) if err != nil { var apiErr *oapierror.GenericOpenAPIError if errors.As(err, &apiErr) { if statusCode := apiErr.StatusCode; statusCode == http.StatusNotFound || statusCode == http.StatusGone { - return true, loadBalancer, nil + return true, nil, nil } } - return true, loadBalancer, err + return true, nil, err } return false, nil, nil }) From 6757c1bb193eb47168541c4e357d0c8035804463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Schmitz?= <152157960+bahkauv70@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:31:15 +0100 Subject: [PATCH 6/6] fix: updated changelogs --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a620e23a..35b02fc94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,11 @@ ## Release (2025-XX-YY) -- `core`: [0.17.0](core/CHANGELOG.md#v0170-2025-03-20) +- `core`: [0.17.0](core/CHANGELOG.md#v0170-2025-03-25) - **New:** Helper functions for generic openapi error codes + - **New:** If a custom http.Client is provided, the http.Transport is respected. This allows customizing the http.Client with custom timeouts or instrumentation. - `alb`: [v0.2.0](services/alb/CHANGELOG.md#v020-2025-03-20) - **New:** API for application load balancer - `cdn`: [v0.1.0](services/cdn/CHANGELOG.md#v010-2025-03-19) - **New:** Introduce new API for content delivery -- `core`: [v0.16.2](core/CHANGELOG.md#v0162-2025-03-21) - - **New:** If a custom http.Client is provided, the http.Transport is respected. This allows customizing the http.Client with custom timeouts or instrumentation. - `serverupdate`: [v1.0.0](services/serverupdate/CHANGELOG.md#v100-2025-03-19) - **Breaking Change:** The region is no longer specified within the client configuration. Instead, the region must be passed as a parameter to any region-specific request. - `serverbackup`: [v1.0.0](services/serverbackup/CHANGELOG.md#v100-2025-03-19)