Skip to content

Commit 1ff737f

Browse files
committed
feat(kms): waiter for keys
1 parent 344b9e0 commit 1ff737f

File tree

3 files changed

+287
-0
lines changed

3 files changed

+287
-0
lines changed

services/kms/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/stackitcloud/stackit-sdk-go/services/kms
33
go 1.21
44

55
require (
6+
github.com/google/go-cmp v0.7.0
67
github.com/google/uuid v1.6.0
78
github.com/stackitcloud/stackit-sdk-go/core v0.17.1
89
)

services/kms/wait/wait.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package wait
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
"time"
8+
9+
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
10+
"github.com/stackitcloud/stackit-sdk-go/core/wait"
11+
"github.com/stackitcloud/stackit-sdk-go/services/kms"
12+
)
13+
14+
const (
15+
StatusKeyActive = "active"
16+
StatusKeyVersionNotReady = "version_not_ready"
17+
StatusKeyDeleted = "deleted"
18+
)
19+
20+
type ApiKmsClient interface {
21+
GetKeyExecute(ctx context.Context, projectId string, regionId string, keyRingId string, keyId string) (*kms.Key, error)
22+
}
23+
24+
func CreateOrUpdateKeyWaitHandler(ctx context.Context, client ApiKmsClient, projectId, region, keyRingId, keyId string) *wait.AsyncActionHandler[kms.Key] {
25+
handler := wait.New(func() (bool, *kms.Key, error) {
26+
response, err := client.GetKeyExecute(ctx, projectId, region, keyRingId, keyId)
27+
if err != nil {
28+
return false, nil, err
29+
}
30+
31+
if response.State != nil {
32+
switch *response.State {
33+
case StatusKeyVersionNotReady:
34+
return false, nil, nil
35+
default:
36+
return true, response, nil
37+
}
38+
}
39+
40+
return false, nil, nil
41+
})
42+
handler.SetTimeout(10 * time.Minute)
43+
return handler
44+
}
45+
46+
func DeleteKeyWaitHandler(ctx context.Context, client ApiKmsClient, projectId, region, keyRingId,keyId string) *wait.AsyncActionHandler[kms.Key] {
47+
handler := wait.New(func() (bool, *kms.Key, error) {
48+
_, err := client.GetKeyExecute(ctx,projectId,region,keyRingId, keyId)
49+
if err != nil {
50+
var apiErr *oapierror.GenericOpenAPIError
51+
if errors.As(err, &apiErr) {
52+
if statusCode := apiErr.StatusCode; statusCode == http.StatusNotFound || statusCode == http.StatusGone {
53+
return true, nil, nil
54+
}
55+
}
56+
return true, nil, err
57+
}
58+
return false, nil, nil
59+
})
60+
handler.SetTimeout(10 * time.Minute)
61+
return handler
62+
}

