diff --git a/images.go b/images.go index c1fd012..69de4c0 100644 --- a/images.go +++ b/images.go @@ -16,8 +16,10 @@ type ImagesService interface { ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) + ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error) GetByID(context.Context, int) (*Image, *Response, error) GetBySlug(context.Context, string) (*Image, *Response, error) + Create(context.Context, *CustomImageCreateRequest) (*Image, *Response, error) Update(context.Context, int, *ImageUpdateRequest) (*Image, *Response, error) Delete(context.Context, int) (*Response, error) } @@ -32,15 +34,20 @@ var _ ImagesService = &ImagesServiceOp{} // Image represents a DigitalOcean Image type Image struct { - ID int `json:"id,float64,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Distribution string `json:"distribution,omitempty"` - Slug string `json:"slug,omitempty"` - Public bool `json:"public,omitempty"` - Regions []string `json:"regions,omitempty"` - MinDiskSize int `json:"min_disk_size,omitempty"` - Created string `json:"created_at,omitempty"` + ID int `json:"id,float64,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Distribution string `json:"distribution,omitempty"` + Slug string `json:"slug,omitempty"` + Public bool `json:"public,omitempty"` + Regions []string `json:"regions,omitempty"` + MinDiskSize int `json:"min_disk_size,omitempty"` + SizeGigaBytes float64 `json:"size_gigabytes,omitempty"` + Created string `json:"created_at,omitempty"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` + Status string `json:"status,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` } // ImageUpdateRequest represents a request to update an image. @@ -48,6 +55,16 @@ type ImageUpdateRequest struct { Name string `json:"name"` } +// CustomImageCreateRequest represents a request to create a custom image. +type CustomImageCreateRequest struct { + Name string `json:"name"` + Url string `json:"url"` + Region string `json:"region"` + Distribution string `json:"distribution,omitempty"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` +} + type imageRoot struct { Image *Image } @@ -60,6 +77,7 @@ type imagesRoot struct { type listImageOptions struct { Private bool `url:"private,omitempty"` Type string `url:"type,omitempty"` + Tag string `url:"tag_name,omitempty"` } func (i Image) String() string { @@ -89,6 +107,12 @@ func (s *ImagesServiceOp) ListUser(ctx context.Context, opt *ListOptions) ([]Ima return s.list(ctx, opt, &listOpt) } +// ListByTag lists all images with a specific tag applied. +func (s *ImagesServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error) { + listOpt := listImageOptions{Tag: tag} + return s.list(ctx, opt, &listOpt) +} + // GetByID retrieves an image by id. func (s *ImagesServiceOp) GetByID(ctx context.Context, imageID int) (*Image, *Response, error) { if imageID < 1 { @@ -107,6 +131,25 @@ func (s *ImagesServiceOp) GetBySlug(ctx context.Context, slug string) (*Image, * return s.get(ctx, interface{}(slug)) } +func (s *ImagesServiceOp) Create(ctx context.Context, createRequest *CustomImageCreateRequest) (*Image, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := s.client.NewRequest(ctx, http.MethodPost, imageBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(imageRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Image, resp, err +} + // Update an image name. func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest *ImageUpdateRequest) (*Image, *Response, error) { if imageID < 1 { @@ -118,7 +161,7 @@ func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest } path := fmt.Sprintf("%s/%d", imageBasePath, imageID) - req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest) + req, err := s.client.NewRequest(ctx, http.MethodPut, path, updateRequest) if err != nil { return nil, nil, err } diff --git a/images_test.go b/images_test.go index 2134952..9dab7f7 100644 --- a/images_test.go +++ b/images_test.go @@ -104,6 +104,32 @@ func TestImages_ListUser(t *testing.T) { } } +func TestImages_ListByTag(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + expected := "foo" + actual := r.URL.Query().Get("tag_name") + if actual != expected { + t.Errorf("'tag_name' query = %v, expected %v", actual, expected) + } + + fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`) + }) + + images, _, err := client.Images.ListByTag(ctx, "foo", nil) + if err != nil { + t.Errorf("Images.ListByTag returned error: %v", err) + } + + expected := []Image{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(images, expected) { + t.Errorf("Images.ListByTag returned %+v, expected %+v", images, expected) + } +} + func TestImages_ListImagesMultiplePages(t *testing.T) { setup() defer teardown() @@ -191,6 +217,52 @@ func TestImages_GetImageBySlug(t *testing.T) { } } +func TestImages_Create(t *testing.T) { + setup() + defer teardown() + + createRequest := &CustomImageCreateRequest{ + Name: "my-new-image", + Url: "http://example.com/distro-amd64.img", + Region: "nyc3", + Distribution: "Ubuntu", + Description: "My new custom image", + Tags: []string{"foo", "bar"}, + } + + mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { + expected := map[string]interface{}{ + "name": "my-new-image", + "url": "http://example.com/distro-amd64.img", + "region": "nyc3", + "distribution": "Ubuntu", + "description": "My new custom image", + "tags": []interface{}{"foo", "bar"}, + } + + var v map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + if !reflect.DeepEqual(v, expected) { + t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) + } + + fmt.Fprintf(w, `{"image": {"id": 1,"created_at": "2018-09-20T19:28:00Z","description": "A custom image","distribution": "Ubuntu","error_message": "","regions": [],"type": "custom","tags":["foo","bar"],"status": "NEW"}}`) + }) + + image, _, err := client.Images.Create(ctx, createRequest) + if err != nil { + t.Errorf("Images.Create returned error: %v", err) + } + + if id := image.ID; id != 1 { + t.Errorf("expected id '%d', received '%d'", 1, id) + } +} + func TestImages_Update(t *testing.T) { setup() defer teardown() @@ -243,19 +315,20 @@ func TestImages_Destroy(t *testing.T) { func TestImage_String(t *testing.T) { image := &Image{ - ID: 1, - Name: "Image", - Type: "snapshot", - Distribution: "Ubuntu", - Slug: "image", - Public: true, - Regions: []string{"one", "two"}, - MinDiskSize: 20, - Created: "2013-11-27T09:24:55Z", + ID: 1, + Name: "Image", + Type: "snapshot", + Distribution: "Ubuntu", + Slug: "image", + Public: true, + Regions: []string{"one", "two"}, + MinDiskSize: 20, + SizeGigaBytes: 2.36, + Created: "2013-11-27T09:24:55Z", } stringified := image.String() - expected := `godo.Image{ID:1, Name:"Image", Type:"snapshot", Distribution:"Ubuntu", Slug:"image", Public:true, Regions:["one" "two"], MinDiskSize:20, Created:"2013-11-27T09:24:55Z"}` + expected := `godo.Image{ID:1, Name:"Image", Type:"snapshot", Distribution:"Ubuntu", Slug:"image", Public:true, Regions:["one" "two"], MinDiskSize:20, SizeGigaBytes:2.36, Created:"2013-11-27T09:24:55Z", Description:"", Status:"", ErrorMessage:""}` if expected != stringified { t.Errorf("Image.String returned %+v, expected %+v", stringified, expected) }