add projects support

This commit is contained in:
Mike Chittenden 2018-09-27 17:33:57 +00:00
parent b3d0928046
commit 0f1b9ca783
11 changed files with 956 additions and 5 deletions

View File

@ -1,5 +1,9 @@
# Change Log
## [v1.6.0] - 2018-10-16
- #185 Projects support [beta] - @mchitten
## [v1.5.0] - 2018-10-01
- #181 Adding tagging images support - @hugocorbucci

View File

@ -97,6 +97,10 @@ func (d Domain) String() string {
return Stringify(d)
}
func (d Domain) URN() string {
return ToURN("Domain", d.Name)
}
// List all domains.
func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain, *Response, error) {
path := domainsBasePath

View File

@ -125,6 +125,10 @@ func (d Droplet) String() string {
return Stringify(d)
}
func (d Droplet) URN() string {
return ToURN("Droplet", d.ID)
}
// DropletRoot represents a Droplet root
type dropletRoot struct {
Droplet *Droplet `json:"droplet"`

View File

@ -49,6 +49,10 @@ func (fw Firewall) String() string {
return Stringify(fw)
}
func (fw Firewall) URN() string {
return ToURN("Firewall", fw.ID)
}
// FirewallRequest represents the configuration to be applied to an existing or a new Firewall.
type FirewallRequest struct {
Name string `json:"name"`

View File

@ -37,6 +37,10 @@ func (f FloatingIP) String() string {
return Stringify(f)
}
func (f FloatingIP) URN() string {
return ToURN("FloatingIP", f.IP)
}
type floatingIPsRoot struct {
FloatingIPs []FloatingIP `json:"floating_ips"`
Links *Links `json:"links"`

12
godo.go
View File

@ -18,7 +18,7 @@ import (
)
const (
libraryVersion = "1.5.0"
libraryVersion = "1.6.0"
defaultBaseURL = "https://api.digitalocean.com/"
userAgent = "godo/" + libraryVersion
mediaType = "application/json"
@ -64,6 +64,7 @@ type Client struct {
LoadBalancers LoadBalancersService
Certificates CertificatesService
Firewalls FirewallsService
Projects ProjectsService
// Optional function called after every successful request made to the DO APIs
onRequestCompleted RequestCompletionCallback
@ -159,23 +160,24 @@ func NewClient(httpClient *http.Client) *Client {
c.Account = &AccountServiceOp{client: c}
c.Actions = &ActionsServiceOp{client: c}
c.CDNs = &CDNServiceOp{client: c}
c.Certificates = &CertificatesServiceOp{client: c}
c.Domains = &DomainsServiceOp{client: c}
c.Droplets = &DropletsServiceOp{client: c}
c.DropletActions = &DropletActionsServiceOp{client: c}
c.Firewalls = &FirewallsServiceOp{client: c}
c.FloatingIPs = &FloatingIPsServiceOp{client: c}
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
c.Images = &ImagesServiceOp{client: c}
c.ImageActions = &ImageActionsServiceOp{client: c}
c.Keys = &KeysServiceOp{client: c}
c.LoadBalancers = &LoadBalancersServiceOp{client: c}
c.Projects = &ProjectsServiceOp{client: c}
c.Regions = &RegionsServiceOp{client: c}
c.Snapshots = &SnapshotsServiceOp{client: c}
c.Sizes = &SizesServiceOp{client: c}
c.Snapshots = &SnapshotsServiceOp{client: c}
c.Storage = &StorageServiceOp{client: c}
c.StorageActions = &StorageActionsServiceOp{client: c}
c.Tags = &TagsServiceOp{client: c}
c.LoadBalancers = &LoadBalancersServiceOp{client: c}
c.Certificates = &CertificatesServiceOp{client: c}
c.Firewalls = &FirewallsServiceOp{client: c}
return c
}

View File

@ -47,6 +47,10 @@ func (l LoadBalancer) String() string {
return Stringify(l)
}
func (l LoadBalancer) URN() string {
return ToURN("LoadBalancer", l.ID)
}
// AsRequest creates a LoadBalancerRequest that can be submitted to Update with the current values of the LoadBalancer.
// Modifying the returned LoadBalancerRequest will not modify the original LoadBalancer.
func (l LoadBalancer) AsRequest() *LoadBalancerRequest {

302
projects.go Normal file
View File

@ -0,0 +1,302 @@
package godo
import (
"context"
"encoding/json"
"fmt"
"net/http"
"path"
)
const (
// DefaultProject is the ID you should use if you are working with your
// default project.
DefaultProject = "default"
projectsBasePath = "/v2/projects"
)
// ProjectsService is an interface for creating and managing Projects with the DigitalOcean API.
// See: https://developers.digitalocean.com/documentation/documentation/v2/#projects
type ProjectsService interface {
List(context.Context, *ListOptions) ([]Project, *Response, error)
GetDefault(context.Context) (*Project, *Response, error)
Get(context.Context, string) (*Project, *Response, error)
Create(context.Context, *CreateProjectRequest) (*Project, *Response, error)
Update(context.Context, string, *UpdateProjectRequest) (*Project, *Response, error)
Delete(context.Context, string) (*Response, error)
ListResources(context.Context, string, *ListOptions) ([]ProjectResource, *Response, error)
AssignResources(context.Context, string, ...interface{}) ([]ProjectResource, *Response, error)
}
// ProjectsServiceOp handles communication with Projects methods of the DigitalOcean API.
type ProjectsServiceOp struct {
client *Client
}
// Project represents a DigitalOcean Project configuration.
type Project struct {
ID string `json:"id"`
OwnerUUID string `json:"owner_uuid"`
OwnerID uint64 `json:"owner_id"`
Name string `json:"name"`
Description string `json:"description"`
Purpose string `json:"purpose"`
Environment string `json:"environment"`
IsDefault bool `json:"is_default"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// String creates a human-readable description of a Project.
func (p Project) String() string {
return Stringify(p)
}
// CreateProjectRequest represents the request to create a new project.
type CreateProjectRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Purpose string `json:"purpose"`
Environment string `json:"environment"`
}
// UpdateProjectRequest represents the request to update project information.
// This type expects certain attribute types, but is built this way to allow
// nil values as well. See `updateProjectRequest` for the "real" types.
type UpdateProjectRequest struct {
Name interface{}
Description interface{}
Purpose interface{}
Environment interface{}
IsDefault interface{}
}
type updateProjectRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
Purpose *string `json:"purpose"`
Environment *string `json:"environment"`
IsDefault *bool `json:"is_default"`
}
// MarshalJSON takes an UpdateRequest and converts it to the "typed" request
// which is sent to the projects API. This is a PATCH request, which allows
// partial attributes, so `null` values are OK.
func (upr *UpdateProjectRequest) MarshalJSON() ([]byte, error) {
d := &updateProjectRequest{}
if str, ok := upr.Name.(string); ok {
d.Name = &str
}
if str, ok := upr.Description.(string); ok {
d.Description = &str
}
if str, ok := upr.Purpose.(string); ok {
d.Purpose = &str
}
if str, ok := upr.Environment.(string); ok {
d.Environment = &str
}
if val, ok := upr.IsDefault.(bool); ok {
d.IsDefault = &val
}
return json.Marshal(d)
}
type assignResourcesRequest struct {
Resources []string `json:"resources"`
}
// ProjectResource is the projects API's representation of a resource.
type ProjectResource struct {
URN string `json:"urn"`
AssignedAt string `json:"assigned_at"`
Links *ProjectResourceLinks `json:"links"`
Status string `json:"status,omitempty"`
}
// ProjetResourceLinks specify the link for more information about the resource.
type ProjectResourceLinks struct {
Self string `json:"self"`
}
type projectsRoot struct {
Projects []Project `json:"projects"`
Links *Links `json:"links"`
}
type projectRoot struct {
Project *Project `json:"project"`
}
type projectResourcesRoot struct {
Resources []ProjectResource `json:"resources"`
Links *Links `json:"links,omitempty"`
}
var _ ProjectsService = &ProjectsServiceOp{}
// List Projects.
func (p *ProjectsServiceOp) List(ctx context.Context, opts *ListOptions) ([]Project, *Response, error) {
path, err := addOptions(projectsBasePath, opts)
if err != nil {
return nil, nil, err
}
req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(projectsRoot)
resp, err := p.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Projects, resp, err
}
// GetDefault project.
func (p *ProjectsServiceOp) GetDefault(ctx context.Context) (*Project, *Response, error) {
return p.getHelper(ctx, "default")
}
// Get retrieves a single project by its ID.
func (p *ProjectsServiceOp) Get(ctx context.Context, projectID string) (*Project, *Response, error) {
return p.getHelper(ctx, projectID)
}
// Create a new project.
func (p *ProjectsServiceOp) Create(ctx context.Context, cr *CreateProjectRequest) (*Project, *Response, error) {
req, err := p.client.NewRequest(ctx, http.MethodPost, projectsBasePath, cr)
if err != nil {
return nil, nil, err
}
root := new(projectRoot)
resp, err := p.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root.Project, resp, err
}
// Update an existing project.
func (p *ProjectsServiceOp) Update(ctx context.Context, projectID string, ur *UpdateProjectRequest) (*Project, *Response, error) {
path := path.Join(projectsBasePath, projectID)
req, err := p.client.NewRequest(ctx, http.MethodPatch, path, ur)
if err != nil {
return nil, nil, err
}
root := new(projectRoot)
resp, err := p.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root.Project, resp, err
}
// Delete an existing project. You cannot have any resources in a project
// before deleting it. See the API documentation for more details.
func (p *ProjectsServiceOp) Delete(ctx context.Context, projectID string) (*Response, error) {
path := path.Join(projectsBasePath, projectID)
req, err := p.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
return p.client.Do(ctx, req, nil)
}
// ListResources lists all resources in a project.
func (p *ProjectsServiceOp) ListResources(ctx context.Context, projectID string, opts *ListOptions) ([]ProjectResource, *Response, error) {
basePath := path.Join(projectsBasePath, projectID, "resources")
path, err := addOptions(basePath, opts)
if err != nil {
return nil, nil, err
}
req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(projectResourcesRoot)
resp, err := p.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Resources, resp, err
}
// AssignResources assigns one or more resources to a project. AssignResources
// accepts resources in two possible formats:
// 1. The resource type, like `&Droplet{ID: 1}` or `&FloatingIP{IP: "1.2.3.4"}`
// 2. A valid DO URN as a string, like "do:droplet:1234"
//
// There is no unassign. To move a resource to another project, just assign
// it to that other project.
func (p *ProjectsServiceOp) AssignResources(ctx context.Context, projectID string, resources ...interface{}) ([]ProjectResource, *Response, error) {
path := path.Join(projectsBasePath, projectID, "resources")
ar := &assignResourcesRequest{
Resources: make([]string, len(resources)),
}
for i, resource := range resources {
switch resource.(type) {
case ResourceWithURN:
ar.Resources[i] = resource.(ResourceWithURN).URN()
case string:
ar.Resources[i] = resource.(string)
default:
return nil, nil, fmt.Errorf("%T must either be a string or have a valid URN method", resource)
}
}
req, err := p.client.NewRequest(ctx, http.MethodPost, path, ar)
if err != nil {
return nil, nil, err
}
root := new(projectResourcesRoot)
resp, err := p.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Resources, resp, err
}
func (p *ProjectsServiceOp) getHelper(ctx context.Context, projectID string) (*Project, *Response, error) {
path := path.Join(projectsBasePath, projectID)
req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(projectRoot)
resp, err := p.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root.Project, resp, err
}

609
projects_test.go Normal file
View File

@ -0,0 +1,609 @@
package godo
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
)
func TestProjects_List(t *testing.T) {
setup()
defer teardown()
projects := []Project{
{
ID: "project-1",
Name: "project-1",
},
{
ID: "project-2",
Name: "project-2",
},
}
mux.HandleFunc("/v2/projects", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
resp, _ := json.Marshal(projects)
fmt.Fprint(w, fmt.Sprintf(`{"projects":%s}`, string(resp)))
})
resp, _, err := client.Projects.List(ctx, nil)
if err != nil {
t.Errorf("Projects.List returned error: %v", err)
}
if !reflect.DeepEqual(resp, projects) {
t.Errorf("Projects.List returned %+v, expected %+v", resp, projects)
}
}
func TestProjects_ListWithMultiplePages(t *testing.T) {
setup()
defer teardown()
mockResp := `
{
"projects": [
{
"uuid": "project-1",
"name": "project-1"
},
{
"uuid": "project-2",
"name": "project-2"
}
],
"links": {
"pages": {
"next": "http://example.com/v2/projects?page=2"
}
}
}`
mux.HandleFunc("/v2/projects", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
fmt.Fprint(w, mockResp)
})
_, resp, err := client.Projects.List(ctx, nil)
if err != nil {
t.Errorf("Projects.List returned error: %v", err)
}
checkCurrentPage(t, resp, 1)
}
func TestProjects_ListWithPageNumber(t *testing.T) {
setup()
defer teardown()
mockResp := `
{
"projects": [
{
"uuid": "project-1",
"name": "project-1"
},
{
"uuid": "project-2",
"name": "project-2"
}
],
"links": {
"pages": {
"next": "http://example.com/v2/projects?page=3",
"prev": "http://example.com/v2/projects?page=1",
"last": "http://example.com/v2/projects?page=3",
"first": "http://example.com/v2/projects?page=1"
}
}
}`
mux.HandleFunc("/v2/projects", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
fmt.Fprint(w, mockResp)
})
_, resp, err := client.Projects.List(ctx, &ListOptions{Page: 2})
if err != nil {
t.Errorf("Projects.List returned error: %v", err)
}
checkCurrentPage(t, resp, 2)
}
func TestProjects_GetDefault(t *testing.T) {
setup()
defer teardown()
project := &Project{
ID: "project-1",
Name: "project-1",
}
mux.HandleFunc("/v2/projects/default", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
resp, _ := json.Marshal(project)
fmt.Fprint(w, fmt.Sprintf(`{"project":%s}`, string(resp)))
})
resp, _, err := client.Projects.GetDefault(ctx)
if err != nil {
t.Errorf("Projects.GetDefault returned error: %v", err)
}
if !reflect.DeepEqual(resp, project) {
t.Errorf("Projects.GetDefault returned %+v, expected %+v", resp, project)
}
}
func TestProjects_GetWithUUID(t *testing.T) {
setup()
defer teardown()
project := &Project{
ID: "project-1",
Name: "project-1",
}
mux.HandleFunc("/v2/projects/project-1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
resp, _ := json.Marshal(project)
fmt.Fprint(w, fmt.Sprintf(`{"project":%s}`, string(resp)))
})
resp, _, err := client.Projects.Get(ctx, "project-1")
if err != nil {
t.Errorf("Projects.Get returned error: %v", err)
}
if !reflect.DeepEqual(resp, project) {
t.Errorf("Projects.Get returned %+v, expected %+v", resp, project)
}
}
func TestProjects_Create(t *testing.T) {
setup()
defer teardown()
createRequest := &CreateProjectRequest{
Name: "my project",
Description: "for my stuff",
Purpose: "Just trying out DigitalOcean",
Environment: "Production",
}
createResp := &Project{
ID: "project-id",
Name: createRequest.Name,
Description: createRequest.Description,
Purpose: createRequest.Purpose,
Environment: createRequest.Environment,
}
mux.HandleFunc("/v2/projects", func(w http.ResponseWriter, r *http.Request) {
v := new(CreateProjectRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, http.MethodPost)
if !reflect.DeepEqual(v, createRequest) {
t.Errorf("Request body = %+v, expected %+v", v, createRequest)
}
resp, _ := json.Marshal(createResp)
fmt.Fprintf(w, fmt.Sprintf(`{"project":%s}`, string(resp)))
})
project, _, err := client.Projects.Create(ctx, createRequest)
if err != nil {
t.Errorf("Projects.Create returned error: %v", err)
}
if !reflect.DeepEqual(project, createResp) {
t.Errorf("Projects.Create returned %+v, expected %+v", project, createResp)
}
}
func TestProjects_UpdateWithOneAttribute(t *testing.T) {
setup()
defer teardown()
updateRequest := &UpdateProjectRequest{
Name: "my-great-project",
}
updateResp := &Project{
ID: "project-id",
Name: updateRequest.Name.(string),
Description: "some-other-description",
Purpose: "some-other-purpose",
Environment: "some-other-env",
IsDefault: false,
}
mux.HandleFunc("/v2/projects/project-1", func(w http.ResponseWriter, r *http.Request) {
reqBytes, respErr := ioutil.ReadAll(r.Body)
if respErr != nil {
t.Error("projects mock didn't work")
}
req := strings.TrimSuffix(string(reqBytes), "\n")
expectedReq := `{"name":"my-great-project","description":null,"purpose":null,"environment":null,"is_default":null}`
if req != expectedReq {
t.Errorf("projects req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req)
}
resp, _ := json.Marshal(updateResp)
fmt.Fprintf(w, fmt.Sprintf(`{"project":%s}`, string(resp)))
})
project, _, err := client.Projects.Update(ctx, "project-1", updateRequest)
if err != nil {
t.Errorf("Projects.Update returned error: %v", err)
}
if !reflect.DeepEqual(project, updateResp) {
t.Errorf("Projects.Update returned %+v, expected %+v", project, updateResp)
}
}
func TestProjects_UpdateWithAllAttributes(t *testing.T) {
setup()
defer teardown()
updateRequest := &UpdateProjectRequest{
Name: "my-great-project",
Description: "some-description",
Purpose: "some-purpose",
Environment: "some-env",
IsDefault: true,
}
updateResp := &Project{
ID: "project-id",
Name: updateRequest.Name.(string),
Description: updateRequest.Description.(string),
Purpose: updateRequest.Purpose.(string),
Environment: updateRequest.Environment.(string),
IsDefault: updateRequest.IsDefault.(bool),
}
mux.HandleFunc("/v2/projects/project-1", func(w http.ResponseWriter, r *http.Request) {
reqBytes, respErr := ioutil.ReadAll(r.Body)
if respErr != nil {
t.Error("projects mock didn't work")
}
req := strings.TrimSuffix(string(reqBytes), "\n")
expectedReq := `{"name":"my-great-project","description":"some-description","purpose":"some-purpose","environment":"some-env","is_default":true}`
if req != expectedReq {
t.Errorf("projects req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req)
}
resp, _ := json.Marshal(updateResp)
fmt.Fprintf(w, fmt.Sprintf(`{"project":%s}`, string(resp)))
})
project, _, err := client.Projects.Update(ctx, "project-1", updateRequest)
if err != nil {
t.Errorf("Projects.Update returned error: %v", err)
}
if !reflect.DeepEqual(project, updateResp) {
t.Errorf("Projects.Update returned %+v, expected %+v", project, updateResp)
}
}
func TestProjects_Destroy(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/projects/project-1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodDelete)
})
_, err := client.Projects.Delete(ctx, "project-1")
if err != nil {
t.Errorf("Projects.Delete returned error: %v", err)
}
}
func TestProjects_ListResources(t *testing.T) {
setup()
defer teardown()
resources := []ProjectResource{
{
URN: "do:droplet:1",
AssignedAt: "2018-09-27 00:00:00",
Links: &ProjectResourceLinks{
Self: "http://example.com/v2/droplets/1",
},
},
{
URN: "do:floatingip:1.2.3.4",
AssignedAt: "2018-09-27 00:00:00",
Links: &ProjectResourceLinks{
Self: "http://example.com/v2/floating_ips/1.2.3.4",
},
},
}
mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
resp, _ := json.Marshal(resources)
fmt.Fprint(w, fmt.Sprintf(`{"resources":%s}`, string(resp)))
})
resp, _, err := client.Projects.ListResources(ctx, "project-1", nil)
if err != nil {
t.Errorf("Projects.List returned error: %v", err)
}
if !reflect.DeepEqual(resp, resources) {
t.Errorf("Projects.ListResources returned %+v, expected %+v", resp, resources)
}
}
func TestProjects_ListResourcesWithMultiplePages(t *testing.T) {
setup()
defer teardown()
mockResp := `
{
"resources": [
{
"urn": "do:droplet:1",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/droplets/1"
}
},
{
"urn": "do:floatingip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
}
],
"links": {
"pages": {
"next": "http://example.com/v2/projects/project-1/resources?page=2"
}
}
}`
mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
fmt.Fprint(w, mockResp)
})
_, resp, err := client.Projects.ListResources(ctx, "project-1", nil)
if err != nil {
t.Errorf("Projects.ListResources returned error: %v", err)
}
checkCurrentPage(t, resp, 1)
}
func TestProjects_ListResourcesWithPageNumber(t *testing.T) {
setup()
defer teardown()
mockResp := `
{
"resources": [
{
"urn": "do:droplet:1",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/droplets/1"
}
},
{
"urn": "do:floatingip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
}
],
"links": {
"pages": {
"next": "http://example.com/v2/projects/project-1/resources?page=3",
"prev": "http://example.com/v2/projects/project-1/resources?page=1",
"last": "http://example.com/v2/projects/project-1/resources?page=3",
"first": "http://example.com/v2/projects/project-1/resources?page=1"
}
}
}`
mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
fmt.Fprint(w, mockResp)
})
_, resp, err := client.Projects.ListResources(ctx, "project-1", &ListOptions{Page: 2})
if err != nil {
t.Errorf("Projects.ListResources returned error: %v", err)
}
checkCurrentPage(t, resp, 2)
}
func TestProjects_AssignFleetResourcesWithTypes(t *testing.T) {
setup()
defer teardown()
assignableResources := []interface{}{
&Droplet{ID: 1234},
&FloatingIP{IP: "1.2.3.4"},
}
mockResp := `
{
"resources": [
{
"urn": "do:droplet:1234",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/droplets/1"
}
},
{
"urn": "do:floatingip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
}
]
}`
mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)
reqBytes, respErr := ioutil.ReadAll(r.Body)
if respErr != nil {
t.Error("projects mock didn't work")
}
req := strings.TrimSuffix(string(reqBytes), "\n")
expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4"]}`
if req != expectedReq {
t.Errorf("projects assign req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req)
}
fmt.Fprint(w, mockResp)
})
_, _, err := client.Projects.AssignResources(ctx, "project-1", assignableResources...)
if err != nil {
t.Errorf("Projects.AssignResources returned error: %v", err)
}
}
func TestProjects_AssignFleetResourcesWithStrings(t *testing.T) {
setup()
defer teardown()
assignableResources := []interface{}{
"do:droplet:1234",
"do:floatingip:1.2.3.4",
}
mockResp := `
{
"resources": [
{
"urn": "do:droplet:1234",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/droplets/1"
}
},
{
"urn": "do:floatingip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
}
]
}`
mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)
reqBytes, respErr := ioutil.ReadAll(r.Body)
if respErr != nil {
t.Error("projects mock didn't work")
}
req := strings.TrimSuffix(string(reqBytes), "\n")
expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4"]}`
if req != expectedReq {
t.Errorf("projects assign req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req)
}
fmt.Fprint(w, mockResp)
})
_, _, err := client.Projects.AssignResources(ctx, "project-1", assignableResources...)
if err != nil {
t.Errorf("Projects.AssignResources returned error: %v", err)
}
}
func TestProjects_AssignFleetResourcesWithStringsAndTypes(t *testing.T) {
setup()
defer teardown()
assignableResources := []interface{}{
"do:droplet:1234",
&FloatingIP{IP: "1.2.3.4"},
}
mockResp := `
{
"resources": [
{
"urn": "do:droplet:1234",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/droplets/1"
}
},
{
"urn": "do:floatingip:1.2.3.4",
"assigned_at": "2018-09-27 00:00:00",
"links": {
"self": "http://example.com/v2/floating_ips/1.2.3.4"
}
}
]
}`
mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)
reqBytes, respErr := ioutil.ReadAll(r.Body)
if respErr != nil {
t.Error("projects mock didn't work")
}
req := strings.TrimSuffix(string(reqBytes), "\n")
expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4"]}`
if req != expectedReq {
t.Errorf("projects assign req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req)
}
fmt.Fprint(w, mockResp)
})
_, _, err := client.Projects.AssignResources(ctx, "project-1", assignableResources...)
if err != nil {
t.Errorf("Projects.AssignResources returned error: %v", err)
}
}
func TestProjects_AssignFleetResourcesWithTypeWithoutURNReturnsError(t *testing.T) {
setup()
defer teardown()
type fakeType struct{}
assignableResources := []interface{}{
fakeType{},
}
_, _, err := client.Projects.AssignResources(ctx, "project-1", assignableResources...)
if err == nil {
t.Errorf("expected Projects.AssignResources to error, but it did not")
}
if err.Error() != "godo.fakeType must either be a string or have a valid URN method" {
t.Errorf("Projects.AssignResources returned the wrong error: %v", err)
}
}

View File

@ -59,6 +59,10 @@ func (f Volume) String() string {
return Stringify(f)
}
func (f Volume) URN() string {
return ToURN("Volume", f.ID)
}
type storageVolumesRoot struct {
Volumes []Volume `json:"volumes"`
Links *Links `json:"links"`

View File

@ -5,10 +5,20 @@ import (
"fmt"
"io"
"reflect"
"strings"
)
var timestampType = reflect.TypeOf(Timestamp{})
type ResourceWithURN interface {
URN() string
}
// ToURN converts the resource type and ID to a valid DO API URN.
func ToURN(resourceType string, id interface{}) string {
return fmt.Sprintf("%s:%s:%v", "do", strings.ToLower(resourceType), id)
}
// Stringify attempts to create a string representation of DigitalOcean types
func Stringify(message interface{}) string {
var buf bytes.Buffer