Merge pull request #339 from jrperritt/cdn-openstack-rackspace
OpenStack and Rackspace CDN Service Support
diff --git a/acceptance/rackspace/cdn/v1/base_test.go b/acceptance/rackspace/cdn/v1/base_test.go
new file mode 100644
index 0000000..135f5b3
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/base_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace/cdn/v1/base"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestBaseOps(t *testing.T) {
+ client := newClient(t)
+ t.Log("Retrieving Home Document")
+ testHomeDocumentGet(t, client)
+
+ t.Log("Pinging root URL")
+ testPing(t, client)
+}
+
+func testHomeDocumentGet(t *testing.T, client *gophercloud.ServiceClient) {
+ hd, err := base.Get(client).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Retrieved home document: %+v", *hd)
+}
+
+func testPing(t *testing.T, client *gophercloud.ServiceClient) {
+ err := base.Ping(client).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Successfully pinged root URL")
+}
diff --git a/acceptance/rackspace/cdn/v1/common.go b/acceptance/rackspace/cdn/v1/common.go
new file mode 100644
index 0000000..2333ca7
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/common.go
@@ -0,0 +1,23 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+ ao, err := rackspace.AuthOptionsFromEnv()
+ th.AssertNoErr(t, err)
+
+ client, err := rackspace.AuthenticatedClient(ao)
+ th.AssertNoErr(t, err)
+
+ c, err := rackspace.NewCDNV1(client, gophercloud.EndpointOpts{})
+ th.AssertNoErr(t, err)
+ return c
+}
diff --git a/acceptance/rackspace/cdn/v1/flavor_test.go b/acceptance/rackspace/cdn/v1/flavor_test.go
new file mode 100644
index 0000000..f26cff0
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/flavor_test.go
@@ -0,0 +1,47 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestFlavor(t *testing.T) {
+ client := newClient(t)
+
+ t.Log("Listing Flavors")
+ id := testFlavorsList(t, client)
+
+ t.Log("Retrieving Flavor")
+ testFlavorGet(t, client, id)
+}
+
+func testFlavorsList(t *testing.T, client *gophercloud.ServiceClient) string {
+ var id string
+ err := flavors.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ flavorList, err := os.ExtractFlavors(page)
+ th.AssertNoErr(t, err)
+
+ for _, flavor := range flavorList {
+ t.Logf("Listing flavor: ID [%s] Providers [%+v]", flavor.ID, flavor.Providers)
+ id = flavor.ID
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ return id
+}
+
+func testFlavorGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ flavor, err := flavors.Get(client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Retrieved Flavor: %+v", *flavor)
+}
diff --git a/acceptance/rackspace/cdn/v1/service_test.go b/acceptance/rackspace/cdn/v1/service_test.go
new file mode 100644
index 0000000..af73dcf
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/service_test.go
@@ -0,0 +1,97 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/cdn/v1/services"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestService(t *testing.T) {
+ client := newClient(t)
+
+ t.Log("Creating Service")
+ loc := testServiceCreate(t, client)
+ t.Logf("Created service at location: %s", loc)
+
+ defer testServiceDelete(t, client, loc)
+
+ t.Log("Updating Service")
+ testServiceUpdate(t, client, loc)
+
+ t.Log("Retrieving Service")
+ testServiceGet(t, client, loc)
+
+ t.Log("Listing Services")
+ testServiceList(t, client)
+}
+
+func testServiceCreate(t *testing.T, client *gophercloud.ServiceClient) string {
+ createOpts := os.CreateOpts{
+ Name: "gophercloud-test-service",
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www.gophercloud-test-service.com",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: "gophercloud-test-service.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ FlavorID: "cdn",
+ }
+ l, err := services.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ return l
+}
+
+func testServiceGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ s, err := services.Get(client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Retrieved service: %+v", *s)
+}
+
+func testServiceUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ updateOpts := os.UpdateOpts{
+ os.UpdateOpt{
+ Op: os.Add,
+ Path: "/domains/-",
+ Value: map[string]interface{}{
+ "domain": "newDomain.com",
+ "protocol": "http",
+ },
+ },
+ }
+ loc, err := services.Update(client, id, updateOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Successfully updated service at location: %s", loc)
+}
+
+func testServiceList(t *testing.T, client *gophercloud.ServiceClient) {
+ err := services.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ serviceList, err := os.ExtractServices(page)
+ th.AssertNoErr(t, err)
+
+ for _, service := range serviceList {
+ t.Logf("Listing service: %+v", service)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func testServiceDelete(t *testing.T, client *gophercloud.ServiceClient, id string) {
+ err := services.Delete(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Successfully deleted service (%s)", id)
+}
diff --git a/acceptance/rackspace/cdn/v1/serviceasset_test.go b/acceptance/rackspace/cdn/v1/serviceasset_test.go
new file mode 100644
index 0000000..1840946
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/serviceasset_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ osServiceAssets "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+ "github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestServiceAsset(t *testing.T) {
+ client := newClient(t)
+
+ t.Log("Creating Service")
+ loc := testServiceCreate(t, client)
+ t.Logf("Created service at location: %s", loc)
+
+ t.Log("Deleting Service Assets")
+ testServiceAssetDelete(t, client, loc)
+}
+
+func testServiceAssetDelete(t *testing.T, client *gophercloud.ServiceClient, url string) {
+ deleteOpts := osServiceAssets.DeleteOpts{
+ All: true,
+ }
+ err := serviceassets.Delete(client, url, deleteOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Log("Successfully deleted all Service Assets")
+}
diff --git a/openstack/cdn/v1/base/doc.go b/openstack/cdn/v1/base/doc.go
new file mode 100644
index 0000000..f78d4f7
--- /dev/null
+++ b/openstack/cdn/v1/base/doc.go
@@ -0,0 +1,4 @@
+// Package base provides information and interaction with the base API
+// resource in the OpenStack CDN service. This API resource allows for
+// retrieving the Home Document and pinging the root URL.
+package base
diff --git a/openstack/cdn/v1/base/fixtures.go b/openstack/cdn/v1/base/fixtures.go
new file mode 100644
index 0000000..19b5ece
--- /dev/null
+++ b/openstack/cdn/v1/base/fixtures.go
@@ -0,0 +1,53 @@
+package base
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleGetSuccessfully creates an HTTP handler at `/` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "resources": {
+ "rel/cdn": {
+ "href-template": "services{?marker,limit}",
+ "href-vars": {
+ "marker": "param/marker",
+ "limit": "param/limit"
+ },
+ "hints": {
+ "allow": [
+ "GET"
+ ],
+ "formats": {
+ "application/json": {}
+ }
+ }
+ }
+ }
+ }
+ `)
+
+ })
+}
+
+// HandlePingSuccessfully creates an HTTP handler at `/ping` on the test handler
+// mux that responds with a `Ping` response.
+func HandlePingSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
diff --git a/openstack/cdn/v1/base/requests.go b/openstack/cdn/v1/base/requests.go
new file mode 100644
index 0000000..9d8632c
--- /dev/null
+++ b/openstack/cdn/v1/base/requests.go
@@ -0,0 +1,30 @@
+package base
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ "github.com/racker/perigee"
+)
+
+// Get retrieves the home document, allowing the user to discover the
+// entire API.
+func Get(c *gophercloud.ServiceClient) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Ping retrieves a ping to the server.
+func Ping(c *gophercloud.ServiceClient) PingResult {
+ var res PingResult
+ _, res.Err = perigee.Request("GET", pingURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ OmitAccept: true,
+ })
+ return res
+}
diff --git a/openstack/cdn/v1/base/requests_test.go b/openstack/cdn/v1/base/requests_test.go
new file mode 100644
index 0000000..a8d95f9
--- /dev/null
+++ b/openstack/cdn/v1/base/requests_test.go
@@ -0,0 +1,43 @@
+package base
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetHomeDocument(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t)
+
+ actual, err := Get(fake.ServiceClient()).Extract()
+ th.CheckNoErr(t, err)
+
+ expected := HomeDocument{
+ "rel/cdn": map[string]interface{}{
+ "href-template": "services{?marker,limit}",
+ "href-vars": map[string]interface{}{
+ "marker": "param/marker",
+ "limit": "param/limit",
+ },
+ "hints": map[string]interface{}{
+ "allow": []string{"GET"},
+ "formats": map[string]interface{}{
+ "application/json": map[string]interface{}{},
+ },
+ },
+ },
+ }
+ th.CheckDeepEquals(t, expected, *actual)
+}
+
+func TestPing(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePingSuccessfully(t)
+
+ err := Ping(fake.ServiceClient()).ExtractErr()
+ th.CheckNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/base/results.go b/openstack/cdn/v1/base/results.go
new file mode 100644
index 0000000..bef1da8
--- /dev/null
+++ b/openstack/cdn/v1/base/results.go
@@ -0,0 +1,35 @@
+package base
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// HomeDocument is a resource that contains all the resources for the CDN API.
+type HomeDocument map[string]interface{}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a home document resource.
+func (r GetResult) Extract() (*HomeDocument, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ submap, ok := r.Body.(map[string]interface{})["resources"]
+ if !ok {
+ return nil, errors.New("Unexpected HomeDocument structure")
+ }
+ casted := HomeDocument(submap.(map[string]interface{}))
+
+ return &casted, nil
+}
+
+// PingResult represents the result of a Ping operation.
+type PingResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/base/urls.go b/openstack/cdn/v1/base/urls.go
new file mode 100644
index 0000000..a95e18b
--- /dev/null
+++ b/openstack/cdn/v1/base/urls.go
@@ -0,0 +1,11 @@
+package base
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL()
+}
+
+func pingURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("ping")
+}
diff --git a/openstack/cdn/v1/flavors/doc.go b/openstack/cdn/v1/flavors/doc.go
new file mode 100644
index 0000000..d406698
--- /dev/null
+++ b/openstack/cdn/v1/flavors/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the flavors API
+// resource in the OpenStack CDN service. This API resource allows for
+// listing flavors and retrieving a specific flavor.
+//
+// A flavor is a mapping configuration to a CDN provider.
+package flavors
diff --git a/openstack/cdn/v1/flavors/fixtures.go b/openstack/cdn/v1/flavors/fixtures.go
new file mode 100644
index 0000000..b86c760
--- /dev/null
+++ b/openstack/cdn/v1/flavors/fixtures.go
@@ -0,0 +1,82 @@
+package flavors
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleListCDNFlavorsSuccessfully creates an HTTP handler at `/flavors` on the test handler mux
+// that responds with a `List` response.
+func HandleListCDNFlavorsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/flavors", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "flavors": [
+ {
+ "id": "europe",
+ "providers": [
+ {
+ "provider": "Fastly",
+ "links": [
+ {
+ "href": "http: //www.fastly.com",
+ "rel": "provider_url"
+ }
+ ]
+ }
+ ],
+ "links": [
+ {
+ "href": "https://www.poppycdn.io/v1.0/flavors/europe",
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+ }
+ `)
+ })
+}
+
+// HandleGetCDNFlavorSuccessfully creates an HTTP handler at `/flavors/{id}` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetCDNFlavorSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/flavors/asia", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "id" : "asia",
+ "providers" : [
+ {
+ "provider" : "ChinaCache",
+ "links": [
+ {
+ "href": "http://www.chinacache.com",
+ "rel": "provider_url"
+ }
+ ]
+ }
+ ],
+ "links": [
+ {
+ "href": "https://www.poppycdn.io/v1.0/flavors/asia",
+ "rel": "self"
+ }
+ ]
+ }
+ `)
+ })
+}
diff --git a/openstack/cdn/v1/flavors/requests.go b/openstack/cdn/v1/flavors/requests.go
new file mode 100644
index 0000000..88ac891
--- /dev/null
+++ b/openstack/cdn/v1/flavors/requests.go
@@ -0,0 +1,27 @@
+package flavors
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a single page of CDN flavors.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ url := listURL(c)
+ createPage := func(r pagination.PageResult) pagination.Page {
+ return FlavorPage{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(c, url, createPage)
+}
+
+// Get retrieves a specific flavor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
diff --git a/openstack/cdn/v1/flavors/requests_test.go b/openstack/cdn/v1/flavors/requests_test.go
new file mode 100644
index 0000000..2fafb36
--- /dev/null
+++ b/openstack/cdn/v1/flavors/requests_test.go
@@ -0,0 +1,93 @@
+package flavors
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleListCDNFlavorsSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractFlavors(page)
+ if err != nil {
+ t.Errorf("Failed to extract flavors: %v", err)
+ return false, err
+ }
+
+ expected := []Flavor{
+ Flavor{
+ ID: "europe",
+ Providers: []Provider{
+ Provider{
+ Provider: "Fastly",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http: //www.fastly.com",
+ Rel: "provider_url",
+ },
+ },
+ },
+ },
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+ Rel: "self",
+ },
+ },
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleGetCDNFlavorSuccessfully(t)
+
+ expected := &Flavor{
+ ID: "asia",
+ Providers: []Provider{
+ Provider{
+ Provider: "ChinaCache",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://www.chinacache.com",
+ Rel: "provider_url",
+ },
+ },
+ },
+ },
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+ Rel: "self",
+ },
+ },
+ }
+
+
+ actual, err := Get(fake.ServiceClient(), "asia").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/cdn/v1/flavors/results.go b/openstack/cdn/v1/flavors/results.go
new file mode 100644
index 0000000..8cab48b
--- /dev/null
+++ b/openstack/cdn/v1/flavors/results.go
@@ -0,0 +1,71 @@
+package flavors
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Provider represents a provider for a particular flavor.
+type Provider struct {
+ // Specifies the name of the provider. The name must not exceed 64 bytes in
+ // length and is limited to unicode, digits, underscores, and hyphens.
+ Provider string `mapstructure:"provider"`
+ // Specifies a list with an href where rel is provider_url.
+ Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// Flavor represents a mapping configuration to a CDN provider.
+type Flavor struct {
+ // Specifies the name of the flavor. The name must not exceed 64 bytes in
+ // length and is limited to unicode, digits, underscores, and hyphens.
+ ID string `mapstructure:"id"`
+ // Specifies the list of providers mapped to this flavor.
+ Providers []Provider `mapstructure:"providers"`
+ // Specifies the self-navigating JSON document paths.
+ Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// FlavorPage is the page returned by a pager when traversing over a
+// collection of CDN flavors.
+type FlavorPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a FlavorPage contains no Flavors.
+func (r FlavorPage) IsEmpty() (bool, error) {
+ flavors, err := ExtractFlavors(r)
+ if err != nil {
+ return true, err
+ }
+ return len(flavors) == 0, nil
+}
+
+// ExtractFlavors extracts and returns Flavors. It is used while iterating over
+// a flavors.List call.
+func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
+ var response struct {
+ Flavors []Flavor `json:"flavors"`
+ }
+
+ err := mapstructure.Decode(page.(FlavorPage).Body, &response)
+ return response.Flavors, err
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that extracts a flavor from a GetResult.
+func (r GetResult) Extract() (*Flavor, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res Flavor
+
+ err := mapstructure.Decode(r.Body, &res)
+
+ return &res, err
+}
diff --git a/openstack/cdn/v1/flavors/urls.go b/openstack/cdn/v1/flavors/urls.go
new file mode 100644
index 0000000..6eb38d2
--- /dev/null
+++ b/openstack/cdn/v1/flavors/urls.go
@@ -0,0 +1,11 @@
+package flavors
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("flavors")
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("flavors", id)
+}
diff --git a/openstack/cdn/v1/serviceassets/doc.go b/openstack/cdn/v1/serviceassets/doc.go
new file mode 100644
index 0000000..ceecaa5
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/doc.go
@@ -0,0 +1,7 @@
+// Package serviceassets provides information and interaction with the
+// serviceassets API resource in the OpenStack CDN service. This API resource
+// allows for deleting cached assets.
+//
+// A service distributes assets across the network. Service assets let you
+// interrogate properties about these assets and perform certain actions on them.
+package serviceassets
diff --git a/openstack/cdn/v1/serviceassets/fixtures.go b/openstack/cdn/v1/serviceassets/fixtures.go
new file mode 100644
index 0000000..38e7fc5
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/fixtures.go
@@ -0,0 +1,19 @@
+package serviceassets
+
+import (
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleDeleteCDNAssetSuccessfully creates an HTTP handler at `/services/{id}/assets` on the test handler mux
+// that responds with a `Delete` response.
+func HandleDeleteCDNAssetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0/assets", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/openstack/cdn/v1/serviceassets/requests.go b/openstack/cdn/v1/serviceassets/requests.go
new file mode 100644
index 0000000..5248ef2
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/requests.go
@@ -0,0 +1,52 @@
+package serviceassets
+
+import (
+ "strings"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// DeleteOptsBuilder allows extensions to add additional parameters to the Delete
+// request.
+type DeleteOptsBuilder interface {
+ ToCDNAssetDeleteParams() (string, error)
+}
+
+// DeleteOpts is a structure that holds options for deleting CDN service assets.
+type DeleteOpts struct {
+ // If all is set to true, specifies that the delete occurs against all of the
+ // assets for the service.
+ All bool `q:"all"`
+ // Specifies the relative URL of the asset to be deleted.
+ URL string `q:"url"`
+}
+
+// ToCDNAssetDeleteParams formats a DeleteOpts into a query string.
+func (opts DeleteOpts) ToCDNAssetDeleteParams() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// Delete accepts a unique service ID or URL and deletes the CDN service asset associated with
+// it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Delete(c *gophercloud.ServiceClient, idOrURL string, opts DeleteOptsBuilder) DeleteResult {
+ var url string
+ if strings.Contains(idOrURL, "/") {
+ url = idOrURL
+ } else {
+ url = deleteURL(c, idOrURL)
+ }
+
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
diff --git a/openstack/cdn/v1/serviceassets/requests_test.go b/openstack/cdn/v1/serviceassets/requests_test.go
new file mode 100644
index 0000000..32896ee
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/requests_test.go
@@ -0,0 +1,18 @@
+package serviceassets
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleDeleteCDNAssetSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/serviceassets/results.go b/openstack/cdn/v1/serviceassets/results.go
new file mode 100644
index 0000000..1d8734b
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/results.go
@@ -0,0 +1,8 @@
+package serviceassets
+
+import "github.com/rackspace/gophercloud"
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/serviceassets/urls.go b/openstack/cdn/v1/serviceassets/urls.go
new file mode 100644
index 0000000..cb0aea8
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/urls.go
@@ -0,0 +1,7 @@
+package serviceassets
+
+import "github.com/rackspace/gophercloud"
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("services", id, "assets")
+}
diff --git a/openstack/cdn/v1/services/doc.go b/openstack/cdn/v1/services/doc.go
new file mode 100644
index 0000000..41f7c60
--- /dev/null
+++ b/openstack/cdn/v1/services/doc.go
@@ -0,0 +1,7 @@
+// Package services provides information and interaction with the services API
+// resource in the OpenStack CDN service. This API resource allows for
+// listing, creating, updating, retrieving, and deleting services.
+//
+// A service represents an application that has its content cached to the edge
+// nodes.
+package services
diff --git a/openstack/cdn/v1/services/errors.go b/openstack/cdn/v1/services/errors.go
new file mode 100644
index 0000000..359584c
--- /dev/null
+++ b/openstack/cdn/v1/services/errors.go
@@ -0,0 +1,7 @@
+package services
+
+import "fmt"
+
+func no(str string) error {
+ return fmt.Errorf("Required parameter %s not provided", str)
+}
diff --git a/openstack/cdn/v1/services/fixtures.go b/openstack/cdn/v1/services/fixtures.go
new file mode 100644
index 0000000..ec1fd52
--- /dev/null
+++ b/openstack/cdn/v1/services/fixtures.go
@@ -0,0 +1,342 @@
+package services
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleListCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
+// that responds with a `List` response.
+func HandleListCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `
+ {
+ "links": [
+ {
+ "rel": "next",
+ "href": "https://www.poppycdn.io/v1.0/services?marker=96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0&limit=20"
+ }
+ ],
+ "services": [
+ {
+ "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ "name": "mywebsite.com",
+ "domains": [
+ {
+ "domain": "www.mywebsite.com"
+ }
+ ],
+ "origins": [
+ {
+ "origin": "mywebsite.com",
+ "port": 80,
+ "ssl": false
+ }
+ ],
+ "caching": [
+ {
+ "name": "default",
+ "ttl": 3600
+ },
+ {
+ "name": "home",
+ "ttl": 17200,
+ "rules": [
+ {
+ "name": "index",
+ "request_url": "/index.htm"
+ }
+ ]
+ },
+ {
+ "name": "images",
+ "ttl": 12800,
+ "rules": [
+ {
+ "name": "images",
+ "request_url": "*.png"
+ }
+ ]
+ }
+ ],
+ "restrictions": [
+ {
+ "name": "website only",
+ "rules": [
+ {
+ "name": "mywebsite.com",
+ "referrer": "www.mywebsite.com"
+ }
+ ]
+ }
+ ],
+ "flavor_id": "asia",
+ "status": "deployed",
+ "errors" : [],
+ "links": [
+ {
+ "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ "rel": "self"
+ },
+ {
+ "href": "mywebsite.com.cdn123.poppycdn.net",
+ "rel": "access_url"
+ },
+ {
+ "href": "https://www.poppycdn.io/v1.0/flavors/asia",
+ "rel": "flavor"
+ }
+ ]
+ },
+ {
+ "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ "name": "myothersite.com",
+ "domains": [
+ {
+ "domain": "www.myothersite.com"
+ }
+ ],
+ "origins": [
+ {
+ "origin": "44.33.22.11",
+ "port": 80,
+ "ssl": false
+ },
+ {
+ "origin": "77.66.55.44",
+ "port": 80,
+ "ssl": false,
+ "rules": [
+ {
+ "name": "videos",
+ "request_url": "^/videos/*.m3u"
+ }
+ ]
+ }
+ ],
+ "caching": [
+ {
+ "name": "default",
+ "ttl": 3600
+ }
+ ],
+ "restrictions": [
+ {}
+ ],
+ "flavor_id": "europe",
+ "status": "deployed",
+ "links": [
+ {
+ "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ "rel": "self"
+ },
+ {
+ "href": "myothersite.com.poppycdn.net",
+ "rel": "access_url"
+ },
+ {
+ "href": "https://www.poppycdn.io/v1.0/flavors/europe",
+ "rel": "flavor"
+ }
+ ]
+ }
+ ]
+ }
+ `)
+ case "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1":
+ fmt.Fprintf(w, `{
+ "services": []
+ }`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+}
+
+// HandleCreateCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
+// that responds with a `Create` response.
+func HandleCreateCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, `
+ {
+ "name": "mywebsite.com",
+ "domains": [
+ {
+ "domain": "www.mywebsite.com"
+ },
+ {
+ "domain": "blog.mywebsite.com"
+ }
+ ],
+ "origins": [
+ {
+ "origin": "mywebsite.com",
+ "port": 80,
+ "ssl": false
+ }
+ ],
+ "restrictions": [
+ {
+ "name": "website only",
+ "rules": [
+ {
+ "name": "mywebsite.com",
+ "referrer": "www.mywebsite.com"
+ }
+ ]
+ }
+ ],
+ "caching": [
+ {
+ "name": "default",
+ "ttl": 3600
+ }
+ ],
+
+ "flavor_id": "cdn"
+ }
+ `)
+ w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+// HandleGetCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ "name": "mywebsite.com",
+ "domains": [
+ {
+ "domain": "www.mywebsite.com",
+ "protocol": "http"
+ }
+ ],
+ "origins": [
+ {
+ "origin": "mywebsite.com",
+ "port": 80,
+ "ssl": false
+ }
+ ],
+ "caching": [
+ {
+ "name": "default",
+ "ttl": 3600
+ },
+ {
+ "name": "home",
+ "ttl": 17200,
+ "rules": [
+ {
+ "name": "index",
+ "request_url": "/index.htm"
+ }
+ ]
+ },
+ {
+ "name": "images",
+ "ttl": 12800,
+ "rules": [
+ {
+ "name": "images",
+ "request_url": "*.png"
+ }
+ ]
+ }
+ ],
+ "restrictions": [
+ {
+ "name": "website only",
+ "rules": [
+ {
+ "name": "mywebsite.com",
+ "referrer": "www.mywebsite.com"
+ }
+ ]
+ }
+ ],
+ "flavor_id": "cdn",
+ "status": "deployed",
+ "errors" : [],
+ "links": [
+ {
+ "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ "rel": "self"
+ },
+ {
+ "href": "blog.mywebsite.com.cdn1.raxcdn.com",
+ "rel": "access_url"
+ },
+ {
+ "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+ "rel": "flavor"
+ }
+ ]
+ }
+ `)
+ })
+}
+
+// HandleUpdateCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Update` response.
+func HandleUpdateCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PATCH")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, `
+ [
+ {
+ "op": "replace",
+ "path": "/origins/0",
+ "value": {
+ "origin": "44.33.22.11",
+ "port": 80,
+ "ssl": false
+ }
+ },
+ {
+ "op": "add",
+ "path": "/domains/0",
+ "value": {"domain": "added.mocksite4.com"}
+ }
+ ]
+ `)
+ w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+// HandleDeleteCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Delete` response.
+func HandleDeleteCDNServiceSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/openstack/cdn/v1/services/requests.go b/openstack/cdn/v1/services/requests.go
new file mode 100644
index 0000000..f88df19
--- /dev/null
+++ b/openstack/cdn/v1/services/requests.go
@@ -0,0 +1,319 @@
+package services
+
+import (
+ "strings"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToCDNServiceListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+}
+
+// ToCDNServiceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToCDNServiceListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// CDN services. It accepts a ListOpts struct, which allows for pagination via
+// marker and limit.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToCDNServiceListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPage := func(r pagination.PageResult) pagination.Page {
+ p := ServicePage{pagination.MarkerPageBase{PageResult: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ pager := pagination.NewPager(c, url, createPage)
+ return pager
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToCDNServiceCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // REQUIRED. Specifies the name of the service. The minimum length for name is
+ // 3. The maximum length is 256.
+ Name string
+ // REQUIRED. Specifies a list of domains used by users to access their website.
+ Domains []Domain
+ // REQUIRED. Specifies a list of origin domains or IP addresses where the
+ // original assets are stored.
+ Origins []Origin
+ // REQUIRED. Specifies the CDN provider flavor ID to use. For a list of
+ // flavors, see the operation to list the available flavors. The minimum
+ // length for flavor_id is 1. The maximum length is 256.
+ FlavorID string
+ // OPTIONAL. Specifies the TTL rules for the assets under this service. Supports wildcards for fine-grained control.
+ Caching []CacheRule
+ // OPTIONAL. Specifies the restrictions that define who can access assets (content from the CDN cache).
+ Restrictions []Restriction
+}
+
+// ToCDNServiceCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToCDNServiceCreateMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.Name == "" {
+ return nil, no("Name")
+ }
+ s["name"] = opts.Name
+
+ if opts.Domains == nil {
+ return nil, no("Domains")
+ }
+ for _, domain := range opts.Domains {
+ if domain.Domain == "" {
+ return nil, no("Domains[].Domain")
+ }
+ }
+ s["domains"] = opts.Domains
+
+ if opts.Origins == nil {
+ return nil, no("Origins")
+ }
+ for _, origin := range opts.Origins {
+ if origin.Origin == "" {
+ return nil, no("Origins[].Origin")
+ }
+ if origin.Rules == nil && len(opts.Origins) > 1 {
+ return nil, no("Origins[].Rules")
+ }
+ for _, rule := range origin.Rules {
+ if rule.Name == "" {
+ return nil, no("Origins[].Rules[].Name")
+ }
+ if rule.RequestURL == "" {
+ return nil, no("Origins[].Rules[].RequestURL")
+ }
+ }
+ }
+ s["origins"] = opts.Origins
+
+ if opts.FlavorID == "" {
+ return nil, no("FlavorID")
+ }
+ s["flavor_id"] = opts.FlavorID
+
+ if opts.Caching != nil {
+ for _, cache := range opts.Caching {
+ if cache.Name == "" {
+ return nil, no("Caching[].Name")
+ }
+ if cache.Rules != nil {
+ for _, rule := range cache.Rules {
+ if rule.Name == "" {
+ return nil, no("Caching[].Rules[].Name")
+ }
+ if rule.RequestURL == "" {
+ return nil, no("Caching[].Rules[].RequestURL")
+ }
+ }
+ }
+ }
+ s["caching"] = opts.Caching
+ }
+
+ if opts.Restrictions != nil {
+ for _, restriction := range opts.Restrictions {
+ if restriction.Name == "" {
+ return nil, no("Restrictions[].Name")
+ }
+ if restriction.Rules != nil {
+ for _, rule := range restriction.Rules {
+ if rule.Name == "" {
+ return nil, no("Restrictions[].Rules[].Name")
+ }
+ }
+ }
+ }
+ s["restrictions"] = opts.Restrictions
+ }
+
+ return s, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new CDN service using the
+// values provided.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToCDNServiceCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ resp, err := perigee.Request("POST", createURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+ res.Header = resp.HttpResponse.Header
+ res.Err = err
+ return res
+}
+
+// Get retrieves a specific service based on its URL or its unique ID. For
+// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Get(c *gophercloud.ServiceClient, idOrURL string) GetResult {
+ var url string
+ if strings.Contains(idOrURL, "/") {
+ url = idOrURL
+ } else {
+ url = getURL(c, idOrURL)
+ }
+
+ var res GetResult
+ _, res.Err = perigee.Request("GET", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+ ToCDNServiceUpdateMap() ([]map[string]interface{}, error)
+}
+
+// Op represents an update operation.
+type Op string
+
+var (
+ // Add is a constant used for performing a "add" operation when updating.
+ Add Op = "add"
+ // Remove is a constant used for performing a "remove" operation when updating.
+ Remove Op = "remove"
+ // Replace is a constant used for performing a "replace" operation when updating.
+ Replace Op = "replace"
+)
+
+// UpdateOpts represents the attributes used when updating an existing CDN service.
+type UpdateOpts []UpdateOpt
+
+// UpdateOpt represents a single update to an existing service. Multiple updates
+// to a service can be submitted at the same time. See UpdateOpts.
+type UpdateOpt struct {
+ // Specifies the update operation to perform.
+ Op Op `json:"op"`
+ // Specifies the JSON Pointer location within the service's JSON representation
+ // of the service parameter being added, replaced or removed.
+ Path string `json:"path"`
+ // Specifies the actual value to be added or replaced. It is not required for
+ // the remove operation.
+ Value map[string]interface{} `json:"value,omitempty"`
+}
+
+// ToCDNServiceUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToCDNServiceUpdateMap() ([]map[string]interface{}, error) {
+ s := make([]map[string]interface{}, len(opts))
+
+ for i, opt := range opts {
+ if opt.Op == "" {
+ return nil, no("Op")
+ }
+ if opt.Path == "" {
+ return nil, no("Path")
+ }
+ if opt.Op != Remove && opt.Value == nil {
+ return nil, no("Value")
+ }
+ s[i] = map[string]interface{}{
+ "op": opt.Op,
+ "path": opt.Path,
+ "value": opt.Value,
+ }
+ }
+
+ return s, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing CDN service using
+// the values provided. idOrURL can be either the service's URL or its ID. For
+// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Update(c *gophercloud.ServiceClient, idOrURL string, opts UpdateOptsBuilder) UpdateResult {
+ var url string
+ if strings.Contains(idOrURL, "/") {
+ url = idOrURL
+ } else {
+ url = updateURL(c, idOrURL)
+ }
+
+ var res UpdateResult
+ reqBody, err := opts.ToCDNServiceUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ resp, err := perigee.Request("PATCH", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+ res.Header = resp.HttpResponse.Header
+ res.Err = err
+ return res
+}
+
+// Delete accepts a service's ID or its URL and deletes the CDN service
+// associated with it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Delete(c *gophercloud.ServiceClient, idOrURL string) DeleteResult {
+ var url string
+ if strings.Contains(idOrURL, "/") {
+ url = idOrURL
+ } else {
+ url = deleteURL(c, idOrURL)
+ }
+
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
diff --git a/openstack/cdn/v1/services/requests_test.go b/openstack/cdn/v1/services/requests_test.go
new file mode 100644
index 0000000..d06afcb
--- /dev/null
+++ b/openstack/cdn/v1/services/requests_test.go
@@ -0,0 +1,333 @@
+package services
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleListCDNServiceSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractServices(page)
+ if err != nil {
+ t.Errorf("Failed to extract services: %v", err)
+ return false, err
+ }
+
+ expected := []Service{
+ Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Name: "mywebsite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.mywebsite.com",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ CacheRule{
+ Name: "home",
+ TTL: 17200,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "index",
+ RequestURL: "/index.htm",
+ },
+ },
+ },
+ CacheRule{
+ Name: "images",
+ TTL: 12800,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "images",
+ RequestURL: "*.png",
+ },
+ },
+ },
+ },
+ Restrictions: []Restriction{
+ Restriction{
+ Name: "website only",
+ Rules: []RestrictionRule{
+ RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ FlavorID: "asia",
+ Status: "deployed",
+ Errors: []Error{},
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "mywebsite.com.cdn123.poppycdn.net",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+ Rel: "flavor",
+ },
+ },
+ },
+ Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ Name: "myothersite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.myothersite.com",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "44.33.22.11",
+ Port: 80,
+ SSL: false,
+ },
+ Origin{
+ Origin: "77.66.55.44",
+ Port: 80,
+ SSL: false,
+ Rules: []OriginRule{
+ OriginRule{
+ Name: "videos",
+ RequestURL: "^/videos/*.m3u",
+ },
+ },
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ },
+ Restrictions: []Restriction{},
+ FlavorID: "europe",
+ Status: "deployed",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "myothersite.com.poppycdn.net",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+ Rel: "flavor",
+ },
+ },
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleCreateCDNServiceSuccessfully(t)
+
+ createOpts := CreateOpts{
+ Name: "mywebsite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.mywebsite.com",
+ },
+ Domain{
+ Domain: "blog.mywebsite.com",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Restrictions: []Restriction{
+ Restriction{
+ Name: "website only",
+ Rules: []RestrictionRule{
+ RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ },
+ FlavorID: "cdn",
+ }
+
+ expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleGetCDNServiceSuccessfully(t)
+
+ expected := &Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Name: "mywebsite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.mywebsite.com",
+ Protocol: "http",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ CacheRule{
+ Name: "home",
+ TTL: 17200,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "index",
+ RequestURL: "/index.htm",
+ },
+ },
+ },
+ CacheRule{
+ Name: "images",
+ TTL: 12800,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "images",
+ RequestURL: "*.png",
+ },
+ },
+ },
+ },
+ Restrictions: []Restriction{
+ Restriction{
+ Name: "website only",
+ Rules: []RestrictionRule{
+ RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ FlavorID: "cdn",
+ Status: "deployed",
+ Errors: []Error{},
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+ Rel: "flavor",
+ },
+ },
+ }
+
+
+ actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleUpdateCDNServiceSuccessfully(t)
+
+ expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ updateOpts := UpdateOpts{
+ UpdateOpt{
+ Op: Replace,
+ Path: "/origins/0",
+ Value: map[string]interface{}{
+ "origin": "44.33.22.11",
+ "port": 80,
+ "ssl": false,
+ },
+ },
+ UpdateOpt{
+ Op: Add,
+ Path: "/domains/0",
+ Value: map[string]interface{}{
+ "domain": "added.mocksite4.com",
+ },
+ },
+ }
+ actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleDeleteCDNServiceSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/services/results.go b/openstack/cdn/v1/services/results.go
new file mode 100644
index 0000000..c64509b
--- /dev/null
+++ b/openstack/cdn/v1/services/results.go
@@ -0,0 +1,197 @@
+package services
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Domain represents a domain used by users to access their website.
+type Domain struct {
+ // Specifies the domain used to access the assets on their website, for which
+ // a CNAME is given to the CDN provider.
+ Domain string `mapstructure:"domain" json:"domain"`
+ // Specifies the protocol used to access the assets on this domain. Only "http"
+ // or "https" are currently allowed. The default is "http".
+ Protocol string `mapstructure:"protocol" json:"protocol,omitempty"`
+}
+
+// OriginRule represents a rule that defines when an origin should be accessed.
+type OriginRule struct {
+ // Specifies the name of this rule.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies the request URL this rule should match for this origin to be used. Regex is supported.
+ RequestURL string `mapstructure:"request_url" json:"request_url"`
+}
+
+// Origin specifies a list of origin domains or IP addresses where the original assets are stored.
+type Origin struct {
+ // Specifies the URL or IP address to pull origin content from.
+ Origin string `mapstructure:"origin" json:"origin"`
+ // Specifies the port used to access the origin. The default is port 80.
+ Port int `mapstructure:"port" json:"port,omitempty"`
+ // Specifies whether or not to use HTTPS to access the origin. The default
+ // is false.
+ SSL bool `mapstructure:"ssl" json:"ssl"`
+ // Specifies a collection of rules that define the conditions when this origin
+ // should be accessed. If there is more than one origin, the rules parameter is required.
+ Rules []OriginRule `mapstructure:"rules" json:"rules,omitempty"`
+}
+
+// TTLRule specifies a rule that determines if a TTL should be applied to an asset.
+type TTLRule struct {
+ // Specifies the name of this rule.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies the request URL this rule should match for this TTL to be used. Regex is supported.
+ RequestURL string `mapstructure:"request_url" json:"request_url"`
+}
+
+// CacheRule specifies the TTL rules for the assets under this service.
+type CacheRule struct {
+ // Specifies the name of this caching rule. Note: 'default' is a reserved name used for the default TTL setting.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies the TTL to apply.
+ TTL int `mapstructure:"ttl" json:"ttl"`
+ // Specifies a collection of rules that determine if this TTL should be applied to an asset.
+ Rules []TTLRule `mapstructure:"rules" json:"rules,omitempty"`
+}
+
+// RestrictionRule specifies a rule that determines if this restriction should be applied to an asset.
+type RestrictionRule struct {
+ // Specifies the name of this rule.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies the http host that requests must come from.
+ Referrer string `mapstructure:"referrer" json:"referrer,omitempty"`
+}
+
+// Restriction specifies a restriction that defines who can access assets (content from the CDN cache).
+type Restriction struct {
+ // Specifies the name of this restriction.
+ Name string `mapstructure:"name" json:"name"`
+ // Specifies a collection of rules that determine if this TTL should be applied to an asset.
+ Rules []RestrictionRule `mapstructure:"rules" json:"rules"`
+}
+
+// Error specifies an error that occurred during the previous service action.
+type Error struct {
+ // Specifies an error message detailing why there is an error.
+ Message string `mapstructure:"message"`
+}
+
+// Service represents a CDN service resource.
+type Service struct {
+ // Specifies the service ID that represents distributed content. The value is
+ // a UUID, such as 96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0, that is generated by the server.
+ ID string `mapstructure:"id"`
+ // Specifies the name of the service.
+ Name string `mapstructure:"name"`
+ // Specifies a list of domains used by users to access their website.
+ Domains []Domain `mapstructure:"domains"`
+ // Specifies a list of origin domains or IP addresses where the original assets are stored.
+ Origins []Origin `mapstructure:"origins"`
+ // Specifies the TTL rules for the assets under this service. Supports wildcards for fine grained control.
+ Caching []CacheRule `mapstructure:"caching"`
+ // Specifies the restrictions that define who can access assets (content from the CDN cache).
+ Restrictions []Restriction `mapstructure:"restrictions" json:"restrictions,omitempty"`
+ // Specifies the CDN provider flavor ID to use. For a list of flavors, see the operation to list the available flavors.
+ FlavorID string `mapstructure:"flavor_id"`
+ // Specifies the current status of the service.
+ Status string `mapstructure:"status"`
+ // Specifies the list of errors that occurred during the previous service action.
+ Errors []Error `mapstructure:"errors"`
+ // Specifies the self-navigating JSON document paths.
+ Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// ServicePage is the page returned by a pager when traversing over a
+// collection of CDN services.
+type ServicePage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no services.
+func (r ServicePage) IsEmpty() (bool, error) {
+ services, err := ExtractServices(r)
+ if err != nil {
+ return true, err
+ }
+ return len(services) == 0, nil
+}
+
+// LastMarker returns the last service in a ListResult.
+func (r ServicePage) LastMarker() (string, error) {
+ services, err := ExtractServices(r)
+ if err != nil {
+ return "", err
+ }
+ if len(services) == 0 {
+ return "", nil
+ }
+ return (services[len(services)-1]).ID, nil
+}
+
+// ExtractServices is a function that takes a ListResult and returns the services' information.
+func ExtractServices(page pagination.Page) ([]Service, error) {
+ var response struct {
+ Services []Service `mapstructure:"services"`
+ }
+
+ err := mapstructure.Decode(page.(ServicePage).Body, &response)
+ return response.Services, err
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+ gophercloud.Result
+}
+
+// Extract is a method that extracts the location of a newly created service.
+func (r CreateResult) Extract() (string, error) {
+ if r.Err != nil {
+ return "", r.Err
+ }
+ if l, ok := r.Header["Location"]; ok && len(l) > 0 {
+ return l[0], nil
+ }
+ return "", nil
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that extracts a service from a GetResult.
+func (r GetResult) Extract() (*Service, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res Service
+
+ err := mapstructure.Decode(r.Body, &res)
+
+ return &res, err
+}
+
+// UpdateResult represents the result of a Update operation.
+type UpdateResult struct {
+ gophercloud.Result
+}
+
+// Extract is a method that extracts the location of an updated service.
+func (r UpdateResult) Extract() (string, error) {
+ if r.Err != nil {
+ return "", r.Err
+ }
+ if l, ok := r.Header["Location"]; ok && len(l) > 0 {
+ return l[0], nil
+ }
+ return "", nil
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/services/urls.go b/openstack/cdn/v1/services/urls.go
new file mode 100644
index 0000000..d953d4c
--- /dev/null
+++ b/openstack/cdn/v1/services/urls.go
@@ -0,0 +1,23 @@
+package services
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("services")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return listURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("services", id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return getURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return getURL(c, id)
+}
diff --git a/openstack/client.go b/openstack/client.go
index 99b3d46..9c12dca 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -203,3 +203,14 @@
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
+
+// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1
+// CDN service.
+func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("cdn")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/rackspace/cdn/v1/base/delegate.go b/rackspace/cdn/v1/base/delegate.go
new file mode 100644
index 0000000..5af7e07
--- /dev/null
+++ b/rackspace/cdn/v1/base/delegate.go
@@ -0,0 +1,18 @@
+package base
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/base"
+)
+
+// Get retrieves the home document, allowing the user to discover the
+// entire API.
+func Get(c *gophercloud.ServiceClient) os.GetResult {
+ return os.Get(c)
+}
+
+// Ping retrieves a ping to the server.
+func Ping(c *gophercloud.ServiceClient) os.PingResult {
+ return os.Ping(c)
+}
diff --git a/rackspace/cdn/v1/base/delegate_test.go b/rackspace/cdn/v1/base/delegate_test.go
new file mode 100644
index 0000000..3c05801
--- /dev/null
+++ b/rackspace/cdn/v1/base/delegate_test.go
@@ -0,0 +1,44 @@
+package base
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/base"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetHomeDocument(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleGetSuccessfully(t)
+
+ actual, err := Get(fake.ServiceClient()).Extract()
+ th.CheckNoErr(t, err)
+
+ expected := os.HomeDocument{
+ "rel/cdn": map[string]interface{}{
+ "href-template": "services{?marker,limit}",
+ "href-vars": map[string]interface{}{
+ "marker": "param/marker",
+ "limit": "param/limit",
+ },
+ "hints": map[string]interface{}{
+ "allow": []string{"GET"},
+ "formats": map[string]interface{}{
+ "application/json": map[string]interface{}{},
+ },
+ },
+ },
+ }
+ th.CheckDeepEquals(t, expected, *actual)
+}
+
+func TestPing(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandlePingSuccessfully(t)
+
+ err := Ping(fake.ServiceClient()).ExtractErr()
+ th.CheckNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/base/doc.go b/rackspace/cdn/v1/base/doc.go
new file mode 100644
index 0000000..5582306
--- /dev/null
+++ b/rackspace/cdn/v1/base/doc.go
@@ -0,0 +1,4 @@
+// Package base provides information and interaction with the base API
+// resource in the Rackspace CDN service. This API resource allows for
+// retrieving the Home Document and pinging the root URL.
+package base
diff --git a/rackspace/cdn/v1/flavors/delegate.go b/rackspace/cdn/v1/flavors/delegate.go
new file mode 100644
index 0000000..7152fa2
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/delegate.go
@@ -0,0 +1,18 @@
+package flavors
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a single page of CDN flavors.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return os.List(c)
+}
+
+// Get retrieves a specific flavor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+ return os.Get(c, id)
+}
diff --git a/rackspace/cdn/v1/flavors/delegate_test.go b/rackspace/cdn/v1/flavors/delegate_test.go
new file mode 100644
index 0000000..9060e54
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/delegate_test.go
@@ -0,0 +1,93 @@
+package flavors
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleListCDNFlavorsSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractFlavors(page)
+ if err != nil {
+ t.Errorf("Failed to extract flavors: %v", err)
+ return false, err
+ }
+
+ expected := []os.Flavor{
+ os.Flavor{
+ ID: "europe",
+ Providers: []os.Provider{
+ os.Provider{
+ Provider: "Fastly",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http: //www.fastly.com",
+ Rel: "provider_url",
+ },
+ },
+ },
+ },
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+ Rel: "self",
+ },
+ },
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleGetCDNFlavorSuccessfully(t)
+
+ expected := &os.Flavor{
+ ID: "asia",
+ Providers: []os.Provider{
+ os.Provider{
+ Provider: "ChinaCache",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://www.chinacache.com",
+ Rel: "provider_url",
+ },
+ },
+ },
+ },
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+ Rel: "self",
+ },
+ },
+ }
+
+ actual, err := Get(fake.ServiceClient(), "asia").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/cdn/v1/flavors/doc.go b/rackspace/cdn/v1/flavors/doc.go
new file mode 100644
index 0000000..4ad966e
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the flavors API
+// resource in the Rackspace CDN service. This API resource allows for
+// listing flavors and retrieving a specific flavor.
+//
+// A flavor is a mapping configuration to a CDN provider.
+package flavors
diff --git a/rackspace/cdn/v1/serviceassets/delegate.go b/rackspace/cdn/v1/serviceassets/delegate.go
new file mode 100644
index 0000000..07c93a8
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/delegate.go
@@ -0,0 +1,13 @@
+package serviceassets
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+)
+
+// Delete accepts a unique ID and deletes the CDN service asset associated with
+// it.
+func Delete(c *gophercloud.ServiceClient, id string, opts os.DeleteOptsBuilder) os.DeleteResult {
+ return os.Delete(c, id, opts)
+}
diff --git a/rackspace/cdn/v1/serviceassets/delegate_test.go b/rackspace/cdn/v1/serviceassets/delegate_test.go
new file mode 100644
index 0000000..328e168
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/delegate_test.go
@@ -0,0 +1,19 @@
+package serviceassets
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleDeleteCDNAssetSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/serviceassets/doc.go b/rackspace/cdn/v1/serviceassets/doc.go
new file mode 100644
index 0000000..46b3d50
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/doc.go
@@ -0,0 +1,7 @@
+// Package serviceassets provides information and interaction with the
+// serviceassets API resource in the Rackspace CDN service. This API resource
+// allows for deleting cached assets.
+//
+// A service distributes assets across the network. Service assets let you
+// interrogate properties about these assets and perform certain actions on them.
+package serviceassets
diff --git a/rackspace/cdn/v1/services/delegate.go b/rackspace/cdn/v1/services/delegate.go
new file mode 100644
index 0000000..10881eb
--- /dev/null
+++ b/rackspace/cdn/v1/services/delegate.go
@@ -0,0 +1,37 @@
+package services
+
+import (
+ "github.com/rackspace/gophercloud"
+
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// CDN services. It accepts a ListOpts struct, which allows for pagination via
+// marker and limit.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(c, opts)
+}
+
+// Create accepts a CreateOpts struct and creates a new CDN service using the
+// values provided.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(c, opts)
+}
+
+// Get retrieves a specific service based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+ return os.Get(c, id)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing CDN service using
+// the values provided.
+func Update(c *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) os.UpdateResult {
+ return os.Update(c, id, opts)
+}
+
+// Delete accepts a unique ID and deletes the CDN service associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) os.DeleteResult {
+ return os.Delete(c, id)
+}
diff --git a/rackspace/cdn/v1/services/delegate_test.go b/rackspace/cdn/v1/services/delegate_test.go
new file mode 100644
index 0000000..baaa439
--- /dev/null
+++ b/rackspace/cdn/v1/services/delegate_test.go
@@ -0,0 +1,333 @@
+package services
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleListCDNServiceSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient(), &os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractServices(page)
+ if err != nil {
+ t.Errorf("Failed to extract services: %v", err)
+ return false, err
+ }
+
+ expected := []os.Service{
+ os.Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Name: "mywebsite.com",
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www.mywebsite.com",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Caching: []os.CacheRule{
+ os.CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ os.CacheRule{
+ Name: "home",
+ TTL: 17200,
+ Rules: []os.TTLRule{
+ os.TTLRule{
+ Name: "index",
+ RequestURL: "/index.htm",
+ },
+ },
+ },
+ os.CacheRule{
+ Name: "images",
+ TTL: 12800,
+ Rules: []os.TTLRule{
+ os.TTLRule{
+ Name: "images",
+ RequestURL: "*.png",
+ },
+ },
+ },
+ },
+ Restrictions: []os.Restriction{
+ os.Restriction{
+ Name: "website only",
+ Rules: []os.RestrictionRule{
+ os.RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ FlavorID: "asia",
+ Status: "deployed",
+ Errors: []os.Error{},
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "mywebsite.com.cdn123.poppycdn.net",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+ Rel: "flavor",
+ },
+ },
+ },
+ os.Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ Name: "myothersite.com",
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www.myothersite.com",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: "44.33.22.11",
+ Port: 80,
+ SSL: false,
+ },
+ os.Origin{
+ Origin: "77.66.55.44",
+ Port: 80,
+ SSL: false,
+ Rules: []os.OriginRule{
+ os.OriginRule{
+ Name: "videos",
+ RequestURL: "^/videos/*.m3u",
+ },
+ },
+ },
+ },
+ Caching: []os.CacheRule{
+ os.CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ },
+ Restrictions: []os.Restriction{},
+ FlavorID: "europe",
+ Status: "deployed",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "myothersite.com.poppycdn.net",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+ Rel: "flavor",
+ },
+ },
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleCreateCDNServiceSuccessfully(t)
+
+ createOpts := os.CreateOpts{
+ Name: "mywebsite.com",
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www.mywebsite.com",
+ },
+ os.Domain{
+ Domain: "blog.mywebsite.com",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Restrictions: []os.Restriction{
+ os.Restriction{
+ Name: "website only",
+ Rules: []os.RestrictionRule{
+ os.RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ Caching: []os.CacheRule{
+ os.CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ },
+ FlavorID: "cdn",
+ }
+
+ expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleGetCDNServiceSuccessfully(t)
+
+ expected := &os.Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Name: "mywebsite.com",
+ Domains: []os.Domain{
+ os.Domain{
+ Domain: "www.mywebsite.com",
+ Protocol: "http",
+ },
+ },
+ Origins: []os.Origin{
+ os.Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Caching: []os.CacheRule{
+ os.CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ os.CacheRule{
+ Name: "home",
+ TTL: 17200,
+ Rules: []os.TTLRule{
+ os.TTLRule{
+ Name: "index",
+ RequestURL: "/index.htm",
+ },
+ },
+ },
+ os.CacheRule{
+ Name: "images",
+ TTL: 12800,
+ Rules: []os.TTLRule{
+ os.TTLRule{
+ Name: "images",
+ RequestURL: "*.png",
+ },
+ },
+ },
+ },
+ Restrictions: []os.Restriction{
+ os.Restriction{
+ Name: "website only",
+ Rules: []os.RestrictionRule{
+ os.RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ FlavorID: "cdn",
+ Status: "deployed",
+ Errors: []os.Error{},
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+ Rel: "flavor",
+ },
+ },
+ }
+
+ actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleUpdateCDNServiceSuccessfully(t)
+
+ expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ updateOpts := os.UpdateOpts{
+ os.UpdateOpt{
+ Op: os.Replace,
+ Path: "/origins/0",
+ Value: map[string]interface{}{
+ "origin": "44.33.22.11",
+ "port": 80,
+ "ssl": false,
+ },
+ },
+ os.UpdateOpt{
+ Op: os.Add,
+ Path: "/domains/0",
+ Value: map[string]interface{}{
+ "domain": "added.mocksite4.com",
+ },
+ },
+ }
+ actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ os.HandleDeleteCDNServiceSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/services/doc.go b/rackspace/cdn/v1/services/doc.go
new file mode 100644
index 0000000..ee6e2a5
--- /dev/null
+++ b/rackspace/cdn/v1/services/doc.go
@@ -0,0 +1,7 @@
+// Package services provides information and interaction with the services API
+// resource in the Rackspace CDN service. This API resource allows for
+// listing, creating, updating, retrieving, and deleting services.
+//
+// A service represents an application that has its content cached to the edge
+// nodes.
+package services
diff --git a/rackspace/client.go b/rackspace/client.go
index 7421ff0..45199a4 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -176,3 +176,14 @@
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
+
+// NewCDNV1 creates a ServiceClient that may be used to access the Rackspace v1
+// CDN service.
+func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("rax:cdn")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}