diff --git a/examples/iaasalpha/image/image.go b/examples/iaasalpha/image/image.go new file mode 100644 index 000000000..a9c1065d3 --- /dev/null +++ b/examples/iaasalpha/image/image.go @@ -0,0 +1,97 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "net/http" + "os" + + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait" +) + +func main() { + // Specify the and project ID + projectId := "PROJECT_ID" + imageFilePath := "PATH/TO/IMAGE" // Should be a path to a valid image file, e.g. "./my-image.qcow2" + imageDiskFormat := "DISK_FORMAT" // E.g. "qcow2", "raw", "iso" + + // Create a new API client, that uses default authentication and configuration + iaasalphaClient, err := iaasalpha.NewAPIClient( + config.WithRegion("eu01"), + ) + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Creating API client: %v\n", err) + os.Exit(1) + } + ctx := context.Background() + + // Create an image + createImagePayload := iaasalpha.CreateImagePayload{ + Name: utils.Ptr("my-image"), + DiskFormat: utils.Ptr(imageDiskFormat), + } + imageCreateResp, err := iaasalphaClient.CreateImage(ctx, projectId).CreateImagePayload(createImagePayload).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when calling `CreateImage`: %v\n", err) + os.Exit(1) + } + fmt.Printf("[iaasalpha API] Image %q has been successfully created.\n", *imageCreateResp.Id) + + // Upload the image by making a PUT request to upload URL + fileContents, err := os.ReadFile(imageFilePath) + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when reading file: %v\n", err) + os.Exit(1) + } + + req, err := http.NewRequest(http.MethodPut, *imageCreateResp.UploadUrl, bytes.NewReader(fileContents)) + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when creating request: %v\n", err) + os.Exit(1) + } + req.Header.Set("Content-Type", "application/octet-stream") + + fmt.Printf("[iaasalpha API] Uploading image contents to %q...\n", *imageCreateResp.Id) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when making request: %v\n", err) + os.Exit(1) + } + if resp.StatusCode != http.StatusOK { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when uploading image: %v\n", resp.Status) + _ = resp.Body.Close() + os.Exit(1) + } + _ = resp.Body.Close() + fmt.Printf("[iaasalpha API] Image %q has been uploaded.\n", *imageCreateResp.Id) + + // Wait for image to become available + image, err := wait.UploadImageWaitHandler(ctx, iaasalphaClient, projectId, *imageCreateResp.Id).WaitWithContext(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when waiting for upload: %v\n", err) + os.Exit(1) + } + + fmt.Printf("[iaasalpha API] Image %q is available.\n", *image.Id) + + // Delete the image + err = iaasalphaClient.DeleteImage(ctx, projectId, *imageCreateResp.Id).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when calling `DeleteImage`: %v\n", err) + } + fmt.Printf("[iaasalpha API] Triggered deletion of image with ID %q.\n", *image.Id) + + // Wait for image to be deleted + _, err = wait.DeleteImageWaitHandler(ctx, iaasalphaClient, projectId, *imageCreateResp.Id).WaitWithContext(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when waiting for deletion: %v\n", err) + os.Exit(1) + } + + fmt.Printf("[iaasalpha API] Image %q has been successfully deleted.\n", *imageCreateResp.Id) +} diff --git a/services/iaasalpha/wait/wait.go b/services/iaasalpha/wait/wait.go index 437f8b677..d9cf6d2b3 100644 --- a/services/iaasalpha/wait/wait.go +++ b/services/iaasalpha/wait/wait.go @@ -22,6 +22,8 @@ const ( VirtualIpCreatedStatus = "CREATED" + ImageAvailableStatus = "AVAILABLE" + RequestCreateAction = "CREATE" RequestUpdateAction = "UPDATE" RequestDeleteAction = "DELETE" @@ -40,6 +42,7 @@ type APIClientInterface interface { GetProjectRequestExecute(ctx context.Context, projectId string, requestId string) (*iaasalpha.Request, error) GetAttachedVolumeExecute(ctx context.Context, projectId string, serverId string, volumeId string) (*iaasalpha.VolumeAttachment, error) GetVirtualIPExecute(ctx context.Context, projectId string, networkId string, virtualIpId string) (*iaasalpha.VirtualIp, error) + GetImageExecute(ctx context.Context, projectId string, imageId string) (*iaasalpha.Image, error) } // CreateVolumeWaitHandler will wait for volume creation @@ -346,3 +349,53 @@ func DeleteVirtualIPWaitHandler(ctx context.Context, a APIClientInterface, proje handler.SetTimeout(15 * 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 +} diff --git a/services/iaasalpha/wait/wait_test.go b/services/iaasalpha/wait/wait_test.go index 6a222e511..3bdd1238c 100644 --- a/services/iaasalpha/wait/wait_test.go +++ b/services/iaasalpha/wait/wait_test.go @@ -17,6 +17,7 @@ type apiClientMocked struct { getProjectRequestFails bool getAttachedVolumeFails bool getVirtualIPFails bool + getImageFails bool isDeleted bool isAttached bool resourceState string @@ -122,6 +123,25 @@ func (a *apiClientMocked) GetVirtualIPExecute(_ context.Context, _, _, _ string) }, 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 TestCreateVolumeWaitHandler(t *testing.T) { tests := []struct { desc string @@ -791,3 +811,115 @@ func TestDeleteVirtualIPWaitHandler(t *testing.T) { }) } } + +func TestImageUploadWaitHandler(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) + } + }) + } +}