Skip to content

Commit 793a9c6

Browse files
Add waiters and example for the image operations on iaasalpha (#1096)
* feat: Add waiters and example for the image operations * fix: Lint fixes * Update services/iaasalpha/wait/wait.go Co-authored-by: Vicente Pinto <[email protected]> * Update services/iaasalpha/wait/wait.go Co-authored-by: Vicente Pinto <[email protected]> * Update services/iaasalpha/wait/wait.go Co-authored-by: Vicente Pinto <[email protected]> * Update examples/iaasalpha/image/image.go Co-authored-by: Vicente Pinto <[email protected]> * feat: Improve descriptions and comments --------- Co-authored-by: Vicente Pinto <[email protected]>
1 parent ac5a4ee commit 793a9c6

File tree

3 files changed

+282
-0
lines changed

3 files changed

+282
-0
lines changed

examples/iaasalpha/image/image.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"net/http"
8+
"os"
9+
10+
"github.com/stackitcloud/stackit-sdk-go/core/config"
11+
"github.com/stackitcloud/stackit-sdk-go/core/utils"
12+
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
13+
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait"
14+
)
15+
16+
func main() {
17+
// Specify the and project ID
18+
projectId := "PROJECT_ID"
19+
imageFilePath := "PATH/TO/IMAGE" // Should be a path to a valid image file, e.g. "./my-image.qcow2"
20+
imageDiskFormat := "DISK_FORMAT" // E.g. "qcow2", "raw", "iso"
21+
22+
// Create a new API client, that uses default authentication and configuration
23+
iaasalphaClient, err := iaasalpha.NewAPIClient(
24+
config.WithRegion("eu01"),
25+
)
26+
if err != nil {
27+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Creating API client: %v\n", err)
28+
os.Exit(1)
29+
}
30+
ctx := context.Background()
31+
32+
// Create an image
33+
createImagePayload := iaasalpha.CreateImagePayload{
34+
Name: utils.Ptr("my-image"),
35+
DiskFormat: utils.Ptr(imageDiskFormat),
36+
}
37+
imageCreateResp, err := iaasalphaClient.CreateImage(ctx, projectId).CreateImagePayload(createImagePayload).Execute()
38+
if err != nil {
39+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when calling `CreateImage`: %v\n", err)
40+
os.Exit(1)
41+
}
42+
fmt.Printf("[iaasalpha API] Image %q has been successfully created.\n", *imageCreateResp.Id)
43+
44+
// Upload the image by making a PUT request to upload URL
45+
fileContents, err := os.ReadFile(imageFilePath)
46+
if err != nil {
47+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when reading file: %v\n", err)
48+
os.Exit(1)
49+
}
50+
51+
req, err := http.NewRequest(http.MethodPut, *imageCreateResp.UploadUrl, bytes.NewReader(fileContents))
52+
if err != nil {
53+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when creating request: %v\n", err)
54+
os.Exit(1)
55+
}
56+
req.Header.Set("Content-Type", "application/octet-stream")
57+
58+
fmt.Printf("[iaasalpha API] Uploading image contents to %q...\n", *imageCreateResp.Id)
59+
client := &http.Client{}
60+
resp, err := client.Do(req)
61+
if err != nil {
62+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when making request: %v\n", err)
63+
os.Exit(1)
64+
}
65+
if resp.StatusCode != http.StatusOK {
66+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when uploading image: %v\n", resp.Status)
67+
_ = resp.Body.Close()
68+
os.Exit(1)
69+
}
70+
_ = resp.Body.Close()
71+
fmt.Printf("[iaasalpha API] Image %q has been uploaded.\n", *imageCreateResp.Id)
72+
73+
// Wait for image to become available
74+
image, err := wait.UploadImageWaitHandler(ctx, iaasalphaClient, projectId, *imageCreateResp.Id).WaitWithContext(ctx)
75+
if err != nil {
76+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when waiting for upload: %v\n", err)
77+
os.Exit(1)
78+
}
79+
80+
fmt.Printf("[iaasalpha API] Image %q is available.\n", *image.Id)
81+
82+
// Delete the image
83+
err = iaasalphaClient.DeleteImage(ctx, projectId, *imageCreateResp.Id).Execute()
84+
if err != nil {
85+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when calling `DeleteImage`: %v\n", err)
86+
}
87+
fmt.Printf("[iaasalpha API] Triggered deletion of image with ID %q.\n", *image.Id)
88+
89+
// Wait for image to be deleted
90+
_, err = wait.DeleteImageWaitHandler(ctx, iaasalphaClient, projectId, *imageCreateResp.Id).WaitWithContext(ctx)
91+
if err != nil {
92+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when waiting for deletion: %v\n", err)
93+
os.Exit(1)
94+
}
95+
96+
fmt.Printf("[iaasalpha API] Image %q has been successfully deleted.\n", *imageCreateResp.Id)
97+
}

