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
+}