Skip to content

Commit a71d857

Browse files
feat(edge): add waiter for edge service (#4178)
1 parent acb9eca commit a71d857

File tree

5 files changed

+692
-3
lines changed

5 files changed

+692
-3
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## Release (2026-XX-YY)
2-
- `edge`: [v0.1.0](services/edge/CHANGELOG.md#v010)
3-
- **New:** STACKIT Edge Cloud service
2+
- `edge`:
3+
- [v0.2.0](services/edge/CHANGELOG.md#v020)
4+
- **Feature:** Add waiter methods for the API
5+
- [v0.1.0](services/edge/CHANGELOG.md#v010)
6+
- **New:** STACKIT Edge Cloud service
47
- `alb`: [v0.8.0](services/alb/CHANGELOG.md#v080)
58
- **Feature:** Switch from `v2beta` API version to `v2` version.
69
- **Feature:** `MaxCredentials` field added to `GetQuotaResponse`

services/edge/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
## v0.2.0
2+
- **Feature:** Add waiter methods for the API
3+
14
## v0.1.0
25
- **New:** STACKIT Edge Cloud service

services/edge/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.1.0
1+
v0.2.0

services/edge/wait/wait.go

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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/edge"
13+
)
14+
15+
const timeoutMinutes time.Duration = 10
16+
17+
var (
18+
ErrInstanceNotFound = errors.New("instance not found")
19+
ErrInstanceStatusUndefined = errors.New("instance status undefined")
20+
ErrInstanceCreationFailed = errors.New("instance creation failed")
21+
ErrInstanceIsBeingDeleted = errors.New("instance is being deleted")
22+
)
23+
24+
// EdgeCloudApiClient is the interface for Edge Cloud API calls which require a waiter.
25+
type EdgeCloudApiClient interface {
26+
GetInstanceExecute(ctx context.Context, projectId, regionId, instanceId string) (*edge.Instance, error)
27+
GetInstanceByNameExecute(ctx context.Context, projectId, regionId, displayName string) (*edge.Instance, error)
28+
GetTokenByInstanceId(ctx context.Context, projectId string, regionId string, instanceId string) edge.ApiGetTokenByInstanceIdRequest
29+
GetTokenByInstanceName(ctx context.Context, projectId string, regionId string, displayName string) edge.ApiGetTokenByInstanceNameRequest
30+
GetKubeconfigByInstanceId(ctx context.Context, projectId string, regionId string, instanceId string) edge.ApiGetKubeconfigByInstanceIdRequest
31+
GetKubeconfigByInstanceName(ctx context.Context, projectId string, regionId string, displayName string) edge.ApiGetKubeconfigByInstanceNameRequest
32+
}
33+
34+
// createOrUpdateInstanceWaitHandler contains the shared logic for waiting on instance creation or updates.
35+
func createOrUpdateInstanceWaitHandler(ctx context.Context, getInstance func(ctx context.Context) (*edge.Instance, error)) *wait.AsyncActionHandler[edge.Instance] {
36+
handler := wait.New(func() (waitFinished bool, response *edge.Instance, err error) {
37+
instance, err := getInstance(ctx)
38+
if err != nil {
39+
return false, nil, err
40+
}
41+
42+
if instance == nil || instance.Status == nil {
43+
return false, nil, ErrInstanceNotFound
44+
}
45+
if instance == nil || instance.Status == nil {
46+
return false, nil, ErrInstanceStatusUndefined
47+
}
48+
49+
status := *instance.Status
50+
switch status {
51+
case edge.INSTANCESTATUS_ACTIVE:
52+
return true, instance, nil
53+
case edge.INSTANCESTATUS_ERROR:
54+
return true, instance, ErrInstanceCreationFailed
55+
case edge.INSTANCESTATUS_RECONCILING:
56+
return false, nil, nil
57+
case edge.INSTANCESTATUS_DELETING:
58+
return true, instance, ErrInstanceIsBeingDeleted
59+
default:
60+
return false, nil, nil
61+
}
62+
})
63+
handler.SetTimeout(timeoutMinutes * time.Minute)
64+
return handler
65+
}
66+
67+
// CreateOrUpdateInstanceWaitHandler waits for instance creation or update by ID to complete.
68+
func CreateOrUpdateInstanceWaitHandler(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, instanceId string) *wait.AsyncActionHandler[edge.Instance] {
69+
return createOrUpdateInstanceWaitHandler(ctx, func(ctx context.Context) (*edge.Instance, error) {
70+
return a.GetInstanceExecute(ctx, projectId, regionId, instanceId)
71+
})
72+
}
73+
74+
// CreateOrUpdateInstanceByNameWaitHandler waits for instance creation or update by name to complete.
75+
func CreateOrUpdateInstanceByNameWaitHandler(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, displayName string) *wait.AsyncActionHandler[edge.Instance] {
76+
return createOrUpdateInstanceWaitHandler(ctx, func(ctx context.Context) (*edge.Instance, error) {
77+
return a.GetInstanceByNameExecute(ctx, projectId, regionId, displayName)
78+
})
79+
}
80+
81+
// deleteInstanceWaitHandler contains the shared logic for waiting on instance deletion.
82+
func deleteInstanceWaitHandler(ctx context.Context, getInstance func(ctx context.Context) (*edge.Instance, error)) *wait.AsyncActionHandler[edge.Instance] {
83+
handler := wait.New(func() (waitFinished bool, response *edge.Instance, err error) {
84+
_, err = getInstance(ctx)
85+
if err == nil {
86+
return false, nil, nil
87+
}
88+
var oapiErr *oapierror.GenericOpenAPIError
89+
if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound {
90+
return true, nil, nil
91+
}
92+
return false, nil, err
93+
})
94+
handler.SetTimeout(timeoutMinutes * time.Minute)
95+
return handler
96+
}
97+
98+
// DeleteInstanceWaitHandler waits for instance deletion by ID.
99+
func DeleteInstanceWaitHandler(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, instanceId string) *wait.AsyncActionHandler[edge.Instance] {
100+
return deleteInstanceWaitHandler(ctx, func(ctx context.Context) (*edge.Instance, error) {
101+
return a.GetInstanceExecute(ctx, projectId, regionId, instanceId)
102+
})
103+
}
104+
105+
// DeleteInstanceByNameWaitHandler waits for instance deletion by name.
106+
func DeleteInstanceByNameWaitHandler(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, displayName string) *wait.AsyncActionHandler[edge.Instance] {
107+
return deleteInstanceWaitHandler(ctx, func(ctx context.Context) (*edge.Instance, error) {
108+
return a.GetInstanceByNameExecute(ctx, projectId, regionId, displayName)
109+
})
110+
}
111+
112+
// kubeconfigWaitHandlerHelper contains the shared logic for waiting for the instance to become ready before retrieving the kubeconfig.
113+
func kubeconfigWaitHandlerHelper(ctx context.Context, checkInstance func(ctx context.Context) error, getKubeconfig func(ctx context.Context) (*edge.Kubeconfig, error)) *wait.AsyncActionHandler[edge.Kubeconfig] {
114+
handler := wait.New(func() (waitFinished bool, response *edge.Kubeconfig, err error) {
115+
err = checkInstance(ctx)
116+
if err != nil {
117+
return false, nil, err
118+
}
119+
kubeconfig, err := getKubeconfig(ctx)
120+
var oapiErr *oapierror.GenericOpenAPIError
121+
if err != nil {
122+
if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound {
123+
return false, nil, nil
124+
}
125+
return false, nil, err
126+
}
127+
return true, kubeconfig, nil
128+
})
129+
handler.SetTimeout(timeoutMinutes * time.Minute)
130+
return handler
131+
}
132+
133+
// KubeconfigWaitHandler waits the instance to become ready before retrieving the kubeconfig by instance ID.
134+
func KubeconfigWaitHandler(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, instanceId string, expirationSeconds *int64) *wait.AsyncActionHandler[edge.Kubeconfig] {
135+
return kubeconfigWaitHandlerHelper(ctx,
136+
func(ctx context.Context) error {
137+
return checkInstanceExistsWithUsableStatus(ctx, a, projectId, regionId, instanceId)
138+
},
139+
func(ctx context.Context) (*edge.Kubeconfig, error) {
140+
req := a.GetKubeconfigByInstanceId(ctx, projectId, regionId, instanceId)
141+
if expirationSeconds != nil {
142+
req = req.ExpirationSeconds(*expirationSeconds)
143+
}
144+
return req.Execute()
145+
},
146+
)
147+
}
148+
149+
// KubeconfigByInstanceNameWaitHandler waits the instance to become ready before retrieving the kubeconfig by instance displayName.
150+
func KubeconfigByInstanceNameWaitHandler(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, displayName string, expirationSeconds *int64) *wait.AsyncActionHandler[edge.Kubeconfig] {
151+
return kubeconfigWaitHandlerHelper(ctx,
152+
func(ctx context.Context) error {
153+
return checkInstanceNameExistsWithUsableStatus(ctx, a, projectId, regionId, displayName)
154+
},
155+
func(ctx context.Context) (*edge.Kubeconfig, error) {
156+
req := a.GetKubeconfigByInstanceName(ctx, projectId, regionId, displayName)
157+
if expirationSeconds != nil {
158+
req = req.ExpirationSeconds(*expirationSeconds)
159+
}
160+
return req.Execute()
161+
},
162+
)
163+
}
164+
165+
// tokenWaitHandlerHelper contains the shared logic for waiting for the instance to become ready before retrieving the service token.
166+
func tokenWaitHandlerHelper(ctx context.Context, checkInstance func(ctx context.Context) error, getToken func(ctx context.Context) (*edge.Token, error)) *wait.AsyncActionHandler[edge.Token] {
167+
handler := wait.New(func() (waitFinished bool, response *edge.Token, err error) {
168+
err = checkInstance(ctx)
169+
if err != nil {
170+
return false, nil, err
171+
}
172+
token, err := getToken(ctx)
173+
var oapiErr *oapierror.GenericOpenAPIError
174+
if err != nil {
175+
if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound {
176+
return false, nil, nil
177+
}
178+
return false, nil, err
179+
}
180+
return true, token, nil
181+
})
182+
handler.SetTimeout(timeoutMinutes * time.Minute)
183+
return handler
184+
}
185+
186+
// TokenWaitHandler waits for the instance to become ready before retrieving the service token by instance ID.
187+
func TokenWaitHandler(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, instanceId string, expirationSeconds *int64) *wait.AsyncActionHandler[edge.Token] {
188+
return tokenWaitHandlerHelper(ctx,
189+
func(ctx context.Context) error {
190+
return checkInstanceExistsWithUsableStatus(ctx, a, projectId, regionId, instanceId)
191+
},
192+
func(ctx context.Context) (*edge.Token, error) {
193+
req := a.GetTokenByInstanceId(ctx, projectId, regionId, instanceId)
194+
if expirationSeconds != nil {
195+
req = req.ExpirationSeconds(*expirationSeconds)
196+
}
197+
return req.Execute()
198+
},
199+
)
200+
}
201+
202+
// TokenByInstanceNameWaitHandler waits for the instance to become ready before retrieving the service token by instance displayName.
203+
func TokenByInstanceNameWaitHandler(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, displayName string, expirationSeconds *int64) *wait.AsyncActionHandler[edge.Token] {
204+
return tokenWaitHandlerHelper(ctx,
205+
func(ctx context.Context) error {
206+
return checkInstanceNameExistsWithUsableStatus(ctx, a, projectId, regionId, displayName)
207+
},
208+
func(ctx context.Context) (*edge.Token, error) {
209+
req := a.GetTokenByInstanceName(ctx, projectId, regionId, displayName)
210+
if expirationSeconds != nil {
211+
req = req.ExpirationSeconds(*expirationSeconds)
212+
}
213+
return req.Execute()
214+
},
215+
)
216+
}
217+
218+
// checkInstanceUsableStatus contains the shared logic for checking instance status.
219+
func checkInstanceUsableStatus(ctx context.Context, getInstance func(ctx context.Context) (*edge.Instance, error), identifierType, identifierValue string) error {
220+
instance, err := getInstance(ctx)
221+
if err != nil {
222+
return err
223+
}
224+
if instance == nil {
225+
return ErrInstanceNotFound
226+
}
227+
if instance.Status == nil {
228+
return ErrInstanceStatusUndefined
229+
}
230+
if *instance.Status == edge.INSTANCESTATUS_ACTIVE || *instance.Status == edge.INSTANCESTATUS_RECONCILING {
231+
return nil
232+
}
233+
return fmt.Errorf("cannot use instance with %s '%s' with status '%s'", identifierType, identifierValue, *instance.Status)
234+
}
235+
236+
// checkInstanceExistsWithUsableStatus checks if the instance with the given instanceId exists and has a usable status.
237+
func checkInstanceExistsWithUsableStatus(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, instanceId string) error {
238+
return checkInstanceUsableStatus(
239+
ctx,
240+
func(ctx context.Context) (*edge.Instance, error) {
241+
return a.GetInstanceExecute(ctx, projectId, regionId, instanceId)
242+
},
243+
"ID",
244+
instanceId,
245+
)
246+
}
247+
248+
// checkInstanceNameExistsWithUsableStatus checks if the instance with the given displayName exists and has a usable status.
249+
func checkInstanceNameExistsWithUsableStatus(ctx context.Context, a EdgeCloudApiClient, projectId, regionId, displayName string) error {
250+
return checkInstanceUsableStatus(
251+
ctx,
252+
func(ctx context.Context) (*edge.Instance, error) {
253+
return a.GetInstanceByNameExecute(ctx, projectId, regionId, displayName)
254+
},
255+
"name",
256+
displayName,
257+
)
258+
}

0 commit comments

Comments
 (0)