Skip to content

Commit 441d6e7

Browse files
committed
feat: implement waiter for alb api
1 parent 7d9b9a9 commit 441d6e7

File tree

3 files changed

+283
-5
lines changed

3 files changed

+283
-5
lines changed

services/alb/go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ module github.com/stackitcloud/stackit-sdk-go/services/alb
22

33
go 1.21
44

5-
require github.com/stackitcloud/stackit-sdk-go/core v0.16.0
6-
75
require (
8-
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
9-
github.com/google/go-cmp v0.7.0 // indirect
10-
github.com/google/uuid v1.6.0 // indirect
6+
github.com/google/go-cmp v0.7.0
7+
github.com/google/uuid v1.6.0
8+
github.com/stackitcloud/stackit-sdk-go/core v0.16.0
119
)
10+
11+
require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect

services/alb/wait/wait.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package wait
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"time"
9+
10+
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
11+
"github.com/stackitcloud/stackit-sdk-go/core/wait"
12+
"github.com/stackitcloud/stackit-sdk-go/services/alb"
13+
)
14+
15+
const (
16+
StatusUnspecified = "STATUS_UNSPECIFIED"
17+
StatusPending = "STATUS_PENDING"
18+
StatusReady = "STATUS_READY"
19+
StatusError = "STATUS_ERROR"
20+
StatusTerminating = "STATUS_TERMINATING"
21+
)
22+
23+
type APIClientLoadbalancerInterface interface {
24+
GetLoadBalancerExecute(ctx context.Context, projectId string, region string, name string) (*alb.LoadBalancer, error)
25+
}
26+
27+
func CreateOrUpdateLoadbalancerWaitHandler(ctx context.Context, client APIClientLoadbalancerInterface, projectId, region, name string) *wait.AsyncActionHandler[alb.LoadBalancer] {
28+
handler := wait.New(func() (bool, *alb.LoadBalancer, error) {
29+
response, err := client.GetLoadBalancerExecute(ctx, projectId, region, name)
30+
if err != nil {
31+
return false, nil, err
32+
}
33+
if response.HasStatus() {
34+
switch *response.Status {
35+
case StatusPending:
36+
return false, nil, nil
37+
case StatusUnspecified:
38+
return true, response, nil
39+
case StatusError:
40+
return true, response, fmt.Errorf("loadbalancer in error: %s", *response.Status)
41+
default:
42+
return true, response, nil
43+
}
44+
}
45+
46+
return false, nil, nil
47+
})
48+
handler.SetTimeout(10 * time.Minute)
49+
return handler
50+
}
51+
52+
func DeleteLoadbalancerWaitHandler(ctx context.Context, client APIClientLoadbalancerInterface, projectId, region, name string) *wait.AsyncActionHandler[alb.LoadBalancer] {
53+
handler := wait.New(func() (bool, *alb.LoadBalancer, error) {
54+
loadBalancer, err := client.GetLoadBalancerExecute(ctx, projectId, region, name)
55+
if err != nil {
56+
var apiErr *oapierror.GenericOpenAPIError
57+
if errors.As(err, &apiErr) {
58+
if statusCode := apiErr.StatusCode; statusCode == http.StatusNotFound || statusCode == http.StatusGone {
59+
return true, loadBalancer, nil
60+
}
61+
}
62+
return true, loadBalancer, err
63+
}
64+
return false, nil, nil
65+
})
66+
handler.SetTimeout(10 * time.Minute)
67+
return handler
68+
}

