Merge remote-tracking branch 'upstream/v0.2.0' into rackspace-compute

Conflicts:
	acceptance/README.md
	rackspace/client.go
diff --git a/rackspace/client.go b/rackspace/client.go
index 05c72b3..cf00dc7 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -121,8 +121,24 @@
 	if err != nil {
 		return nil, err
 	}
+
 	return &gophercloud.ServiceClient{
 		Provider: client,
 		Endpoint: url,
 	}, nil
 }
+
+// NewObjectCDNV1 creates a ServiceClient that may be used with the Rackspace v1 CDN.
+func NewObjectCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("rax:object-cdn")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
+}
+
+// NewObjectStorageV1 creates a ServiceClient that may be used with the Rackspace v1 object storage package.
+func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return os.NewObjectStorageV1(client, eo)
+}
diff --git a/rackspace/objectstorage/v1/accounts/delegate.go b/rackspace/objectstorage/v1/accounts/delegate.go
new file mode 100644
index 0000000..ae3de26
--- /dev/null
+++ b/rackspace/objectstorage/v1/accounts/delegate.go
@@ -0,0 +1,38 @@
+package accounts
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
+)
+
+// Get is a function that retrieves an account's metadata. To extract just the
+// custom metadata, call the ExtractMetadata method on the GetResult. To extract
+// all the headers that are returned (including the metadata), call the
+// ExtractHeaders method on the GetResult.
+func Get(c *gophercloud.ServiceClient) os.GetResult {
+	return os.Get(c, nil)
+}
+
+// UpdateOpts is a structure that contains parameters for updating, creating, or
+// deleting an account's metadata.
+type UpdateOpts struct {
+	Metadata    map[string]string
+	TempURLKey  string `h:"X-Account-Meta-Temp-URL-Key"`
+	TempURLKey2 string `h:"X-Account-Meta-Temp-URL-Key-2"`
+}
+
+// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers.
+func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) {
+	headers, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		headers["X-Account-Meta-"+k] = v
+	}
+	return headers, err
+}
+
+func Update(c *gophercloud.ServiceClient, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, opts)
+}
diff --git a/rackspace/objectstorage/v1/accounts/delegate_test.go b/rackspace/objectstorage/v1/accounts/delegate_test.go
new file mode 100644
index 0000000..c568bd6
--- /dev/null
+++ b/rackspace/objectstorage/v1/accounts/delegate_test.go
@@ -0,0 +1,30 @@
+package accounts
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetAccounts(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetAccountSuccessfully(t)
+
+	options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}}
+	res := Update(fake.ServiceClient(), options)
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestUpdateAccounts(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateAccountSuccessfully(t)
+
+	expected := map[string]string{"Foo": "bar"}
+	actual, err := Get(fake.ServiceClient()).ExtractMetadata()
+	th.CheckNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/objectstorage/v1/bulk/requests.go b/rackspace/objectstorage/v1/bulk/requests.go
new file mode 100644
index 0000000..7a08869
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/requests.go
@@ -0,0 +1,51 @@
+package bulk
+
+import (
+	"net/url"
+	"strings"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// DeleteOptsBuilder allows extensions to add additional parameters to the
+// Delete request.
+type DeleteOptsBuilder interface {
+	ToBulkDeleteBody() (string, error)
+}
+
+// DeleteOpts is a structure that holds parameters for deleting an object.
+type DeleteOpts []string
+
+// ToBulkDeleteBody formats a DeleteOpts into a request body.
+func (opts DeleteOpts) ToBulkDeleteBody() (string, error) {
+	return url.QueryEscape(strings.Join(opts, "\n")), nil
+}
+
+// Delete will delete objects or containers in bulk.
+func Delete(c *gophercloud.ServiceClient, opts DeleteOptsBuilder) DeleteResult {
+	var res DeleteResult
+
+	if opts == nil {
+		return res
+	}
+
+	reqString, err := opts.ToBulkDeleteBody()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	reqBody := strings.NewReader(reqString)
+
+	resp, err := perigee.Request("DELETE", deleteURL(c), perigee.Options{
+		ContentType: "text/plain",
+		MoreHeaders: c.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		ReqBody:     reqBody,
+		Results:     &res.Body,
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
diff --git a/rackspace/objectstorage/v1/bulk/requests_test.go b/rackspace/objectstorage/v1/bulk/requests_test.go
new file mode 100644
index 0000000..8b5578e
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/requests_test.go
@@ -0,0 +1,36 @@
+package bulk
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestBulkDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.AssertEquals(t, r.URL.RawQuery, "bulk-delete")
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+      {
+        "Number Not Found": 1,
+        "Response Status": "200 OK",
+        "Errors": [],
+        "Number Deleted": 1,
+        "Response Body": ""
+      }
+    `)
+	})
+
+	options := DeleteOpts{"gophercloud-testcontainer1", "gophercloud-testcontainer2"}
+	actual, err := Delete(fake.ServiceClient(), options).ExtractBody()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, actual.NumberDeleted, 1)
+}
diff --git a/rackspace/objectstorage/v1/bulk/results.go b/rackspace/objectstorage/v1/bulk/results.go
new file mode 100644
index 0000000..fddc125
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/results.go
@@ -0,0 +1,28 @@
+package bulk
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// DeleteResult represents the result of a bulk delete operation.
+type DeleteResult struct {
+	gophercloud.Result
+}
+
+// DeleteRespBody is the form of the response body returned by a bulk delete request.
+type DeleteRespBody struct {
+	NumberNotFound int      `mapstructure:"Number Not Found"`
+	ResponseStatus string   `mapstructure:"Response Status"`
+	Errors         []string `mapstructure:"Errors"`
+	NumberDeleted  int      `mapstructure:"Number Deleted"`
+	ResponseBody   string   `mapstructure:"Response Body"`
+}
+
+// ExtractBody will extract the body returned by the bulk extract request.
+func (dr DeleteResult) ExtractBody() (DeleteRespBody, error) {
+	var resp DeleteRespBody
+	err := mapstructure.Decode(dr.Body, &resp)
+	return resp, err
+}
diff --git a/rackspace/objectstorage/v1/bulk/urls.go b/rackspace/objectstorage/v1/bulk/urls.go
new file mode 100644
index 0000000..2e11203
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/urls.go
@@ -0,0 +1,11 @@
+package bulk
+
+import "github.com/rackspace/gophercloud"
+
+func deleteURL(c *gophercloud.ServiceClient) string {
+	return c.Endpoint + "?bulk-delete"
+}
+
+func extractURL(c *gophercloud.ServiceClient, ext string) string {
+	return c.Endpoint + "?extract-archive=" + ext
+}
diff --git a/rackspace/objectstorage/v1/bulk/urls_test.go b/rackspace/objectstorage/v1/bulk/urls_test.go
new file mode 100644
index 0000000..9169e52
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/urls_test.go
@@ -0,0 +1,26 @@
+package bulk
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient())
+	expected := endpoint + "?bulk-delete"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestExtractURL(t *testing.T) {
+	actual := extractURL(endpointClient(), "tar")
+	expected := endpoint + "?extract-archive=tar"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/delegate.go b/rackspace/objectstorage/v1/cdncontainers/delegate.go
new file mode 100644
index 0000000..d7eef20
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/delegate.go
@@ -0,0 +1,71 @@
+package cdncontainers
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractNames interprets a page of List results when just the container
+// names are requested.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	return os.ExtractNames(page)
+}
+
+// ListOpts are options for listing Rackspace CDN containers.
+type ListOpts struct {
+	EndMarker string `q:"end_marker"`
+	Format    string `q:"format"`
+	Limit     int    `q:"limit"`
+	Marker    string `q:"marker"`
+}
+
+// ToContainerListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each container.
+func (opts ListOpts) ToContainerListParams() (bool, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return false, "", err
+	}
+	return false, q.String(), nil
+}
+
+// List is a function that retrieves containers associated with the account as
+// well as account metadata. It returns a pager which can be iterated with the
+// EachPage function.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Get is a function that retrieves the metadata of a container. To extract just
+// the custom metadata, pass the GetResult response to the ExtractMetadata
+// function.
+func Get(c *gophercloud.ServiceClient, containerName string) os.GetResult {
+	return os.Get(c, containerName)
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or
+// deleting a container's metadata.
+type UpdateOpts struct {
+	CDNEnabled   bool `h:"X-Cdn-Enabled"`
+	LogRetention bool `h:"X-Log-Retention"`
+	TTL          int  `h:"X-Ttl"`
+}
+
+// ToContainerUpdateMap formats a CreateOpts into a map of headers.
+func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	h["X-Cdn-Enabled"] = strconv.FormatBool(opts.CDNEnabled)
+	return h, nil
+}
+
+// Update is a function that creates, updates, or deletes a container's
+// metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, containerName, opts)
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/delegate_test.go b/rackspace/objectstorage/v1/cdncontainers/delegate_test.go
new file mode 100644
index 0000000..02c3c5e
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/delegate_test.go
@@ -0,0 +1,50 @@
+package cdncontainers
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListCDNContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListContainerNamesSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ExpectedListNames, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetCDNContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetContainerSuccessfully(t)
+
+	_, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata()
+	th.CheckNoErr(t, err)
+
+}
+
+func TestUpdateCDNContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateContainerSuccessfully(t)
+
+	options := &UpdateOpts{TTL: 3600}
+	res := Update(fake.ServiceClient(), "testContainer", options)
+	th.CheckNoErr(t, res.Err)
+
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/requests.go b/rackspace/objectstorage/v1/cdncontainers/requests.go
new file mode 100644
index 0000000..9cb6e9c
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/requests.go
@@ -0,0 +1,58 @@
+package cdncontainers
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// EnableOptsBuilder allows extensions to add additional parameters to the Enable
+// request.
+type EnableOptsBuilder interface {
+	ToCDNContainerEnableMap() (map[string]string, error)
+}
+
+// EnableOpts is a structure that holds options for enabling a CDN container.
+type EnableOpts struct {
+	// CDNEnabled indicates whether or not the container is CDN enabled. Set to
+	// `true` to enable the container. Note that changing this setting from true
+	// to false will disable the container in the CDN but only after the TTL has
+	// expired.
+	CDNEnabled bool `h:"X-Cdn-Enabled"`
+	// TTL is the time-to-live for the container (in seconds).
+	TTL int `h:"X-Ttl"`
+}
+
+// ToCDNContainerEnableMap formats an EnableOpts into a map of headers.
+func (opts EnableOpts) ToCDNContainerEnableMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	return h, nil
+}
+
+// Enable is a function that enables/disables a CDN container.
+func Enable(c *gophercloud.ServiceClient, containerName string, opts EnableOptsBuilder) EnableResult {
+	var res EnableResult
+	h := c.Provider.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := opts.ToCDNContainerEnableMap()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+
+	resp, err := perigee.Request("PUT", enableURL(c, containerName), perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{201, 202, 204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/requests_test.go b/rackspace/objectstorage/v1/cdncontainers/requests_test.go
new file mode 100644
index 0000000..28b963d
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/requests_test.go
@@ -0,0 +1,29 @@
+package cdncontainers
+
+import (
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestEnableCDNContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Add("X-Ttl", "259200")
+		w.Header().Add("X-Cdn-Enabled", "True")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	options := &EnableOpts{CDNEnabled: true, TTL: 259200}
+	actual := Enable(fake.ServiceClient(), "testContainer", options)
+	th.AssertNoErr(t, actual.Err)
+	th.CheckEquals(t, actual.Header["X-Ttl"][0], "259200")
+	th.CheckEquals(t, actual.Header["X-Cdn-Enabled"][0], "True")
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/results.go b/rackspace/objectstorage/v1/cdncontainers/results.go
new file mode 100644
index 0000000..374d884
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/results.go
@@ -0,0 +1,8 @@
+package cdncontainers
+
+import "github.com/rackspace/gophercloud"
+
+// EnableResult represents the result of a get operation.
+type EnableResult struct {
+	gophercloud.Result
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/urls.go b/rackspace/objectstorage/v1/cdncontainers/urls.go
new file mode 100644
index 0000000..80653f2
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/urls.go
@@ -0,0 +1,7 @@
+package cdncontainers
+
+import "github.com/rackspace/gophercloud"
+
+func enableURL(c *gophercloud.ServiceClient, containerName string) string {
+	return c.ServiceURL(containerName)
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/urls_test.go b/rackspace/objectstorage/v1/cdncontainers/urls_test.go
new file mode 100644
index 0000000..aa5bfe6
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/urls_test.go
@@ -0,0 +1,20 @@
+package cdncontainers
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestEnableURL(t *testing.T) {
+	actual := enableURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/rackspace/objectstorage/v1/cdnobjects/delegate.go b/rackspace/objectstorage/v1/cdnobjects/delegate.go
new file mode 100644
index 0000000..e9d2ff1
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdnobjects/delegate.go
@@ -0,0 +1,11 @@
+package cdnobjects
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+)
+
+// Delete is a function that deletes an object from the CDN.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult {
+	return os.Delete(c, containerName, objectName, nil)
+}
diff --git a/rackspace/objectstorage/v1/cdnobjects/delegate_test.go b/rackspace/objectstorage/v1/cdnobjects/delegate_test.go
new file mode 100644
index 0000000..b5e04a9
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdnobjects/delegate_test.go
@@ -0,0 +1,19 @@
+package cdnobjects
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDeleteCDNObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDeleteObjectSuccessfully(t)
+
+	res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil)
+	th.AssertNoErr(t, res.Err)
+
+}
diff --git a/rackspace/objectstorage/v1/containers/delegate.go b/rackspace/objectstorage/v1/containers/delegate.go
new file mode 100644
index 0000000..77ed002
--- /dev/null
+++ b/rackspace/objectstorage/v1/containers/delegate.go
@@ -0,0 +1,93 @@
+package containers
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractInfo interprets a page of List results when full container info
+// is requested.
+func ExtractInfo(page pagination.Page) ([]os.Container, error) {
+	return os.ExtractInfo(page)
+}
+
+// ExtractNames interprets a page of List results when just the container
+// names are requested.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	return os.ExtractNames(page)
+}
+
+// List is a function that retrieves containers associated with the account as
+// well as account metadata. It returns a pager which can be iterated with the
+// EachPage function.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// CreateOpts is a structure that holds parameters for creating a container.
+type CreateOpts struct {
+	Metadata         map[string]string
+	ContainerRead    string `h:"X-Container-Read"`
+	ContainerWrite   string `h:"X-Container-Write"`
+	VersionsLocation string `h:"X-Versions-Location"`
+}
+
+// ToContainerCreateMap formats a CreateOpts into a map of headers.
+func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+	return h, nil
+}
+
+// Create is a function that creates a new container.
+func Create(c *gophercloud.ServiceClient, containerName string, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, containerName, opts)
+}
+
+// Delete is a function that deletes a container.
+func Delete(c *gophercloud.ServiceClient, containerName string) os.DeleteResult {
+	return os.Delete(c, containerName)
+}
+
+// UpdateOpts is a structure that holds parameters for updating or creating a
+// container's metadata.
+type UpdateOpts struct {
+	Metadata               map[string]string
+	ContainerRead          string `h:"X-Container-Read"`
+	ContainerWrite         string `h:"X-Container-Write"`
+	ContentType            string `h:"Content-Type"`
+	DetectContentType      bool   `h:"X-Detect-Content-Type"`
+	RemoveVersionsLocation string `h:"X-Remove-Versions-Location"`
+	VersionsLocation       string `h:"X-Versions-Location"`
+}
+
+// ToContainerUpdateMap formats a CreateOpts into a map of headers.
+func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+	return h, nil
+}
+
+// Update is a function that creates, updates, or deletes a container's
+// metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, containerName, opts)
+}
+
+// Get is a function that retrieves the metadata of a container. To extract just
+// the custom metadata, pass the GetResult response to the ExtractMetadata
+// function.
+func Get(c *gophercloud.ServiceClient, containerName string) os.GetResult {
+	return os.Get(c, containerName)
+}
diff --git a/rackspace/objectstorage/v1/containers/delegate_test.go b/rackspace/objectstorage/v1/containers/delegate_test.go
new file mode 100644
index 0000000..7ba4eb2
--- /dev/null
+++ b/rackspace/objectstorage/v1/containers/delegate_test.go
@@ -0,0 +1,91 @@
+package containers
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListContainerInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListContainerInfoSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), &os.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ExpectedListInfo, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestListContainerNames(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListContainerNamesSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), &os.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		if err != nil {
+			t.Errorf("Failed to extract container names: %v", err)
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, os.ExpectedListNames, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestCreateContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateContainerSuccessfully(t)
+
+	options := os.CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}}
+	res := Create(fake.ServiceClient(), "testContainer", options)
+	th.CheckNoErr(t, res.Err)
+	th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0])
+
+}
+
+func TestDeleteContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDeleteContainerSuccessfully(t)
+
+	res := Delete(fake.ServiceClient(), "testContainer")
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestUpdateContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateContainerSuccessfully(t)
+
+	options := &os.UpdateOpts{Metadata: map[string]string{"foo": "bar"}}
+	res := Update(fake.ServiceClient(), "testContainer", options)
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestGetContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetContainerSuccessfully(t)
+
+	_, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata()
+	th.CheckNoErr(t, err)
+}
diff --git a/rackspace/objectstorage/v1/objects/delegate.go b/rackspace/objectstorage/v1/objects/delegate.go
new file mode 100644
index 0000000..bd4a4f0
--- /dev/null
+++ b/rackspace/objectstorage/v1/objects/delegate.go
@@ -0,0 +1,90 @@
+package objects
+
+import (
+	"io"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractInfo is a function that takes a page of objects and returns their full information.
+func ExtractInfo(page pagination.Page) ([]os.Object, error) {
+	return os.ExtractInfo(page)
+}
+
+// ExtractNames is a function that takes a page of objects and returns only their names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	return os.ExtractNames(page)
+}
+
+// List is a function that retrieves objects in the container as
+// well as container metadata. It returns a pager which can be iterated with the
+// EachPage function.
+func List(c *gophercloud.ServiceClient, containerName string, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, containerName, opts)
+}
+
+// Download is a function that retrieves the content and metadata for an object.
+// To extract just the content, pass the DownloadResult response to the
+// ExtractContent function.
+func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DownloadOptsBuilder) os.DownloadResult {
+	return os.Download(c, containerName, objectName, opts)
+}
+
+// Create is a function that creates a new object or replaces an existing object.
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, containerName, objectName, content, opts)
+}
+
+// CopyOpts is a structure that holds parameters for copying one object to
+// another.
+type CopyOpts struct {
+	Metadata           map[string]string
+	ContentDisposition string `h:"Content-Disposition"`
+	ContentEncoding    string `h:"Content-Encoding"`
+	ContentLength      int    `h:"Content-Length"`
+	ContentType        string `h:"Content-Type"`
+	CopyFrom           string `h:"X-Copy_From"`
+	Destination        string `h:"Destination"`
+	DetectContentType  bool   `h:"X-Detect-Content-Type"`
+}
+
+// ToObjectCopyMap formats a CopyOpts into a map of headers.
+func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+	// `Content-Length` is required and a value of "0" is acceptable, but calling `gophercloud.BuildHeaders`
+	// will remove the `Content-Length` header if it's set to 0 (or equivalently not set). This will add
+	// the header if it's not already set.
+	if _, ok := h["Content-Length"]; !ok {
+		h["Content-Length"] = "0"
+	}
+	return h, nil
+}
+
+// Copy is a function that copies one object to another.
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts os.CopyOptsBuilder) os.CopyResult {
+	return os.Copy(c, containerName, objectName, opts)
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult {
+	return os.Delete(c, containerName, objectName, opts)
+}
+
+// Get is a function that retrieves the metadata of an object. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts os.GetOptsBuilder) os.GetResult {
+	return os.Get(c, containerName, objectName, opts)
+}
+
+// Update is a function that creates, updates, or deletes an object's metadata.
+func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, containerName, objectName, opts)
+}
diff --git a/rackspace/objectstorage/v1/objects/delegate_test.go b/rackspace/objectstorage/v1/objects/delegate_test.go
new file mode 100644
index 0000000..08831ec
--- /dev/null
+++ b/rackspace/objectstorage/v1/objects/delegate_test.go
@@ -0,0 +1,115 @@
+package objects
+
+import (
+	"bytes"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDownloadObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDownloadObjectSuccessfully(t)
+
+	content, err := Download(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractContent()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, string(content), "Successful download with Gophercloud")
+}
+
+func TestListObjectsInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListObjectsInfoSuccessfully(t)
+
+	count := 0
+	options := &os.ListOpts{Full: true}
+	err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ExpectedListInfo, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestListObjectNames(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListObjectNamesSuccessfully(t)
+
+	count := 0
+	options := &os.ListOpts{Full: false}
+	err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		if err != nil {
+			t.Errorf("Failed to extract container names: %v", err)
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, os.ExpectedListNames, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestCreateObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateObjectSuccessfully(t)
+
+	content := bytes.NewBufferString("Did gyre and gimble in the wabe")
+	options := &os.CreateOpts{ContentType: "application/json"}
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestCopyObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCopyObjectSuccessfully(t)
+
+	options := &CopyOpts{Destination: "/newTestContainer/newTestObject"}
+	res := Copy(fake.ServiceClient(), "testContainer", "testObject", options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestDeleteObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDeleteObjectSuccessfully(t)
+
+	res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateObjectSuccessfully(t)
+
+	options := &os.UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}}
+	res := Update(fake.ServiceClient(), "testContainer", "testObject", options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestGetObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetObjectSuccessfully(t)
+
+	expected := map[string]string{"Gophercloud-Test": "objects"}
+	actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}