services/kms/wait/wait_test.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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/kms"
15+
)
16+
17+
var (
18+
testProject = uuid.NewString()
19+
testRegion = "eu01"
20+
testName = "testlb"
21+
testDate = time.Now()
22+
testKeyRingId = uuid.NewString()
23+
testKeyId = uuid.NewString()
24+
)
25+
26+
var _ ApiKmsClient = (*apiKmsMocked)(nil)
27+
28+
type response struct {
29+
key *kms.Key
30+
err error
31+
}
32+
33+
type apiKmsMocked struct {
34+
n int
35+
responses []response
36+
}
37+
38+
// GetKeyExecute implements ApiKmsClient.
39+
func (a *apiKmsMocked) GetKeyExecute(ctx context.Context, projectId string, region string, keyRingId string, keyId string) (*kms.Key, error) {
40+
resp := a.responses[a.n]
41+
a.n++
42+
a.n %= len(a.responses)
43+
return resp.key, resp.err
44+
}
45+
46+
func fixtureKey(state string) *kms.Key {
47+
return &kms.Key{
48+
Algorithm: kms.ALGORITHM_AES_256_GCM.Ptr(),
49+
Backend: kms.BACKEND_SOFTWARE.Ptr(),
50+
CreatedAt: &testDate,
51+
DeletionDate: &testDate,
52+
Description: utils.Ptr("test-description"),
53+
DisplayName: utils.Ptr("test-displayname"),
54+
Id: &testKeyId,
55+
ImportOnly: utils.Ptr(false),
56+
KeyRingId: &testKeyRingId,
57+
Purpose: kms.PURPOSE_SYMMETRIC_ENCRYPT_DECRYPT.Ptr(),
58+
State: &state,
59+
}
60+
}
61+
62+
func TestCreateOrUpdateKeyWaitHandler(t *testing.T) {
63+
tests := []struct {
64+
name string
65+
responses []response
66+
want *kms.Key
67+
wantErr bool
68+
}{
69+
{
70+
"create succeeded immediately",
71+
[]response{
72+
{fixtureKey(StatusKeyActive), nil},
73+
},
74+
fixtureKey(StatusKeyActive),
75+
false,
76+
},
77+
{
78+
"create succeeded delayed",
79+
[]response{
80+
{fixtureKey(StatusKeyVersionNotReady), nil},
81+
{fixtureKey(StatusKeyVersionNotReady), nil},
82+
{fixtureKey(StatusKeyVersionNotReady), nil},
83+
{fixtureKey(StatusKeyActive), nil},
84+
},
85+
fixtureKey(StatusKeyActive),
86+
false,
87+
},
88+
{
89+
"create failed delayed",
90+
[]response{
91+
{fixtureKey(StatusKeyVersionNotReady), nil},
92+
{fixtureKey(StatusKeyVersionNotReady), nil},
93+
{fixtureKey(StatusKeyVersionNotReady), nil},
94+
{fixtureKey(StatusKeyDeleted), nil},
95+
},
96+
fixtureKey(StatusKeyDeleted),
97+
false,
98+
},
99+
{
100+
"timeout",
101+
[]response{
102+
{fixtureKey(StatusKeyVersionNotReady), nil},
103+
},
104+
nil,
105+
true,
106+
},
107+
{
108+
"broken state",
109+
[]response{
110+
{fixtureKey("bogus"), nil},
111+
},
112+
fixtureKey("bogus"),
113+
false,
114+
},
115+
// no special update tests needed as the states are the same
116+
}
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
ctx := context.Background()
120+
client := &apiKmsMocked{
121+
responses: tt.responses,
122+
}
123+
124+
handler := CreateOrUpdateKeyWaitHandler(ctx, client, testProject, testRegion, testKeyRingId, testKeyId)
125+
got, err := handler.SetTimeout(1 * time.Second).
126+
SetThrottle(250 * time.Millisecond).
127+
WaitWithContext(ctx)
128+
129+
if (err != nil) != tt.wantErr {
130+
t.Fatalf("unexpected error response. want %v but got %v ", tt.wantErr, err)
131+
}
132+
133+
if diff := cmp.Diff(tt.want, got); diff != "" {
134+
t.Errorf("differing key %s", diff)
135+
}
136+
})
137+
}
138+
}
139+
140+
func TestDeleteKeyWaitHandler(t *testing.T) {
141+
tests := []struct {
142+
name string
143+
responses []response
144+
wantErr bool
145+
}{
146+
{
147+
"Delete with '404' succeeded immediately",
148+
[]response{
149+
{nil, oapierror.NewError(http.StatusNotFound, "not found")},
150+
},
151+
false,
152+
},
153+
{
154+
"Delete with '404' delayed",
155+
[]response{
156+
{fixtureKey(StatusKeyVersionNotReady), nil},
157+
{fixtureKey(StatusKeyVersionNotReady), nil},
158+
{fixtureKey(StatusKeyVersionNotReady), nil},
159+
{nil, oapierror.NewError(http.StatusNotFound, "not found")},
160+
},
161+
false,
162+
},
163+
{
164+
"Delete with 'gone' succeeded immediately",
165+
[]response{
166+
{nil, oapierror.NewError(http.StatusGone, "gone")},
167+
},
168+
false,
169+
},
170+
{
171+
"Delete with 'gone' delayed",
172+
[]response{
173+
{fixtureKey(StatusKeyVersionNotReady), nil},
174+
{fixtureKey(StatusKeyVersionNotReady), nil},
175+
{fixtureKey(StatusKeyVersionNotReady), nil},
176+
{nil, oapierror.NewError(http.StatusGone, "not found")},
177+
},
178+
false,
179+
},
180+
{
181+
"Delete with error delayed",
182+
[]response{
183+
{fixtureKey(StatusKeyVersionNotReady), nil},
184+
{fixtureKey(StatusKeyVersionNotReady), nil},
185+
186+
{fixtureKey(StatusKeyVersionNotReady), nil},
187+
{fixtureKey(StatusKeyDeleted), oapierror.NewError(http.StatusInternalServerError, "kapow")},
188+
},
189+
true,
190+
},
191+
{
192+
"Cannot delete",
193+
[]response{
194+
{fixtureKey(StatusKeyVersionNotReady), nil},
195+
{fixtureKey(StatusKeyVersionNotReady), nil},
196+
{fixtureKey(StatusKeyVersionNotReady), nil},
197+
{fixtureKey(StatusKeyDeleted), oapierror.NewError(http.StatusOK, "ok")},
198+
},
199+
true,
200+
},
201+
}
202+
for _, tt := range tests {
203+
t.Run(tt.name, func(t *testing.T) {
204+
ctx := context.Background()
205+
client := &apiKmsMocked{
206+
responses: tt.responses,
207+
}
208+
handler := DeleteKeyWaitHandler(ctx, client, testProject, testRegion, testKeyRingId, testKeyId)
209+
_, err := handler.SetTimeout(1 * time.Second).
210+
SetThrottle(250 * time.Millisecond).
211+
WaitWithContext(ctx)
212+
213+
if tt.wantErr != (err != nil) {
214+
t.Fatalf("wrong error result. want err: %v got %v", tt.wantErr, err)
215+
}
216+
if tt.wantErr {
217+
var apiErr *oapierror.GenericOpenAPIError
218+
if !errors.As(err, &apiErr) {
219+
t.Fatalf("expected openapi error, got %v", err)
220+
}
221+
}
222+
})
223+
}
224+
}

0 commit comments

Comments
 (0)