services/alb/wait/wait_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package wait
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
"testing"
8+
"time"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/google/uuid"
12+
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
13+
"github.com/stackitcloud/stackit-sdk-go/core/utils"
14+
"github.com/stackitcloud/stackit-sdk-go/services/alb"
15+
)
16+
17+
var (
18+
testProject = uuid.NewString()
19+
testRegion = "eu01"
20+
testName = "testlb"
21+
)
22+
23+
var _ APIClientLoadbalancerInterface = (*apiClientLoadbalancerMocked)(nil)
24+
25+
type response struct {
26+
loadbalancer *alb.LoadBalancer
27+
err error
28+
}
29+
30+
type apiClientLoadbalancerMocked struct {
31+
n int
32+
responses []response
33+
}
34+
35+
// GetLoadBalancerExecute implements APIClientLoadbalancerInterface.
36+
func (a *apiClientLoadbalancerMocked) GetLoadBalancerExecute(_ context.Context, _, _, _ string) (*alb.LoadBalancer, error) {
37+
resp := a.responses[a.n]
38+
a.n++
39+
a.n %= len(a.responses)
40+
return resp.loadbalancer, resp.err
41+
}
42+
43+
func TestCreateOrUpdateLoadbalancerWaitHandler(t *testing.T) {
44+
tests := []struct {
45+
name string
46+
responses []response
47+
want *alb.LoadBalancer
48+
wantErr bool
49+
}{
50+
{
51+
"create succeeded immediately",
52+
[]response{
53+
{&alb.LoadBalancer{Status: alb.PtrString(StatusReady)}, nil},
54+
},
55+
&alb.LoadBalancer{Status: utils.Ptr(StatusReady)},
56+
false,
57+
},
58+
{
59+
"create succeeded delayed",
60+
[]response{
61+
{&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil},
62+
{&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil},
63+
{&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil},
64+
{&alb.LoadBalancer{Status: alb.PtrString(StatusReady)}, nil},
65+
},
66+
&alb.LoadBalancer{Status: utils.Ptr(StatusReady)},
67+
false,
68+
},
69+
{
70+
"create failed delayed",
71+
[]response{
72+
{&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil},
73+
{&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil},
74+
{&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil},
75+
{&alb.LoadBalancer{Status: alb.PtrString(StatusError)}, nil},
76+
},
77+
&alb.LoadBalancer{Status: utils.Ptr(StatusError)},
78+
true,
79+
},
80+
{
81+
"timeout",
82+
[]response{
83+
{&alb.LoadBalancer{Status: alb.PtrString(StatusPending)}, nil},
84+
},
85+
nil,
86+
true,
87+
},
88+
{
89+
"broken state",
90+
[]response{
91+
{&alb.LoadBalancer{Status: alb.PtrString("bogus")}, nil},
92+
},
93+
&alb.LoadBalancer{Status: alb.PtrString("bogus")},
94+
false,
95+
},
96+
// no special update tests needed as the states are the same
97+
}
98+
for _, tt := range tests {
99+
t.Run(tt.name, func(t *testing.T) {
100+
ctx := context.Background()
101+
client := &apiClientLoadbalancerMocked{
102+
responses: tt.responses,
103+
}
104+
handler := CreateOrUpdateLoadbalancerWaitHandler(ctx, client, testProject, testRegion, testName)
105+
got, err := handler.SetTimeout(1 * time.Second).
106+
SetThrottle(250 * time.Millisecond).
107+
WaitWithContext(ctx)
108+
109+
if (err != nil) != tt.wantErr {
110+
t.Fatalf("unexpected error response. want %v but got %qe ", tt.wantErr, err)
111+
}
112+
113+
if diff := cmp.Diff(tt.want, got); diff != "" {
114+
t.Errorf("differing loadbalancer %s", diff)
115+
}
116+
})
117+
}
118+
}
119+
func httpStatus(code int, status string) *oapierror.GenericOpenAPIError {
120+
return &oapierror.GenericOpenAPIError{
121+
StatusCode: code,
122+
ErrorMessage: status,
123+
Model: map[string]any{},
124+
}
125+
}
126+
127+
func TestDeleteLoadbalancerWaitHandler(t *testing.T) {
128+
tests := []struct {
129+
name string
130+
responses []response
131+
wantErr bool
132+
}{
133+
{
134+
"Delete with '404' succeeded immediately",
135+
[]response{
136+
{nil, httpStatus(http.StatusNotFound, "not found")},
137+
},
138+
false,
139+
},
140+
{
141+
"Delete with '404' delayed",
142+
[]response{
143+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
144+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
145+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
146+
{nil, httpStatus(http.StatusNotFound, "not found")},
147+
},
148+
false,
149+
},
150+
{
151+
"Delete with 'gone' succeeded immediately",
152+
[]response{
153+
{nil, httpStatus(http.StatusGone, "gone")},
154+
},
155+
false,
156+
},
157+
{
158+
"Delete with 'gone' delayed",
159+
[]response{
160+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
161+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
162+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
163+
{nil, httpStatus(http.StatusGone, "not found")},
164+
},
165+
false,
166+
},
167+
{
168+
"Delete with error delayed",
169+
[]response{
170+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
171+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
172+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
173+
{&alb.LoadBalancer{Status: utils.Ptr(string(StatusError))}, httpStatus(http.StatusInternalServerError, "kapow")},
174+
},
175+
true,
176+
},
177+
{
178+
"Cannot delete",
179+
[]response{
180+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
181+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
182+
{&alb.LoadBalancer{Status: utils.Ptr(StatusPending)}, nil},
183+
{&alb.LoadBalancer{Status: utils.Ptr(string(StatusError))}, httpStatus(http.StatusOK, "ok")},
184+
},
185+
true,
186+
},
187+
}
188+
for _, tt := range tests {
189+
t.Run(tt.name, func(t *testing.T) {
190+
ctx := context.Background()
191+
client := &apiClientLoadbalancerMocked{
192+
responses: tt.responses,
193+
}
194+
handler := DeleteLoadbalancerWaitHandler(ctx, client, testProject, testRegion, testName)
195+
_, err := handler.SetTimeout(1 * time.Second).
196+
SetThrottle(250 * time.Millisecond).
197+
WaitWithContext(ctx)
198+
199+
if tt.wantErr != (err != nil) {
200+
t.Fatalf("wrong error result. want err: %v got %v", tt.wantErr, err)
201+
}
202+
if tt.wantErr {
203+
var apiErr *oapierror.GenericOpenAPIError
204+
if !errors.As(err, &apiErr) {
205+
t.Fatalf("expected openapi error, got %v", err)
206+
}
207+
}
208+
})
209+
}
210+
}

0 commit comments

Comments
 (0)