services/iaasalpha/wait/wait.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const (
2222

2323
VirtualIpCreatedStatus = "CREATED"
2424

25+
ImageAvailableStatus = "AVAILABLE"
26+
2527
RequestCreateAction = "CREATE"
2628
RequestUpdateAction = "UPDATE"
2729
RequestDeleteAction = "DELETE"
@@ -40,6 +42,7 @@ type APIClientInterface interface {
4042
GetProjectRequestExecute(ctx context.Context, projectId string, requestId string) (*iaasalpha.Request, error)
4143
GetAttachedVolumeExecute(ctx context.Context, projectId string, serverId string, volumeId string) (*iaasalpha.VolumeAttachment, error)
4244
GetVirtualIPExecute(ctx context.Context, projectId string, networkId string, virtualIpId string) (*iaasalpha.VirtualIp, error)
45+
GetImageExecute(ctx context.Context, projectId string, imageId string) (*iaasalpha.Image, error)
4346
}
4447

4548
// CreateVolumeWaitHandler will wait for volume creation
@@ -346,3 +349,53 @@ func DeleteVirtualIPWaitHandler(ctx context.Context, a APIClientInterface, proje
346349
handler.SetTimeout(15 * time.Minute)
347350
return handler
348351
}
352+
353+
// UploadImageWaitHandler will wait for the status image to become AVAILABLE, which indicates the upload of the image has been completed successfully
354+
func UploadImageWaitHandler(ctx context.Context, a APIClientInterface, projectId, imageId string) *wait.AsyncActionHandler[iaasalpha.Image] {
355+
handler := wait.New(func() (waitFinished bool, response *iaasalpha.Image, err error) {
356+
image, err := a.GetImageExecute(ctx, projectId, imageId)
357+
if err != nil {
358+
return false, image, err
359+
}
360+
if image.Id == nil || image.Status == nil {
361+
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)
362+
}
363+
if *image.Id == imageId && *image.Status == ImageAvailableStatus {
364+
return true, image, nil
365+
}
366+
if *image.Id == imageId && *image.Status == ErrorStatus {
367+
return true, image, fmt.Errorf("upload failed for image with id %s", imageId)
368+
}
369+
return false, image, nil
370+
})
371+
handler.SetTimeout(45 * time.Minute)
372+
return handler
373+
}
374+
375+
// DeleteImageWaitHandler will wait for image deletion
376+
func DeleteImageWaitHandler(ctx context.Context, a APIClientInterface, projectId, imageId string) *wait.AsyncActionHandler[iaasalpha.Image] {
377+
handler := wait.New(func() (waitFinished bool, response *iaasalpha.Image, err error) {
378+
image, err := a.GetImageExecute(ctx, projectId, imageId)
379+
if err == nil {
380+
if image != nil {
381+
if image.Id == nil || image.Status == nil {
382+
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)
383+
}
384+
if *image.Id == imageId && *image.Status == DeleteSuccess {
385+
return true, image, nil
386+
}
387+
}
388+
return false, nil, nil
389+
}
390+
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
391+
if !ok {
392+
return false, image, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError: %w", err)
393+
}
394+
if oapiErr.StatusCode != http.StatusNotFound {
395+
return false, image, err
396+
}
397+
return true, nil, nil
398+
})
399+
handler.SetTimeout(15 * time.Minute)
400+
return handler
401+
}

