Skip to content

Commit 74169c3

Browse files
committed
feat: Add waiters and example for the image operations
1 parent bcb343c commit 74169c3

File tree

3 files changed

+284
-0
lines changed

3 files changed

+284
-0
lines changed

examples/iaasalpha/image/image.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
} else {
42+
fmt.Printf("[iaasalpha API] Image %q has been successfully created.\n", *imageCreateResp.Id)
43+
}
44+
45+
// Upload the image by making a PUT request to upload URL
46+
fileContents, err := os.ReadFile(imageFilePath)
47+
if err != nil {
48+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when reading file: %v\n", err)
49+
os.Exit(1)
50+
}
51+
52+
req, err := http.NewRequest(http.MethodPut, *imageCreateResp.UploadUrl, bytes.NewReader(fileContents))
53+
if err != nil {
54+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when creating request: %v\n", err)
55+
os.Exit(1)
56+
}
57+
req.Header.Set("Content-Type", "application/octet-stream")
58+
59+
fmt.Printf("[iaasalpha API] Uploading image %q...\n", *imageCreateResp.Id)
60+
client := &http.Client{}
61+
resp, err := client.Do(req)
62+
if err != nil {
63+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when making request: %v\n", err)
64+
os.Exit(1)
65+
}
66+
defer resp.Body.Close()
67+
if resp.StatusCode != http.StatusOK {
68+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when uploading image: %v\n", resp.Status)
69+
os.Exit(1)
70+
} else {
71+
fmt.Printf("[iaasalpha API] Image %q has been successfully uploaded.\n", *imageCreateResp.Id)
72+
}
73+
74+
// Wait for image to become available
75+
image, err := wait.ImageUploadWaitHandler(ctx, iaasalphaClient, projectId, *imageCreateResp.Id).WaitWithContext(ctx)
76+
if err != nil {
77+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when waiting for creation: %v\n", err)
78+
os.Exit(1)
79+
}
80+
81+
fmt.Printf("[iaasalpha API] Image %q is available.\n", *image.Id)
82+
83+
// Delete the image
84+
err = iaasalphaClient.DeleteImage(ctx, projectId, *imageCreateResp.Id).Execute()
85+
if err != nil {
86+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when calling `DeleteImage`: %v\n", err)
87+
} else {
88+
fmt.Printf("[iaasalpha API] public IP %q has been successfully deleted.\n", *imageCreateResp.Id)
89+
}
90+
91+
// Wait for image to be deleted
92+
_, err = wait.DeleteImageWaitHandler(ctx, iaasalphaClient, projectId, *imageCreateResp.Id).WaitWithContext(ctx)
93+
if err != nil {
94+
fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when waiting for deletion: %v\n", err)
95+
os.Exit(1)
96+
}
97+
98+
fmt.Printf("[iaasalpha API] Image %q has been successfully deleted.\n", *imageCreateResp.Id)
99+
}

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+
// ImageUploadWaitHandler will wait for server creation
354+
func ImageUploadWaitHandler(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("create 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("create failed for image with id %s", imageId)
368+
}
369+
return false, image, nil
370+
})
371+
handler.SetTimeout(15 * time.Minute)
372+
return handler
373+
}
374+
375+
// DeleteImageWaitHandler will wait for volume 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 := ImageUploadWaitHandler(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)