Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add waiters for image operations (IaaS) #1245

Merged
merged 3 commits into from
Dec 16, 2024
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
>
> Use `github.com/stackitcloud/stackit-sdk-go/services/authorization` instead.

- `iaas`: [v0.18](services/iaas/CHANGELOG.md#v0180-2024-12-16)
- **Feature:** Add waiters for async operations: `UploadImageWaitHandler` and `DeleteImageWaitHandler`
- `iaas`: [v0.17.0](services/iaas/CHANGELOG.md#v0170-2024-12-16)
- **Feature:** Add new methods to manage affinity groups: `CreateAffinityGroup`, `DeleteAffinityGroup`, `GetAffinityGroup`, and `ListAffinityGroup`
- **Feature:** Add new methods to manage backups: `CreateBackup`, `DeleteBackup`, `GetBackup`, `ListBackup`, `RestoreBackup`, `ExecuteBackup`,`UpdateBackup`
Expand Down
4 changes: 4 additions & 0 deletions services/iaas/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## v0.18.0 (2024-12-16)

- **Feature:** Add waiters for async operations: `UploadImageWaitHandler` and `DeleteImageWaitHandler`

## v0.17.0 (2024-12-16)

- **Feature:** Add new methods to manage affinity groups: `CreateAffinityGroup`, `DeleteAffinityGroup`, `GetAffinityGroup`, and `ListAffinityGroup`
Expand Down
53 changes: 53 additions & 0 deletions services/iaas/wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const (
ServerDeallocatedStatus = "DEALLOCATED"
ServerRescueStatus = "RESCUE"

ImageAvailableStatus = "AVAILABLE"

RequestCreateAction = "CREATE"
RequestUpdateAction = "UPDATE"
RequestDeleteAction = "DELETE"
Expand All @@ -43,6 +45,7 @@ type APIClientInterface interface {
GetVolumeExecute(ctx context.Context, projectId string, volumeId string) (*iaas.Volume, error)
GetServerExecute(ctx context.Context, projectId string, serverId string) (*iaas.Server, error)
GetAttachedVolumeExecute(ctx context.Context, projectId string, serverId string, volumeId string) (*iaas.VolumeAttachment, error)
GetImageExecute(ctx context.Context, projectId string, imageId string) (*iaas.Image, error)
}

// CreateNetworkAreaWaitHandler will wait for network area creation
Expand Down Expand Up @@ -546,3 +549,53 @@ func RemoveVolumeFromServerWaitHandler(ctx context.Context, a APIClientInterface
handler.SetTimeout(10 * time.Minute)
return handler
}

// UploadImageWaitHandler will wait for the status image to become AVAILABLE, which indicates the upload of the image has been completed successfully
func UploadImageWaitHandler(ctx context.Context, a APIClientInterface, projectId, imageId string) *wait.AsyncActionHandler[iaas.Image] {
handler := wait.New(func() (waitFinished bool, response *iaas.Image, err error) {
image, err := a.GetImageExecute(ctx, projectId, imageId)
if err != nil {
return false, image, err
}
if image.Id == nil || image.Status == nil {
return false, image, fmt.Errorf("upload failed for image with id %s, the response is not valid: the id or the status are missing", imageId)
}
if *image.Id == imageId && *image.Status == ImageAvailableStatus {
return true, image, nil
}
if *image.Id == imageId && *image.Status == ErrorStatus {
return true, image, fmt.Errorf("upload failed for image with id %s", imageId)
}
return false, image, nil
})
handler.SetTimeout(45 * time.Minute)
return handler
}

// DeleteImageWaitHandler will wait for image deletion
func DeleteImageWaitHandler(ctx context.Context, a APIClientInterface, projectId, imageId string) *wait.AsyncActionHandler[iaas.Image] {
handler := wait.New(func() (waitFinished bool, response *iaas.Image, err error) {
image, err := a.GetImageExecute(ctx, projectId, imageId)
if err == nil {
if image != nil {
if image.Id == nil || image.Status == nil {
return false, image, fmt.Errorf("delete failed for image with id %s, the response is not valid: the id or the status are missing", imageId)
}
if *image.Id == imageId && *image.Status == DeleteSuccess {
return true, image, nil
}
}
return false, nil, nil
}
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if !ok {
return false, image, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError: %w", err)
}
if oapiErr.StatusCode != http.StatusNotFound {
return false, image, err
}
return true, nil, nil
})
handler.SetTimeout(15 * time.Minute)
return handler
}
132 changes: 132 additions & 0 deletions services/iaas/wait/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type apiClientMocked struct {
getVolumeFails bool
getServerFails bool
getAttachedVolumeFails bool
getImageFails bool
isAttached bool
requestAction string
returnResizing bool
Expand Down Expand Up @@ -142,6 +143,25 @@ func (a *apiClientMocked) GetAttachedVolumeExecute(_ context.Context, _, _, _ st
}, nil
}

func (a *apiClientMocked) GetImageExecute(_ context.Context, _, _ string) (*iaas.Image, error) {
if a.getImageFails {
return nil, &oapierror.GenericOpenAPIError{
StatusCode: 500,
}
}

if a.isDeleted {
return nil, &oapierror.GenericOpenAPIError{
StatusCode: 404,
}
}

return &iaas.Image{
Id: utils.Ptr("iid"),
Status: &a.resourceState,
}, nil
}

func TestCreateNetworkAreaWaitHandler(t *testing.T) {
tests := []struct {
desc string
Expand Down Expand Up @@ -1372,3 +1392,115 @@ func TestRemoveVolumeFromServerWaitHandler(t *testing.T) {
})
}
}

func TestUploadImageWaitHandler(t *testing.T) {
tests := []struct {
desc string
getFails bool
resourceState string
wantErr bool
wantResp bool
}{
{
desc: "upload_succeeded",
getFails: false,
resourceState: ImageAvailableStatus,
wantErr: false,
wantResp: true,
},
{
desc: "error_status",
getFails: false,
resourceState: ErrorStatus,
wantErr: true,
wantResp: true,
},
{
desc: "get_fails",
getFails: true,
resourceState: "",
wantErr: true,
wantResp: false,
},
{
desc: "timeout",
getFails: false,
resourceState: "ANOTHER Status",
wantErr: true,
wantResp: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
apiClient := &apiClientMocked{
getImageFails: tt.getFails,
resourceState: tt.resourceState,
}

var wantRes *iaas.Image
if tt.wantResp {
wantRes = &iaas.Image{
Id: utils.Ptr("iid"),
Status: utils.Ptr(tt.resourceState),
}
}

handler := UploadImageWaitHandler(context.Background(), apiClient, "pid", "iid")

gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())

if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
if !cmp.Equal(gotRes, wantRes) {
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
}
})
}
}

func TestDeleteImageWaitHandler(t *testing.T) {
tests := []struct {
desc string
getFails bool
isDeleted bool
resourceState string
wantErr bool
}{
{
desc: "delete_succeeded",
getFails: false,
isDeleted: true,
wantErr: false,
},
{
desc: "get_fails",
getFails: true,
resourceState: "",
wantErr: true,
},
{
desc: "timeout",
getFails: false,
resourceState: "ANOTHER Status",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
apiClient := &apiClientMocked{
getImageFails: tt.getFails,
isDeleted: tt.isDeleted,
resourceState: tt.resourceState,
}

handler := DeleteImageWaitHandler(context.Background(), apiClient, "pid", "iid")

_, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())

if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Loading