diff --git a/CHANGELOG.md b/CHANGELOG.md index 8995519..7ee72e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [v1.4.0] - 2018-08-22 + +- #170 Add support for Spaces CDN - @sunny-b + ## [v1.3.0] - 2018-05-24 - #170 Add support for volume formatting - @adamwg diff --git a/cdn.go b/cdn.go new file mode 100644 index 0000000..980c4bd --- /dev/null +++ b/cdn.go @@ -0,0 +1,195 @@ +package godo + +import ( + "context" + "fmt" + "net/http" + "time" +) + +const cdnBasePath = "v2/cdn/endpoints" + +// CDNService is an interface for managing Spaces CDN with the DigitalOcean API. +type CDNService interface { + List(context.Context, *ListOptions) ([]CDN, *Response, error) + Get(context.Context, string) (*CDN, *Response, error) + Create(context.Context, *CDNCreateRequest) (*CDN, *Response, error) + UpdateTTL(context.Context, string, *CDNUpdateRequest) (*CDN, *Response, error) + FlushCache(context.Context, string, *CDNFlushCacheRequest) (*Response, error) + Delete(context.Context, string) (*Response, error) +} + +// CDNServiceOp handles communication with the CDN related methods of the +// DigitalOcean API. +type CDNServiceOp struct { + client *Client +} + +var _ CDNService = &CDNServiceOp{} + +// CDN represents a DigitalOcean CDN +type CDN struct { + ID string `json:"id"` + Origin string `json:"origin"` + Endpoint string `json:"endpoint"` + CreatedAt time.Time `json:"created_at"` + TTL uint32 `json:"ttl"` +} + +// CDNRoot represents a response from the DigitalOcean API +type cdnRoot struct { + Endpoint *CDN `json:"endpoint"` +} + +type cdnsRoot struct { + Endpoints []CDN `json:"endpoints"` + Links *Links `json:"links"` +} + +// CDNCreateRequest represents a request to create a CDN. +type CDNCreateRequest struct { + Origin string `json:"origin"` + TTL uint32 `json:"ttl"` +} + +// CDNUpdateRequest represents a request to update the ttl of a CDN. +type CDNUpdateRequest struct { + TTL uint32 `json:"ttl"` +} + +// CDNFlushCacheRequest represents a request to flush cache of a CDN. +type CDNFlushCacheRequest struct { + Files []string `json:"files"` +} + +// List all CDN endpoints +func (c CDNServiceOp) List(ctx context.Context, opt *ListOptions) ([]CDN, *Response, error) { + path, err := addOptions(cdnBasePath, opt) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(cdnsRoot) + resp, err := c.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Endpoints, resp, err +} + +// Get individual CDN. It requires a non-empty cdn id. +func (c CDNServiceOp) Get(ctx context.Context, id string) (*CDN, *Response, error) { + if len(id) == 0 { + return nil, nil, NewArgError("id", "cannot be an empty string") + } + + path := fmt.Sprintf("%s/%s", cdnBasePath, id) + + req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(cdnRoot) + resp, err := c.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Endpoint, resp, err +} + +// Create a new CDN +func (c CDNServiceOp) Create(ctx context.Context, createRequest *CDNCreateRequest) (*CDN, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := c.client.NewRequest(ctx, http.MethodPost, cdnBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(cdnRoot) + resp, err := c.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Endpoint, resp, err +} + +// UpdateTTL updates the ttl of individual CDN +func (c CDNServiceOp) UpdateTTL(ctx context.Context, id string, updateRequest *CDNUpdateRequest) (*CDN, *Response, error) { + if updateRequest == nil { + return nil, nil, NewArgError("updateRequest", "cannot be nil") + } + + if len(id) == 0 { + return nil, nil, NewArgError("id", "cannot be an empty string") + } + + path := fmt.Sprintf("%s/%s", cdnBasePath, id) + + req, err := c.client.NewRequest(ctx, http.MethodPut, path, updateRequest) + if err != nil { + return nil, nil, err + } + + root := new(cdnRoot) + resp, err := c.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Endpoint, resp, err +} + +// FlushCache flushes the cache of an individual CDN. Requires a non-empty slice of file paths and/or wildcards +func (c CDNServiceOp) FlushCache(ctx context.Context, id string, flushCacheRequest *CDNFlushCacheRequest) (*Response, error) { + if flushCacheRequest == nil { + return nil, NewArgError("flushCacheRequest", "cannot be nil") + } + + if len(id) == 0 { + return nil, NewArgError("id", "cannot be an empty string") + } + + path := fmt.Sprintf("%s/%s", cdnBasePath, id) + + req, err := c.client.NewRequest(ctx, http.MethodDelete, path, flushCacheRequest) + if err != nil { + return nil, err + } + + resp, err := c.client.Do(ctx, req, nil) + + return resp, err +} + +// Delete an individual CDN +func (c CDNServiceOp) Delete(ctx context.Context, id string) (*Response, error) { + if len(id) == 0 { + return nil, NewArgError("id", "cannot be an empty string") + } + + path := fmt.Sprintf("%s/%s", cdnBasePath, id) + + req, err := c.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + resp, err := c.client.Do(ctx, req, nil) + + return resp, err +} diff --git a/cdn_test.go b/cdn_test.go new file mode 100644 index 0000000..9d35adc --- /dev/null +++ b/cdn_test.go @@ -0,0 +1,290 @@ +package godo + +import ( + "fmt" + "net/http" + "reflect" + "testing" + "time" +) + +func TestCDN_ListCDN(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/cdn/endpoints", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint( + w, + `{ + "endpoints": [ + { + "id": "892071a0-bb95-49bc-8021-3afd67a210bf", + "origin": "my-space.nyc3.digitaloceanspaces.com", + "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", + "ttl": 3600, + "created_at": "2012-10-02T15:00:01.05Z" + }, + { + "id": "892071a0-bb95-55bd-8021-3afd67a210bf", + "origin": "my-space1.nyc3.digitaloceanspaces.com", + "endpoint": "my-space1.nyc3.cdn.digitaloceanspaces.com", + "ttl": 3600, + "created_at": "2012-10-03T15:00:01.05Z" + } + ] + }`, + ) + }) + + cdns, _, err := client.CDNs.List(ctx, nil) + if err != nil { + t.Errorf("CDNs.List returned error: %v", err) + } + + expected := []CDN{ + { + ID: "892071a0-bb95-49bc-8021-3afd67a210bf", + Origin: "my-space.nyc3.digitaloceanspaces.com", + Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", + TTL: 3600, + CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), + }, + { + ID: "892071a0-bb95-55bd-8021-3afd67a210bf", + Origin: "my-space1.nyc3.digitaloceanspaces.com", + Endpoint: "my-space1.nyc3.cdn.digitaloceanspaces.com", + TTL: 3600, + CreatedAt: time.Date(2012, 10, 03, 15, 00, 01, 50000000, time.UTC), + }, + } + + if !reflect.DeepEqual(cdns, expected) { + t.Errorf("CDNs.List returned %+v, expected %+v", cdns, expected) + } +} + +func TestCDN_ListCDNMultiplePages(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/cdn/endpoints", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint( + w, + `{ + "endpoints": [ + { + "id": "892071a0-bb95-49bc-8021-3afd67a210bf", + "origin": "my-space.nyc3.digitaloceanspaces.com", + "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", + "ttl": 3600, + "created_at": "2012-10-02T15:00:01.05Z" + }, + { + "id": "892071a0-bb95-55bd-8021-3afd67a210bf", + "origin": "my-space1.nyc3.digitaloceanspaces.com", + "endpoint": "my-space1.nyc3.cdn.digitaloceanspaces.com", + "ttl": 3600, + "created_at": "2012-10-03T15:00:01.05Z" + } + ], + "links":{"pages":{"next":"http://example.com/v2/cdn/endpoints/?page=2"}} + }`, + ) + }) + + _, resp, err := client.CDNs.List(ctx, nil) + if err != nil { + t.Errorf("CDNs.List multiple page returned error: %v", err) + } + + checkCurrentPage(t, resp, 1) +} + +func TestCDN_RetrievePageByNumber(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/cdn/endpoints", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint( + w, + `{ + "endpoints": [ + { + "id": "892071a0-bb95-49bc-8021-3afd67a210bf", + "origin": "my-space.nyc3.digitaloceanspaces.com", + "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", + "ttl": 3600, + "created_at": "2012-10-02T15:00:01.05Z" + }, + { + "id": "892071a0-bb95-55bd-8021-3afd67a210bf", + "origin": "my-space1.nyc3.digitaloceanspaces.com", + "endpoint": "my-space1.nyc3.cdn.digitaloceanspaces.com", + "ttl": 3600, + "created_at": "2012-10-03T15:00:01.05Z" + } + ], + "links":{"pages":{ + "next":"http://example.com/v2/cdn/endpoints/?page=3", + "prev":"http://example.com/v2/cdn/endpoints/?page=1", + "last":"http://example.com/v2/cdn/endpoints/?page=3", + "first":"http://example.com/v2/cdn/endpoints/?page=1"}} + }`, + ) + }) + + _, resp, err := client.CDNs.List(ctx, &ListOptions{Page: 2}) + if err != nil { + t.Errorf("CDNs.List singular page returned error: %v", err) + } + + checkCurrentPage(t, resp, 2) +} + +func TestCDN_GetCDN(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/cdn/endpoints/12345", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint( + w, + `{ + "endpoint": { + "id": "12345", + "origin": "my-space.nyc3.digitaloceanspaces.com", + "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", + "ttl": 3600, + "created_at": "2012-10-02T15:00:01.05Z" + } + }`, + ) + }) + + cdn, _, err := client.CDNs.Get(ctx, "12345") + if err != nil { + t.Errorf("CDNs.Get returned error: %v", err) + } + + expected := &CDN{ + ID: "12345", + Origin: "my-space.nyc3.digitaloceanspaces.com", + Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", + TTL: 3600, + CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), + } + + if !reflect.DeepEqual(cdn, expected) { + t.Errorf("CDNs.Get returned %+v, expected %+v", cdn, expected) + } +} + +func TestCDN_CreateCDN(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/cdn/endpoints", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPost) + fmt.Fprint( + w, + `{ + "endpoint": { + "id": "12345", + "origin": "my-space.nyc3.digitaloceanspaces.com", + "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", + "ttl": 3600, + "created_at": "2012-10-02T15:00:01.05Z" + } + }`, + ) + }) + + req := &CDNCreateRequest{Origin: "my-space.nyc3.digitaloceanspaces.com", TTL: 3600} + cdn, _, err := client.CDNs.Create(ctx, req) + if err != nil { + t.Errorf("CDNs.Create returned error: %v", err) + } + + expected := &CDN{ + ID: "12345", + Origin: "my-space.nyc3.digitaloceanspaces.com", + Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", + TTL: 3600, + CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), + } + + if !reflect.DeepEqual(cdn, expected) { + t.Errorf("CDNs.Create returned %+v, expected %+v", cdn, expected) + } +} + +func TestCDN_DeleteCDN(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/cdn/endpoints/12345", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.CDNs.Delete(ctx, "12345") + if err != nil { + t.Errorf("CDNs.Delete returned error: %v", err) + } +} + +func TestCDN_UpdateTTLCDN(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/cdn/endpoints/12345", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPut) + fmt.Fprint( + w, + `{ + "endpoint": { + "id": "12345", + "origin": "my-space.nyc3.digitaloceanspaces.com", + "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", + "ttl": 60, + "created_at": "2012-10-02T15:00:01.05Z" + } + }`, + ) + }) + + req := &CDNUpdateRequest{TTL: 60} + cdn, _, err := client.CDNs.UpdateTTL(ctx, "12345", req) + if err != nil { + t.Errorf("CDNs.UpdateTTL returned error: %v", err) + } + + expected := &CDN{ + ID: "12345", + Origin: "my-space.nyc3.digitaloceanspaces.com", + Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", + TTL: 60, + CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), + } + + if !reflect.DeepEqual(cdn, expected) { + t.Errorf("CDNs.UpdateTTL returned %+v, expected %+v", cdn, expected) + } +} + +func TestCDN_FluchCacheCDN(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/cdn/endpoints/12345", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + req := &CDNFlushCacheRequest{Files: []string{"*"}} + _, err := client.CDNs.FlushCache(ctx, "12345", req) + if err != nil { + t.Errorf("CDNs.FlushCache returned error: %v", err) + } +} diff --git a/godo.go b/godo.go index 8ec5865..12eb11c 100644 --- a/godo.go +++ b/godo.go @@ -18,7 +18,7 @@ import ( ) const ( - libraryVersion = "1.3.0" + libraryVersion = "1.4.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" @@ -46,6 +46,7 @@ type Client struct { // Services used for communicating with the API Account AccountService Actions ActionsService + CDNs CDNService Domains DomainsService Droplets DropletsService DropletActions DropletActionsService @@ -157,6 +158,7 @@ func NewClient(httpClient *http.Client) *Client { c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent} c.Account = &AccountServiceOp{client: c} c.Actions = &ActionsServiceOp{client: c} + c.CDNs = &CDNServiceOp{client: c} c.Domains = &DomainsServiceOp{client: c} c.Droplets = &DropletsServiceOp{client: c} c.DropletActions = &DropletActionsServiceOp{client: c} diff --git a/godo_test.go b/godo_test.go index e075293..296037a 100644 --- a/godo_test.go +++ b/godo_test.go @@ -74,6 +74,7 @@ func testClientServices(t *testing.T, c *Client) { services := []string{ "Account", "Actions", + "CDNs", "Domains", "Droplets", "DropletActions",