Skip to content
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
97 changes: 97 additions & 0 deletions examples/iaasalpha/image/image.go
Original file line number Diff line number Diff line change
@@ -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)
}
53 changes: 53 additions & 0 deletions services/iaasalpha/wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const (

VirtualIpCreatedStatus = "CREATED"

ImageAvailableStatus = "AVAILABLE"

RequestCreateAction = "CREATE"
RequestUpdateAction = "UPDATE"
RequestDeleteAction = "DELETE"
Expand All @@ -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
Expand Down Expand Up @@ -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
}
132 changes: 132 additions & 0 deletions services/iaasalpha/wait/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type apiClientMocked struct {
getProjectRequestFails bool
getAttachedVolumeFails bool
getVirtualIPFails bool
getImageFails bool
isDeleted bool
isAttached bool
resourceState string
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
})
}
}
Loading