Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
## Release (2025-XX-YY)
- `alb`: [v0.1.0](services/alb/CHANGELOG.md#v010-2025-03-19)
- `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)
Expand Down
3 changes: 3 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
17 changes: 17 additions & 0 deletions core/oapierror/oapierror.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions services/alb/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
## v0.2.0 (2025-03-20)
- **Enhancement:** Provider waiter for loadbalancer api

## v0.1.0 (2025-03-19)
- **New:** API for application load balancer
2 changes: 0 additions & 2 deletions services/alb/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions services/alb/wait/wait.go
Original file line number Diff line number Diff line change
@@ -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 false, nil, 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) {
_, 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, nil, nil
}
}
return true, nil, err
}
return false, nil, nil
})
handler.SetTimeout(10 * time.Minute)
return handler
}
203 changes: 203 additions & 0 deletions services/alb/wait/wait_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
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 TestDeleteLoadbalancerWaitHandler(t *testing.T) {
tests := []struct {
name string
responses []response
wantErr bool
}{
{
"Delete with '404' succeeded immediately",
[]response{
{nil, oapierror.NewError(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, oapierror.NewError(http.StatusNotFound, "not found")},
},
false,
},
{
"Delete with 'gone' succeeded immediately",
[]response{
{nil, oapierror.NewError(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, oapierror.NewError(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))}, oapierror.NewError(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))}, oapierror.NewError(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)
}
}
})
}
}
Loading