services/iaasalpha/wait/wait_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type apiClientMocked struct {
1717
getProjectRequestFails bool
1818
getAttachedVolumeFails bool
1919
getVirtualIPFails bool
20+
getImageFails bool
2021
isDeleted bool
2122
isAttached bool
2223
resourceState string
@@ -122,6 +123,25 @@ func (a *apiClientMocked) GetVirtualIPExecute(_ context.Context, _, _, _ string)
122123
}, nil
123124
}
124125

126+
func (a *apiClientMocked) GetImageExecute(_ context.Context, _, _ string) (*iaasalpha.Image, error) {
127+
if a.getImageFails {
128+
return nil, &oapierror.GenericOpenAPIError{
129+
StatusCode: 500,
130+
}
131+
}
132+
133+
if a.isDeleted {
134+
return nil, &oapierror.GenericOpenAPIError{
135+
StatusCode: 404,
136+
}
137+
}
138+
139+
return &iaasalpha.Image{
140+
Id: utils.Ptr("iid"),
141+
Status: &a.resourceState,
142+
}, nil
143+
}
144+
125145
func TestCreateVolumeWaitHandler(t *testing.T) {
126146
tests := []struct {
127147
desc string
@@ -791,3 +811,115 @@ func TestDeleteVirtualIPWaitHandler(t *testing.T) {
791811
})
792812
}
793813
}
814+
815+
func TestImageUploadWaitHandler(t *testing.T) {
816+
tests := []struct {
817+
desc string
818+
getFails bool
819+
resourceState string
820+
wantErr bool
821+
wantResp bool
822+
}{
823+
{
824+
desc: "upload_succeeded",
825+
getFails: false,
826+
resourceState: ImageAvailableStatus,
827+
wantErr: false,
828+
wantResp: true,
829+
},
830+
{
831+
desc: "error_status",
832+
getFails: false,
833+
resourceState: ErrorStatus,
834+
wantErr: true,
835+
wantResp: true,
836+
},
837+
{
838+
desc: "get_fails",
839+
getFails: true,
840+
resourceState: "",
841+
wantErr: true,
842+
wantResp: false,
843+
},
844+
{
845+
desc: "timeout",
846+
getFails: false,
847+
resourceState: "ANOTHER Status",
848+
wantErr: true,
849+
wantResp: true,
850+
},
851+
}
852+
for _, tt := range tests {
853+
t.Run(tt.desc, func(t *testing.T) {
854+
apiClient := &apiClientMocked{
855+
getImageFails: tt.getFails,
856+
resourceState: tt.resourceState,
857+
}
858+
859+
var wantRes *iaasalpha.Image
860+
if tt.wantResp {
861+
wantRes = &iaasalpha.Image{
862+
Id: utils.Ptr("iid"),
863+
Status: utils.Ptr(tt.resourceState),
864+
}
865+
}
866+
867+
handler := UploadImageWaitHandler(context.Background(), apiClient, "pid", "iid")
868+
869+
gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
870+
871+
if (err != nil) != tt.wantErr {
872+
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
873+
}
874+
if !cmp.Equal(gotRes, wantRes) {
875+
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
876+
}
877+
})
878+
}
879+
}
880+
881+
func TestDeleteImageWaitHandler(t *testing.T) {
882+
tests := []struct {
883+
desc string
884+
getFails bool
885+
isDeleted bool
886+
resourceState string
887+
wantErr bool
888+
}{
889+
{
890+
desc: "delete_succeeded",
891+
getFails: false,
892+
isDeleted: true,
893+
wantErr: false,
894+
},
895+
{
896+
desc: "get_fails",
897+
getFails: true,
898+
resourceState: "",
899+
wantErr: true,
900+
},
901+
{
902+
desc: "timeout",
903+
getFails: false,
904+
resourceState: "ANOTHER Status",
905+
wantErr: true,
906+
},
907+
}
908+
for _, tt := range tests {
909+
t.Run(tt.desc, func(t *testing.T) {
910+
apiClient := &apiClientMocked{
911+
getImageFails: tt.getFails,
912+
isDeleted: tt.isDeleted,
913+
resourceState: tt.resourceState,
914+
}
915+
916+
handler := DeleteImageWaitHandler(context.Background(), apiClient, "pid", "iid")
917+
918+
_, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
919+
920+
if (err != nil) != tt.wantErr {
921+
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
922+
}
923+
})
924+
}
925+
}

0 commit comments

Comments
 (0)