Skip to content

Commit 97dfe71

Browse files
authored
add wait handler model serving (#1527)
* add wait handler model serving * add release notes
1 parent 462bd00 commit 97dfe71

File tree

5 files changed

+316
-2
lines changed

5 files changed

+316
-2
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
- `postgresflex`: [v1.0.1](services/postgresflex/CHANGELOG.md#v101-2025-03-12)
44
- **Bugfix:** `DeleteUserWaitHandler` is now also using the region as parameter.
5+
- `modelserving`: [v0.2.0](services/modelserving/CHANGELOG.md#v020-2025-03-14)
6+
- **New**: STACKIT Model Serving module wait handler added.
57

68
## Release (2025-03-05)
79

services/modelserving/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## v0.2.0 (2025-03-14)
2+
3+
- **New**: STACKIT Model Serving module wait handler added.
4+
15
## v0.1.0 (2025-02-25)
26

37
- **New**: STACKIT Model Serving module can be used to manage the STACKIT Model Serving.

services/modelserving/go.mod

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ module github.com/stackitcloud/stackit-sdk-go/services/modelserving
22

33
go 1.21
44

5-
require github.com/stackitcloud/stackit-sdk-go/core v0.16.0
5+
require (
6+
github.com/google/go-cmp v0.7.0
7+
github.com/stackitcloud/stackit-sdk-go/core v0.16.0
8+
)
69

710
require (
811
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
9-
github.com/google/go-cmp v0.7.0 // indirect
1012
github.com/google/uuid v1.6.0 // indirect
1113
)

services/modelserving/wait/wait.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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/modelserving"
13+
)
14+
15+
const (
16+
activeState = "active"
17+
)
18+
19+
type APIClientInterface interface {
20+
GetTokenExecute(ctx context.Context, region, projectId, tokenId string) (*modelserving.GetTokenResponse, error)
21+
}
22+
23+
func CreateModelServingWaitHandler(ctx context.Context, a APIClientInterface, region, projectId, tokenId string) *wait.AsyncActionHandler[modelserving.GetTokenResponse] {
24+
handler := wait.New(func() (waitFinished bool, response *modelserving.GetTokenResponse, err error) {
25+
getTokenResp, err := a.GetTokenExecute(ctx, region, projectId, tokenId)
26+
if err != nil {
27+
return false, nil, err
28+
}
29+
if getTokenResp.Token.State == nil {
30+
return false, nil, fmt.Errorf(
31+
"token state is missing for token with id %s",
32+
tokenId,
33+
)
34+
}
35+
if *getTokenResp.Token.State == activeState {
36+
return true, getTokenResp, nil
37+
}
38+
39+
return false, nil, nil
40+
})
41+
42+
handler.SetTimeout(10 * time.Minute)
43+
44+
return handler
45+
}
46+
47+
// UpdateModelServingWaitHandler will wait for the model serving auth token to be updated.
48+
// Eventually it will have a different implementation, but for now it's the same as the create handler.
49+
func UpdateModelServingWaitHandler(ctx context.Context, a APIClientInterface, region, projectId, tokenId string) *wait.AsyncActionHandler[modelserving.GetTokenResponse] {
50+
return CreateModelServingWaitHandler(ctx, a, region, projectId, tokenId)
51+
}
52+
53+
func DeleteModelServingWaitHandler(ctx context.Context, a APIClientInterface, region, projectId, tokenId string) *wait.AsyncActionHandler[modelserving.GetTokenResponse] {
54+
handler := wait.New(
55+
func() (waitFinished bool, response *modelserving.GetTokenResponse, err error) {
56+
_, err = a.GetTokenExecute(ctx, region, projectId, tokenId)
57+
if err != nil {
58+
var oapiErr *oapierror.GenericOpenAPIError
59+
if errors.As(err, &oapiErr) {
60+
if oapiErr.StatusCode == http.StatusNotFound {
61+
return true, nil, nil
62+
}
63+
}
64+
65+
return false, nil, err
66+
}
67+
68+
return false, nil, nil
69+
},
70+
)
71+
72+
handler.SetTimeout(10 * time.Minute)
73+
74+
return handler
75+
}
+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package wait
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/google/go-cmp/cmp"
9+
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
10+
"github.com/stackitcloud/stackit-sdk-go/core/utils"
11+
"github.com/stackitcloud/stackit-sdk-go/services/modelserving"
12+
)
13+
14+
type apiClientMocked struct {
15+
getFails bool
16+
resourceState string
17+
statusCode int
18+
}
19+
20+
func (a *apiClientMocked) GetTokenExecute(_ context.Context, _, _, _ string) (*modelserving.GetTokenResponse, error) {
21+
if a.getFails {
22+
return nil, &oapierror.GenericOpenAPIError{
23+
StatusCode: a.statusCode,
24+
}
25+
}
26+
27+
return &modelserving.GetTokenResponse{
28+
Token: &modelserving.Token{
29+
State: utils.Ptr(a.resourceState),
30+
Id: utils.Ptr("tid"),
31+
},
32+
}, nil
33+
}
34+
35+
func TestCreateModelServingWaitHandler(t *testing.T) {
36+
tests := []struct {
37+
desc string
38+
getFails bool
39+
statusCode int
40+
resourceState string
41+
wantErr bool
42+
wantResp bool
43+
}{
44+
{
45+
desc: "create_succeeded",
46+
getFails: false,
47+
statusCode: 200,
48+
resourceState: activeState,
49+
wantErr: false,
50+
wantResp: true,
51+
},
52+
{
53+
desc: "get_fails",
54+
getFails: true,
55+
statusCode: 500,
56+
resourceState: "",
57+
wantErr: true,
58+
wantResp: false,
59+
},
60+
{
61+
desc: "timeout",
62+
getFails: false,
63+
statusCode: 200,
64+
resourceState: "ANOTHER_STATE",
65+
wantErr: true,
66+
wantResp: false,
67+
},
68+
}
69+
for _, tt := range tests {
70+
t.Run(tt.desc, func(t *testing.T) {
71+
apiClient := &apiClientMocked{
72+
getFails: tt.getFails,
73+
statusCode: tt.statusCode,
74+
resourceState: tt.resourceState,
75+
}
76+
77+
var wantRes *modelserving.GetTokenResponse
78+
if tt.wantResp {
79+
wantRes = &modelserving.GetTokenResponse{
80+
Token: &modelserving.Token{
81+
State: utils.Ptr(tt.resourceState),
82+
Id: utils.Ptr("tid"),
83+
},
84+
}
85+
}
86+
87+
handler := CreateModelServingWaitHandler(context.Background(), apiClient, "region", "pid", "tid")
88+
89+
gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
90+
91+
if (err != nil) != tt.wantErr {
92+
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
93+
}
94+
if !cmp.Equal(gotRes, wantRes) {
95+
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
96+
}
97+
})
98+
}
99+
}
100+
101+
func TestUpdateModelServingWaitHandler(t *testing.T) {
102+
tests := []struct {
103+
desc string
104+
getFails bool
105+
statusCode int
106+
resourceState string
107+
wantErr bool
108+
wantResp bool
109+
}{
110+
{
111+
desc: "update_succeeded",
112+
getFails: false,
113+
statusCode: 200,
114+
resourceState: activeState,
115+
wantErr: false,
116+
wantResp: true,
117+
},
118+
{
119+
desc: "get_fails",
120+
getFails: true,
121+
statusCode: 500,
122+
resourceState: "",
123+
wantErr: true,
124+
wantResp: false,
125+
},
126+
{
127+
desc: "timeout",
128+
getFails: false,
129+
statusCode: 200,
130+
resourceState: "ANOTHER_STATE",
131+
wantErr: true,
132+
wantResp: false,
133+
},
134+
}
135+
for _, tt := range tests {
136+
t.Run(tt.desc, func(t *testing.T) {
137+
apiClient := &apiClientMocked{
138+
getFails: tt.getFails,
139+
statusCode: tt.statusCode,
140+
resourceState: tt.resourceState,
141+
}
142+
143+
var wantRes *modelserving.GetTokenResponse
144+
if tt.wantResp {
145+
wantRes = &modelserving.GetTokenResponse{
146+
Token: &modelserving.Token{
147+
State: utils.Ptr(tt.resourceState),
148+
Id: utils.Ptr("tid"),
149+
},
150+
}
151+
}
152+
153+
handler := UpdateModelServingWaitHandler(context.Background(), apiClient, "region", "pid", "tid")
154+
155+
gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
156+
157+
if (err != nil) != tt.wantErr {
158+
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
159+
}
160+
if !cmp.Equal(gotRes, wantRes) {
161+
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
162+
}
163+
})
164+
}
165+
}
166+
167+
func TestDeleteModelServingWaitHandler(t *testing.T) {
168+
tests := []struct {
169+
desc string
170+
getFails bool
171+
statusCode int
172+
resourceState string
173+
wantErr bool
174+
wantResp bool
175+
}{
176+
{
177+
desc: "delete_succeeded",
178+
getFails: true,
179+
statusCode: 404,
180+
resourceState: "",
181+
wantErr: false,
182+
wantResp: false,
183+
},
184+
{
185+
desc: "delete_in_progress",
186+
getFails: false,
187+
statusCode: 200,
188+
resourceState: "DELETING",
189+
wantErr: true, // Should timeout since delete is not complete
190+
wantResp: false,
191+
},
192+
{
193+
desc: "get_fails_with_other_error",
194+
getFails: true,
195+
statusCode: 500,
196+
resourceState: "",
197+
wantErr: true,
198+
wantResp: false,
199+
},
200+
}
201+
for _, tt := range tests {
202+
t.Run(tt.desc, func(t *testing.T) {
203+
apiClient := &apiClientMocked{
204+
getFails: tt.getFails,
205+
statusCode: tt.statusCode,
206+
resourceState: tt.resourceState,
207+
}
208+
209+
var wantRes *modelserving.GetTokenResponse
210+
if tt.wantResp {
211+
wantRes = &modelserving.GetTokenResponse{
212+
Token: &modelserving.Token{
213+
State: utils.Ptr(tt.resourceState),
214+
Id: utils.Ptr("tid"),
215+
},
216+
}
217+
}
218+
219+
handler := DeleteModelServingWaitHandler(context.Background(), apiClient, "region", "pid", "tid")
220+
221+
gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
222+
223+
if (err != nil) != tt.wantErr {
224+
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
225+
}
226+
if !cmp.Equal(gotRes, wantRes) {
227+
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
228+
}
229+
})
230+
}
231+
}

0 commit comments

Comments
 (0)