Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
54 changes: 54 additions & 0 deletions services/iaas/wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/core/wait"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
)

const (
Expand All @@ -24,6 +25,8 @@ const (
ServerDeallocatedStatus = "DEALLOCATED"
ServerRescueStatus = "RESCUE"

ImageAvailableStatus = "AVAILABLE"

RequestCreateAction = "CREATE"
RequestUpdateAction = "UPDATE"
RequestDeleteAction = "DELETE"
Expand All @@ -43,6 +46,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) (*iaasalpha.Image, error)
}

// CreateNetworkAreaWaitHandler will wait for network area creation
Expand Down Expand Up @@ -546,3 +550,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[iaasalpha.Image] {
handler := wait.New(func() (waitFinished bool, response *iaasalpha.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[iaasalpha.Image] {
handler := wait.New(func() (waitFinished bool, response *iaasalpha.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
}
133 changes: 133 additions & 0 deletions services/iaas/wait/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
)

type apiClientMocked struct {
Expand All @@ -20,6 +21,7 @@ type apiClientMocked struct {
getVolumeFails bool
getServerFails bool
getAttachedVolumeFails bool
getImageFails bool
isAttached bool
requestAction string
returnResizing bool
Expand Down Expand Up @@ -142,6 +144,25 @@ func (a *apiClientMocked) GetAttachedVolumeExecute(_ context.Context, _, _, _ st
}, nil
}

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

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

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

func TestCreateNetworkAreaWaitHandler(t *testing.T) {
tests := []struct {
desc string
Expand Down Expand Up @@ -1372,3 +1393,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 *iaasalpha.Image
if tt.wantResp {
wantRes = &iaasalpha.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