diff --git a/services/iaasalpha/wait/wait.go b/services/iaasalpha/wait/wait.go index 55449990b..598c0a80c 100644 --- a/services/iaasalpha/wait/wait.go +++ b/services/iaasalpha/wait/wait.go @@ -17,12 +17,24 @@ const ( ErrorStatus = "ERROR" ServerActiveStatus = "ACTIVE" ServerResizingStatus = "RESIZING" + + RequestCreateAction = "CREATE" + RequestUpdateAction = "UPDATE" + RequestDeleteAction = "DELETE" + RequestCreatedStatus = "CREATED" + RequestUpdatedStatus = "UPDATED" + RequestDeletedStatus = "DELETED" + RequestFailedStatus = "FAILED" + + XRequestIDHeader = "X-Request-Id" ) // Interfaces needed for tests type APIClientInterface interface { GetVolumeExecute(ctx context.Context, projectId string, volumeId string) (*iaasalpha.Volume, error) GetServerExecute(ctx context.Context, projectId string, serverId string) (*iaasalpha.Server, error) + GetProjectRequestExecute(ctx context.Context, projectId string, requestId string) (*iaasalpha.Request, error) + GetAttachedVolumeExecute(ctx context.Context, projectId string, serverId string, volumeId string) (*iaasalpha.VolumeAttachment, error) } // CreateVolumeWaitHandler will wait for volume creation @@ -165,3 +177,117 @@ func DeleteServerWaitHandler(ctx context.Context, a APIClientInterface, projectI handler.SetTimeout(20 * time.Minute) return handler } + +// ProjectRequestWaitHandler will wait for a request to succeed. +// +// It receives a request ID that can be obtained from the "X-Request-Id" header in the HTTP response of any operation in the IaaS API. +// To get this response header, use the "runtime.WithCaptureHTTPResponse" method from the "core" packaghe to get the raw HTTP response of an SDK operation. +// Then, the value of the request ID can be obtained by accessing the header key which is defined in the constant "XRequestIDHeader" of this package. +// +// Example usage: +// +// var httpResp *http.Response +// ctxWithHTTPResp := runtime.WithCaptureHTTPResponse(context.Background(), &httpResp) +// +// err = iaasalphaClient.AddPublicIpToServer(ctxWithHTTPResp, projectId, serverId, publicIpId).Execute() +// +// requestId := httpResp.Header[wait.XRequestIDHeader][0] +// _, err = wait.ProjectRequestWaitHandler(context.Background(), iaasalphaClient, projectId, requestId).WaitWithContext(context.Background()) +func ProjectRequestWaitHandler(ctx context.Context, a APIClientInterface, projectId, requestId string) *wait.AsyncActionHandler[iaasalpha.Request] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.Request, err error) { + request, err := a.GetProjectRequestExecute(ctx, projectId, requestId) + if err != nil { + return false, request, err + } + + if request == nil { + return false, nil, fmt.Errorf("request failed for request with id %s: nil response from GetProjectRequestExecute", requestId) + } + + if request.RequestId == nil || request.RequestAction == nil || request.Status == nil { + return false, request, fmt.Errorf("request failed for request with id %s, the response is not valid: the id, the request action or the status are missing", requestId) + } + + if *request.RequestId != requestId { + return false, request, fmt.Errorf("request failed for request with id %s: the response id doesn't match the request id", requestId) + } + + switch *request.RequestAction { + case RequestCreateAction: + if *request.Status == RequestCreatedStatus { + return true, request, nil + } + case RequestUpdateAction: + if *request.Status == RequestUpdatedStatus { + return true, request, nil + } + case RequestDeleteAction: + if *request.Status == RequestDeletedStatus { + return true, request, nil + } + default: + return false, request, fmt.Errorf("request failed for request with id %s, the request action %s is not supported", requestId, *request.RequestAction) + } + + if *request.Status == RequestFailedStatus { + return true, request, fmt.Errorf("request failed for request with id %s", requestId) + } + + return false, request, nil + }) + handler.SetTimeout(20 * time.Minute) + return handler +} + +// AddVolumeToServerWaitHandler will wait for a volume to be attached to a server +func AddVolumeToServerWaitHandler(ctx context.Context, a APIClientInterface, projectId, serverId, volumeId string) *wait.AsyncActionHandler[iaasalpha.VolumeAttachment] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.VolumeAttachment, err error) { + volumeAttachment, err := a.GetAttachedVolumeExecute(ctx, projectId, serverId, volumeId) + if err == nil { + if volumeAttachment != nil { + if volumeAttachment.VolumeId == nil { + return false, volumeAttachment, fmt.Errorf("attachment failed for server with id %s and volume with id %s, the response is not valid: the volume id is missing", serverId, volumeId) + } + if *volumeAttachment.VolumeId == volumeId { + return true, volumeAttachment, 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, volumeAttachment, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError: %w", err) + } + if oapiErr.StatusCode != http.StatusNotFound { + return false, volumeAttachment, err + } + return false, nil, nil + }) + handler.SetTimeout(10 * time.Minute) + return handler +} + +// RemoveVolumeFromServerWaitHandler will wait for a volume to be attached to a server +func RemoveVolumeFromServerWaitHandler(ctx context.Context, a APIClientInterface, projectId, serverId, volumeId string) *wait.AsyncActionHandler[iaasalpha.VolumeAttachment] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.VolumeAttachment, err error) { + volumeAttachment, err := a.GetAttachedVolumeExecute(ctx, projectId, serverId, volumeId) + if err == nil { + if volumeAttachment != nil { + if volumeAttachment.VolumeId == nil { + return false, volumeAttachment, fmt.Errorf("remove volume failed for server with id %s and volume with id %s, the response is not valid: the volume id is missing", serverId, volumeId) + } + } + 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, volumeAttachment, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError: %w", err) + } + if oapiErr.StatusCode != http.StatusNotFound { + return false, volumeAttachment, err + } + return true, nil, nil + }) + handler.SetTimeout(10 * time.Minute) + return handler +} diff --git a/services/iaasalpha/wait/wait_test.go b/services/iaasalpha/wait/wait_test.go index 88e815d59..0331a513e 100644 --- a/services/iaasalpha/wait/wait_test.go +++ b/services/iaasalpha/wait/wait_test.go @@ -12,11 +12,15 @@ import ( ) type apiClientMocked struct { - getVolumeFails bool - getServerFails bool - isDeleted bool - resourceState string - returnResizing bool + getVolumeFails bool + getServerFails bool + getProjectRequestFails bool + getAttachedVolumeFails bool + isDeleted bool + isAttached bool + resourceState string + requestAction string + returnResizing bool } func (a *apiClientMocked) GetVolumeExecute(_ context.Context, _, _ string) (*iaasalpha.Volume, error) { @@ -65,6 +69,39 @@ func (a *apiClientMocked) GetServerExecute(_ context.Context, _, _ string) (*iaa }, nil } +func (a *apiClientMocked) GetProjectRequestExecute(_ context.Context, _, _ string) (*iaasalpha.Request, error) { + if a.getProjectRequestFails { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 500, + } + } + + return &iaasalpha.Request{ + RequestId: utils.Ptr("rid"), + RequestAction: &a.requestAction, + Status: &a.resourceState, + }, nil +} + +func (a *apiClientMocked) GetAttachedVolumeExecute(_ context.Context, _, _, _ string) (*iaasalpha.VolumeAttachment, error) { + if a.getAttachedVolumeFails { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 500, + } + } + + if !a.isAttached { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 404, + } + } + + return &iaasalpha.VolumeAttachment{ + ServerId: utils.Ptr("sid"), + VolumeId: utils.Ptr("vid"), + }, nil +} + func TestCreateVolumeWaitHandler(t *testing.T) { tests := []struct { desc string @@ -396,3 +433,214 @@ func TestResizeServerWaitHandler(t *testing.T) { }) } } + +func TestProjectRequestWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + requestState string + requestAction string + wantErr bool + wantResp bool + }{ + { + desc: "create_succeeded", + getFails: false, + requestAction: RequestCreateAction, + requestState: RequestCreatedStatus, + wantErr: false, + wantResp: true, + }, + { + desc: "update_succeeded", + getFails: false, + requestAction: RequestUpdateAction, + requestState: RequestUpdatedStatus, + wantErr: false, + wantResp: true, + }, + { + desc: "delete_succeeded", + getFails: false, + requestAction: RequestDeleteAction, + requestState: RequestDeletedStatus, + wantErr: false, + wantResp: true, + }, + { + desc: "unsupported_action", + getFails: false, + requestAction: "OTHER_ACTION", + wantErr: true, + wantResp: true, + }, + { + desc: "error_status", + getFails: false, + requestAction: RequestCreateAction, + requestState: ErrorStatus, + wantErr: true, + wantResp: true, + }, + { + desc: "get_fails", + getFails: true, + requestState: "", + wantErr: true, + wantResp: false, + }, + { + desc: "timeout", + getFails: false, + requestAction: RequestCreateAction, + requestState: "ANOTHER Status", + wantErr: true, + wantResp: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getProjectRequestFails: tt.getFails, + requestAction: tt.requestAction, + resourceState: tt.requestState, + } + + var wantRes *iaasalpha.Request + if tt.wantResp { + wantRes = &iaasalpha.Request{ + RequestId: utils.Ptr("rid"), + RequestAction: &tt.requestAction, + Status: &tt.requestState, + } + } + + handler := ProjectRequestWaitHandler(context.Background(), apiClient, "pid", "rid") + + 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 TestAddVolumeToServerWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + isAttached bool + wantErr bool + wantResp bool + }{ + { + desc: "attachment_succeeded", + getFails: false, + isAttached: true, + wantErr: false, + wantResp: true, + }, + { + desc: "get_fails", + getFails: true, + wantErr: true, + wantResp: false, + }, + { + desc: "timeout", + getFails: false, + isAttached: false, + wantErr: true, + wantResp: false, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getAttachedVolumeFails: tt.getFails, + isAttached: tt.isAttached, + } + + var wantRes *iaasalpha.VolumeAttachment + if tt.wantResp { + wantRes = &iaasalpha.VolumeAttachment{ + ServerId: utils.Ptr("sid"), + VolumeId: utils.Ptr("vid"), + } + } + + handler := AddVolumeToServerWaitHandler(context.Background(), apiClient, "pid", "sid", "vid") + + 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 TestRemoveVolumeFromServerWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + isAttached bool + wantErr bool + wantResp bool + }{ + { + desc: "removal_succeeded", + getFails: false, + isAttached: false, + wantErr: false, + wantResp: false, + }, + { + desc: "get_fails", + getFails: true, + wantErr: true, + wantResp: false, + }, + { + desc: "timeout", + getFails: false, + isAttached: true, + wantErr: true, + wantResp: false, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getAttachedVolumeFails: tt.getFails, + isAttached: tt.isAttached, + } + + var wantRes *iaasalpha.VolumeAttachment + if tt.wantResp { + wantRes = &iaasalpha.VolumeAttachment{ + ServerId: utils.Ptr("sid"), + VolumeId: utils.Ptr("vid"), + } + } + + handler := RemoveVolumeFromServerWaitHandler(context.Background(), apiClient, "pid", "sid", "vid") + + 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) + } + }) + } +}