Skip to content

Commit

Permalink
Ecloud instance images (#124)
Browse files Browse the repository at this point in the history
* add response handling helpers

* add CreateInstanceImage

* add image update/delete/create

* fix signature
  • Loading branch information
0x4c6565 committed Sep 23, 2022
1 parent 2f828b1 commit c5c13ee
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 8 deletions.
35 changes: 35 additions & 0 deletions pkg/connection/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package connection

func Get[T any](conn Connection, resource string, parameters APIRequestParameters, handlers ...ResponseHandler) (*APIResponseBodyData[T], error) {
response, err := conn.Get(resource, parameters)
return handleResponse[T](response, err, handlers)
}

func Post[T any](conn Connection, resource string, body interface{}, handlers ...ResponseHandler) (*APIResponseBodyData[T], error) {
response, err := conn.Post(resource, body)
return handleResponse[T](response, err, handlers)
}

func Put[T any](conn Connection, resource string, body interface{}, handlers ...ResponseHandler) (*APIResponseBodyData[T], error) {
response, err := conn.Put(resource, body)
return handleResponse[T](response, err, handlers)
}

func Patch[T any](conn Connection, resource string, body interface{}, handlers ...ResponseHandler) (*APIResponseBodyData[T], error) {
response, err := conn.Patch(resource, body)
return handleResponse[T](response, err, handlers)
}

func Delete[T any](conn Connection, resource string, body interface{}, handlers ...ResponseHandler) (*APIResponseBodyData[T], error) {
response, err := conn.Delete(resource, body)
return handleResponse[T](response, err, handlers)
}

func handleResponse[T any](response *APIResponse, err error, handlers []ResponseHandler) (*APIResponseBodyData[T], error) {
responseBody := &APIResponseBodyData[T]{}
if err != nil {
return responseBody, err
}

return responseBody, response.HandleResponse(responseBody, handlers...)
}
14 changes: 14 additions & 0 deletions pkg/connection/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ func (r *APIResponse) ValidateStatusCode(codes []int, respBody ResponseBody) err

type ResponseHandler func(resp *APIResponse) error

func StatusCodeResponseHandler(code int, err error) ResponseHandler {
return func(resp *APIResponse) error {
if resp.StatusCode == code {
return err
}

return nil
}
}

func NotFoundResponseHandler(err error) ResponseHandler {
return StatusCodeResponseHandler(404, err)
}

// HandleResponse deserializes the response body into provided respBody, and validates
// the response using the optionally provided ResponseHandler handler
func (r *APIResponse) HandleResponse(respBody ResponseBody, handlers ...ResponseHandler) error {
Expand Down
20 changes: 12 additions & 8 deletions pkg/service/ecloud/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -681,14 +681,18 @@ type BillingMetric struct {

// Image represents an eCloud image
type Image struct {
ID string `json:"id"`
Name string `json:"name"`
LogoURI string `json:"logo_uri"`
Description string `json:"description"`
DocumentationURI string `json:"documentation_uri"`
Publisher string `json:"publisher"`
CreatedAt connection.DateTime `json:"created_at"`
UpdatedAt connection.DateTime `json:"updated_at"`
ID string `json:"id"`
Name string `json:"name"`
LogoURI string `json:"logo_uri"`
Description string `json:"description"`
DocumentationURI string `json:"documentation_uri"`
Platform string `json:"platform"`
Visibility string `json:"visibility"`
VPCID string `json:"vpc_id"`
AvailabilityZoneID string `json:"availability_zone_id"`
Sync ResourceSync `json:"sync"`
CreatedAt connection.DateTime `json:"created_at"`
UpdatedAt connection.DateTime `json:"updated_at"`
}

// ImageParameter represents an eCloud image parameter
Expand Down
13 changes: 13 additions & 0 deletions pkg/service/ecloud/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -602,3 +602,16 @@ type CreateAffinityRuleMemberRequest struct {
type UpdateVPNSessionPreSharedKeyRequest struct {
PSK string `json:"psk"`
}

// CreateInstanceImageRequest represents a request to create an instance image
type CreateInstanceImageRequest struct {
Name string `json:"name,omitempty"`
}

// UpdateImageRequest represents a request to update an image
type UpdateImageRequest struct {
Name string `json:"name,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
DocumentationURI string `json:"documentation_uri,omitempty"`
Description string `json:"description,omitempty"`
}
3 changes: 3 additions & 0 deletions pkg/service/ecloud/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ type ECloudService interface {
DetachInstanceVolume(instanceID string, req AttachDetachInstanceVolumeRequest) (string, error)
GetInstanceFloatingIPs(instanceID string, parameters connection.APIRequestParameters) ([]FloatingIP, error)
GetInstanceFloatingIPsPaginated(instanceID string, parameters connection.APIRequestParameters) (*connection.Paginated[FloatingIP], error)
CreateInstanceImage(instanceID string, req CreateInstanceImageRequest) (TaskReference, error)

// Floating IP
GetFloatingIPs(parameters connection.APIRequestParameters) ([]FloatingIP, error)
Expand Down Expand Up @@ -277,6 +278,8 @@ type ECloudService interface {
GetImages(parameters connection.APIRequestParameters) ([]Image, error)
GetImagesPaginated(parameters connection.APIRequestParameters) (*connection.Paginated[Image], error)
GetImage(imageID string) (Image, error)
UpdateImage(imageID string, req UpdateImageRequest) (TaskReference, error)
DeleteImage(imageID string) (string, error)
GetImageParameters(imageID string, parameters connection.APIRequestParameters) ([]ImageParameter, error)
GetImageParametersPaginated(imageID string, parameters connection.APIRequestParameters) (*connection.Paginated[ImageParameter], error)
GetImageMetadata(imageID string, parameters connection.APIRequestParameters) ([]ImageMetadata, error)
Expand Down
30 changes: 30 additions & 0 deletions pkg/service/ecloud/service_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,36 @@ func (s *Service) getImageResponseBody(imageID string) (*connection.APIResponseB
})
}

// UpdateImage removes a single Image by ID
func (s *Service) UpdateImage(imageID string, req UpdateImageRequest) (TaskReference, error) {
body, err := s.updateImageResponseBody(imageID, req)

return body.Data, err
}

func (s *Service) updateImageResponseBody(imageID string, req UpdateImageRequest) (*connection.APIResponseBodyData[TaskReference], error) {
if imageID == "" {
return &connection.APIResponseBodyData[TaskReference]{}, fmt.Errorf("invalid image id")
}

return connection.Patch[TaskReference](s.connection, fmt.Sprintf("/ecloud/v2/images/%s", imageID), &req, connection.NotFoundResponseHandler(&ImageNotFoundError{ID: imageID}))
}

// DeleteImage removes a single Image by ID
func (s *Service) DeleteImage(imageID string) (string, error) {
body, err := s.deleteImageResponseBody(imageID)

return body.Data.TaskID, err
}

func (s *Service) deleteImageResponseBody(imageID string) (*connection.APIResponseBodyData[TaskReference], error) {
if imageID == "" {
return &connection.APIResponseBodyData[TaskReference]{}, fmt.Errorf("invalid image id")
}

return connection.Delete[TaskReference](s.connection, fmt.Sprintf("/ecloud/v2/images/%s", imageID), nil, connection.NotFoundResponseHandler(&ImageNotFoundError{ID: imageID}))
}

// GetImageParameters retrieves a list of parameters
func (s *Service) GetImageParameters(imageID string, parameters connection.APIRequestParameters) ([]ImageParameter, error) {
return connection.InvokeRequestAll(func(p connection.APIRequestParameters) (*connection.Paginated[ImageParameter], error) {
Expand Down
122 changes: 122 additions & 0 deletions pkg/service/ecloud/service_image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,128 @@ func TestGetImage(t *testing.T) {
})
}

func TestUpdateImage(t *testing.T) {
t.Run("Valid_NoError", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

c := mocks.NewMockConnection(mockCtrl)

s := Service{
connection: c,
}

req := UpdateImageRequest{
Name: "testimage",
}

c.EXPECT().Patch("/ecloud/v2/images/img-abcdef12", gomock.Eq(&req)).Return(&connection.APIResponse{
Response: &http.Response{
Body: ioutil.NopCloser(bytes.NewReader([]byte("{\"data\":{\"task_id\":\"task-abcdef12\"},\"meta\":{\"location\":\"\"}}"))),
StatusCode: 202,
},
}, nil)

task, err := s.UpdateImage("img-abcdef12", req)

assert.Nil(t, err)
assert.Equal(t, "task-abcdef12", task.TaskID)
})

t.Run("ConnectionError_ReturnsError", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

c := mocks.NewMockConnection(mockCtrl)

s := Service{
connection: c,
}

c.EXPECT().Patch("/ecloud/v2/images/img-abcdef12", gomock.Any()).Return(&connection.APIResponse{}, errors.New("test error 1")).Times(1)

_, err := s.UpdateImage("img-abcdef12", UpdateImageRequest{})

assert.NotNil(t, err)
assert.Equal(t, "test error 1", err.Error())
})

t.Run("InvalidInstanceID_ReturnsError", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

c := mocks.NewMockConnection(mockCtrl)

s := Service{
connection: c,
}

_, err := s.UpdateImage("", UpdateImageRequest{})

assert.NotNil(t, err)
assert.Equal(t, "invalid image id", err.Error())
})
}

func TestDeleteImage(t *testing.T) {
t.Run("Valid_NoError", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

c := mocks.NewMockConnection(mockCtrl)

s := Service{
connection: c,
}

c.EXPECT().Delete("/ecloud/v2/images/img-abcdef12", nil).Return(&connection.APIResponse{
Response: &http.Response{
Body: ioutil.NopCloser(bytes.NewReader([]byte("{\"data\":{\"task_id\":\"task-abcdef12\"},\"meta\":{\"location\":\"\"}}"))),
StatusCode: 202,
},
}, nil)

taskID, err := s.DeleteImage("img-abcdef12")

assert.Nil(t, err)
assert.Equal(t, "task-abcdef12", taskID)
})

t.Run("ConnectionError_ReturnsError", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

c := mocks.NewMockConnection(mockCtrl)

s := Service{
connection: c,
}

c.EXPECT().Delete("/ecloud/v2/images/img-abcdef12", gomock.Any()).Return(&connection.APIResponse{}, errors.New("test error 1")).Times(1)

_, err := s.DeleteImage("img-abcdef12")

assert.NotNil(t, err)
assert.Equal(t, "test error 1", err.Error())
})

t.Run("InvalidInstanceID_ReturnsError", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

c := mocks.NewMockConnection(mockCtrl)

s := Service{
connection: c,
}

_, err := s.DeleteImage("")

assert.NotNil(t, err)
assert.Equal(t, "invalid image id", err.Error())
})
}

func TestGetImageParameters(t *testing.T) {
t.Run("Single", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
Expand Down
15 changes: 15 additions & 0 deletions pkg/service/ecloud/service_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,18 @@ func (s *Service) getInstanceFloatingIPsPaginatedResponseBody(instanceID string,
return nil
})
}

// CreateInstanceImage attaches a volume to an instance
func (s *Service) CreateInstanceImage(instanceID string, req CreateInstanceImageRequest) (TaskReference, error) {
body, err := s.createInstanceImageResponseBody(instanceID, req)

return body.Data, err
}

func (s *Service) createInstanceImageResponseBody(instanceID string, req CreateInstanceImageRequest) (*connection.APIResponseBodyData[TaskReference], error) {
if instanceID == "" {
return &connection.APIResponseBodyData[TaskReference]{}, fmt.Errorf("invalid instance id")
}

return connection.Post[TaskReference](s.connection, fmt.Sprintf("/ecloud/v2/instances/%s/create-image", instanceID), &req, connection.NotFoundResponseHandler(&InstanceNotFoundError{ID: instanceID}))
}
63 changes: 63 additions & 0 deletions pkg/service/ecloud/service_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1709,3 +1709,66 @@ func TestGetInstanceFloatingIPs(t *testing.T) {
assert.IsType(t, &InstanceNotFoundError{}, err)
})
}

func TestCreateInstanceImage(t *testing.T) {
t.Run("Valid_NoError", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

c := mocks.NewMockConnection(mockCtrl)

s := Service{
connection: c,
}

req := CreateInstanceImageRequest{
Name: "testimage",
}

c.EXPECT().Post("/ecloud/v2/instances/i-abcdef12/create-image", gomock.Eq(&req)).Return(&connection.APIResponse{
Response: &http.Response{
Body: ioutil.NopCloser(bytes.NewReader([]byte("{\"data\":{\"task_id\":\"task-abcdef12\"},\"meta\":{\"location\":\"\"}}"))),
StatusCode: 202,
},
}, nil)

taskID, err := s.CreateInstanceImage("i-abcdef12", req)

assert.Nil(t, err)
assert.Equal(t, "task-abcdef12", taskID)
})

t.Run("ConnectionError_ReturnsError", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

c := mocks.NewMockConnection(mockCtrl)

s := Service{
connection: c,
}

c.EXPECT().Post("/ecloud/v2/instances/i-abcdef12/create-image", gomock.Any()).Return(&connection.APIResponse{}, errors.New("test error 1")).Times(1)

_, err := s.CreateInstanceImage("i-abcdef12", CreateInstanceImageRequest{})

assert.NotNil(t, err)
assert.Equal(t, "test error 1", err.Error())
})

t.Run("InvalidInstanceID_ReturnsError", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

c := mocks.NewMockConnection(mockCtrl)

s := Service{
connection: c,
}

_, err := s.CreateInstanceImage("", CreateInstanceImageRequest{})

assert.NotNil(t, err)
assert.Equal(t, "invalid instance id", err.Error())
})
}

0 comments on commit c5c13ee

Please sign in to comment.