Merge pull request #209 from smashwilson/map-no-more
Provide structs for server creation and update
diff --git a/acceptance/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
new file mode 100644
index 0000000..5835048
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -0,0 +1,87 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+)
+
+func TestSnapshots(t *testing.T) {
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ v, err := volumes.Create(client, &volumes.CreateOpts{
+ Name: "gophercloud-test-volume",
+ Size: 1,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Failed to create volume: %v\n", err)
+ }
+
+ err = volumes.WaitForStatus(client, v.ID, "available", 120)
+ if err != nil {
+ t.Fatalf("Failed to create volume: %v\n", err)
+ }
+
+ t.Logf("Created volume: %v\n", v)
+
+ ss, err := snapshots.Create(client, &snapshots.CreateOpts{
+ Name: "gophercloud-test-snapshot",
+ VolumeID: v.ID,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Failed to create snapshot: %v\n", err)
+ }
+
+ err = snapshots.WaitForStatus(client, ss.ID, "available", 120)
+ if err != nil {
+ t.Fatalf("Failed to create snapshot: %v\n", err)
+ }
+
+ t.Logf("Created snapshot: %+v\n", ss)
+
+ err = snapshots.Delete(client, ss.ID)
+ if err != nil {
+ t.Fatalf("Failed to delete snapshot: %v", err)
+ }
+
+ err = gophercloud.WaitFor(120, func() (bool, error) {
+ _, err := snapshots.Get(client, ss.ID).Extract()
+ if err != nil {
+ return true, nil
+ }
+
+ return false, nil
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete snapshot: %v", err)
+ }
+
+ t.Log("Deleted snapshot\n")
+
+ err = volumes.Delete(client, v.ID)
+ if err != nil {
+ t.Errorf("Failed to delete volume: %v", err)
+ }
+
+ err = gophercloud.WaitFor(120, func() (bool, error) {
+ _, err := volumes.Get(client, v.ID).Extract()
+ if err != nil {
+ return true, nil
+ }
+
+ return false, nil
+ })
+ if err != nil {
+ t.Errorf("Failed to delete volume: %v", err)
+ }
+
+ t.Log("Deleted volume\n")
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumes_test.go b/acceptance/openstack/blockstorage/v1/volumes_test.go
new file mode 100644
index 0000000..21a47ac
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -0,0 +1,88 @@
+// +build acceptance blockstorage
+
+package v1
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
+
+func TestVolumes(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ cv, err := volumes.Create(client, &volumes.CreateOpts{
+ Size: 1,
+ Name: "gophercloud-test-volume",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+ if err != nil {
+ t.Error(err)
+ }
+ err = volumes.Delete(client, cv.ID)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ _, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{
+ Name: "gophercloud-updated-volume",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ v, err := volumes.Get(client, cv.ID).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ fmt.Printf("Got volume: %+v\n", v)
+
+ if v.Name != "gophercloud-updated-volume" {
+ t.Errorf("Unable to update volume: Expected name: gophercloud-updated-volume\nActual name: %s", v.Name)
+ }
+
+ err = volumes.List(client, &volumes.ListOpts{Name: "gophercloud-updated-volume"}).EachPage(func(page pagination.Page) (bool, error) {
+ vols, err := volumes.ExtractVolumes(page)
+ if len(vols) != 1 {
+ t.Errorf("Expected 1 volume, got %d", len(vols))
+ }
+ return true, err
+ })
+ if err != nil {
+ t.Errorf("Error listing volumes: %v", err)
+ }
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
new file mode 100644
index 0000000..416e341
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestVolumeTypes(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
+ ExtraSpecs: map[string]interface{}{
+ "capabilities": "gpu",
+ "priority": 3,
+ },
+ Name: "gophercloud-test-volumeType",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ time.Sleep(10000 * time.Millisecond)
+ err = volumetypes.Delete(client, vt.ID)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+ t.Logf("Created volume type: %+v\n", vt)
+
+ vt, err = volumetypes.Get(client, vt.ID).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ t.Logf("Got volume type: %+v\n", vt)
+
+ err = volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ volTypes, err := volumetypes.ExtractVolumeTypes(page)
+ if len(volTypes) != 1 {
+ t.Errorf("Expected 1 volume type, got %d", len(volTypes))
+ }
+ t.Logf("Listing volume types: %+v\n", volTypes)
+ return true, err
+ })
+ if err != nil {
+ t.Errorf("Error trying to list volume types: %v", err)
+ }
+}
diff --git a/acceptance/openstack/identity_test.go b/acceptance/openstack/identity/v2/identity_test.go
similarity index 99%
rename from acceptance/openstack/identity_test.go
rename to acceptance/openstack/identity/v2/identity_test.go
index 8b6035d..ff4c9cd 100644
--- a/acceptance/openstack/identity_test.go
+++ b/acceptance/openstack/identity/v2/identity_test.go
@@ -1,6 +1,6 @@
// +build acceptance
-package openstack
+package v2
import (
"fmt"
diff --git a/acceptance/openstack/identity/v3/token_test.go b/acceptance/openstack/identity/v3/token_test.go
index d5f9ea6..341acb7 100644
--- a/acceptance/openstack/identity/v3/token_test.go
+++ b/acceptance/openstack/identity/v3/token_test.go
@@ -34,15 +34,10 @@
service := openstack.NewIdentityV3(provider)
// Use the service to create a token.
- result, err := tokens3.Create(service, ao, nil)
+ token, err := tokens3.Create(service, ao, nil).Extract()
if err != nil {
t.Fatalf("Unable to get token: %v", err)
}
- token, err := result.TokenID()
- if err != nil {
- t.Fatalf("Unable to extract token from response: %v", err)
- }
-
- t.Logf("Acquired token: %s", token)
+ t.Logf("Acquired token: %s", token.ID)
}
diff --git a/acceptance/openstack/objectstorage/v1/accounts_test.go b/acceptance/openstack/objectstorage/v1/accounts_test.go
new file mode 100644
index 0000000..6768927
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/accounts_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v1
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
+)
+
+func TestAccounts(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client, err := newClient()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Update an account's metadata.
+ err = accounts.Update(client, accounts.UpdateOpts{
+ Metadata: metadata,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Defer the deletion of the metadata set above.
+ defer func() {
+ tempMap := make(map[string]string)
+ for k := range metadata {
+ tempMap[k] = ""
+ }
+ err = accounts.Update(client, accounts.UpdateOpts{
+ Metadata: tempMap,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ // Retrieve account metadata.
+ gr, err := accounts.Get(client, accounts.GetOpts{})
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Extract the custom metadata from the 'Get' response.
+ am := accounts.ExtractMetadata(gr)
+ for k := range metadata {
+ if am[k] != metadata[strings.Title(k)] {
+ t.Errorf("Expected custom metadata with key: %s", k)
+ return
+ }
+ }
+}
diff --git a/acceptance/openstack/objectstorage/v1/common.go b/acceptance/openstack/objectstorage/v1/common.go
new file mode 100644
index 0000000..08065a4
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/common.go
@@ -0,0 +1,28 @@
+// +build acceptance
+
+package v1
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "os"
+)
+
+var metadata = map[string]string{"gopher": "cloud"}
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ return openstack.NewStorageV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
diff --git a/acceptance/openstack/objectstorage/v1/containers_test.go b/acceptance/openstack/objectstorage/v1/containers_test.go
new file mode 100644
index 0000000..b541307
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/containers_test.go
@@ -0,0 +1,108 @@
+// +build acceptance
+
+package v1
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// numContainers is the number of containers to create for testing.
+var numContainers = 2
+
+func TestContainers(t *testing.T) {
+ // Create a new client to execute the HTTP requests. See common.go for newClient body.
+ client, err := newClient()
+ if err != nil {
+ t.Error(err)
+ }
+
+ // Create a slice of random container names.
+ cNames := make([]string, numContainers)
+ for i := 0; i < numContainers; i++ {
+ cNames[i] = tools.RandomString("gophercloud-test-container-", 8)
+ }
+
+ // Create numContainers containers.
+ for i := 0; i < len(cNames); i++ {
+ _, err := containers.Create(client, cNames[i], nil).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ }
+ }
+ // Delete the numContainers containers after function completion.
+ defer func() {
+ for i := 0; i < len(cNames); i++ {
+ _, err = containers.Delete(client, cNames[i]).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ }
+ }
+ }()
+
+ // List the numContainer names that were just created. To just list those,
+ // the 'prefix' parameter is used.
+ err = containers.List(client, &containers.ListOpts{Full: true, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) {
+ containerList, err := containers.ExtractInfo(page)
+ if err != nil {
+ t.Error(err)
+ }
+ for _, n := range containerList {
+ t.Logf("Container: Name [%s] Count [%d] Bytes [%d]",
+ n.Name, n.Count, n.Bytes)
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ }
+
+ // List the info for the numContainer containers that were created.
+ err = containers.List(client, &containers.ListOpts{Full: false, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) {
+ containerList, err := containers.ExtractNames(page)
+ if err != nil {
+ return false, err
+ }
+ for _, n := range containerList {
+ t.Logf("Container: Name [%s]", n)
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ }
+
+ // Update one of the numContainer container metadata.
+ _, err = containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: metadata}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ }
+ // After the tests are done, delete the metadata that was set.
+ defer func() {
+ tempMap := make(map[string]string)
+ for k := range metadata {
+ tempMap[k] = ""
+ }
+ _, err = containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: tempMap}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ }
+ }()
+
+ // Retrieve a container's metadata.
+ cm, err := containers.Get(client, cNames[0]).ExtractMetadata()
+ if err != nil {
+ t.Error(err)
+ }
+ for k := range metadata {
+ if cm[k] != metadata[strings.Title(k)] {
+ t.Errorf("Expected custom metadata with key: %s", k)
+ }
+ }
+}
diff --git a/acceptance/openstack/objectstorage/v1/objects_test.go b/acceptance/openstack/objectstorage/v1/objects_test.go
new file mode 100644
index 0000000..5a63a4c
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/objects_test.go
@@ -0,0 +1,162 @@
+// +build acceptance
+
+package v1
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+ "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// numObjects is the number of objects to create for testing.
+var numObjects = 2
+
+func TestObjects(t *testing.T) {
+ // Create a provider client for executing the HTTP request.
+ // See common.go for more information.
+ client, err := newClient()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Make a slice of length numObjects to hold the random object names.
+ oNames := make([]string, numObjects)
+ for i := 0; i < len(oNames); i++ {
+ oNames[i] = tools.RandomString("test-object-", 8)
+ }
+
+ // Create a container to hold the test objects.
+ cName := tools.RandomString("test-container-", 8)
+ _, err = containers.Create(client, cName, nil).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Defer deletion of the container until after testing.
+ defer func() {
+ _, err = containers.Delete(client, cName).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ // Create a slice of buffers to hold the test object content.
+ oContents := make([]*bytes.Buffer, numObjects)
+ for i := 0; i < numObjects; i++ {
+ oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10)))
+ _, err = objects.Create(client, cName, oNames[i], oContents[i], nil).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }
+ // Delete the objects after testing.
+ defer func() {
+ for i := 0; i < numObjects; i++ {
+ _, err = objects.Delete(client, cName, oNames[i], nil).ExtractHeaders()
+ }
+ }()
+
+ ons := make([]string, 0, len(oNames))
+ err = objects.List(client, cName, &objects.ListOpts{Full: false, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) {
+ names, err := objects.ExtractNames(page)
+ if err != nil {
+ return false, err
+ }
+ ons = append(ons, names...)
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(ons) != len(oNames) {
+ t.Errorf("Expected %d names and got %d", len(oNames), len(ons))
+ return
+ }
+
+ ois := make([]objects.Object, 0, len(oNames))
+ err = objects.List(client, cName, &objects.ListOpts{Full: true, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) {
+ info, err := objects.ExtractInfo(page)
+ if err != nil {
+ return false, nil
+ }
+
+ ois = append(ois, info...)
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(ois) != len(oNames) {
+ t.Errorf("Expected %d containers and got %d", len(oNames), len(ois))
+ return
+ }
+
+ // Copy the contents of one object to another.
+ _, err = objects.Copy(client, cName, oNames[0], &objects.CopyOpts{Destination: cName + "/" + oNames[1]}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Download one of the objects that was created above.
+ o1Content, err := objects.Download(client, cName, oNames[0], nil).ExtractContent()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Download the another object that was create above.
+ o2Content, err := objects.Download(client, cName, oNames[1], nil).ExtractContent()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Compare the two object's contents to test that the copy worked.
+ if string(o2Content) != string(o1Content) {
+ t.Errorf("Copy failed. Expected\n%s\nand got\n%s", string(o1Content), string(o2Content))
+ return
+ }
+
+ // Update an object's metadata.
+ _, err = objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: metadata}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ // Delete the object's metadata after testing.
+ defer func() {
+ tempMap := make(map[string]string)
+ for k := range metadata {
+ tempMap[k] = ""
+ }
+ _, err = objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: tempMap}).ExtractHeaders()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ // Retrieve an object's metadata.
+ om, err := objects.Get(client, cName, oNames[0], nil).ExtractMetadata()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ for k := range metadata {
+ if om[k] != metadata[strings.Title(k)] {
+ t.Errorf("Expected custom metadata with key: %s", k)
+ return
+ }
+ }
+}
diff --git a/acceptance/openstack/storage_test.go b/acceptance/openstack/storage_test.go
deleted file mode 100644
index e7907d1..0000000
--- a/acceptance/openstack/storage_test.go
+++ /dev/null
@@ -1,364 +0,0 @@
-// +build acceptance
-
-package openstack
-
-import (
- "bytes"
- "os"
- "strings"
- "testing"
-
- "github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/acceptance/tools"
- "github.com/rackspace/gophercloud/openstack"
- "github.com/rackspace/gophercloud/openstack/storage/v1/accounts"
- "github.com/rackspace/gophercloud/openstack/storage/v1/containers"
- "github.com/rackspace/gophercloud/openstack/storage/v1/objects"
- "github.com/rackspace/gophercloud/openstack/utils"
- "github.com/rackspace/gophercloud/pagination"
-)
-
-var metadata = map[string]string{"gopher": "cloud"}
-var numContainers = 2
-var numObjects = 2
-
-func newClient() (*gophercloud.ServiceClient, error) {
- ao, err := utils.AuthOptions()
- if err != nil {
- return nil, err
- }
-
- client, err := openstack.AuthenticatedClient(ao)
- if err != nil {
- return nil, err
- }
-
- return openstack.NewStorageV1(client, gophercloud.EndpointOpts{
- Region: os.Getenv("OS_REGION_NAME"),
- })
-}
-
-func TestAccount(t *testing.T) {
- client, err := newClient()
- if err != nil {
- t.Error(err)
- return
- }
-
- err = accounts.Update(client, accounts.UpdateOpts{
- Metadata: metadata,
- })
- if err != nil {
- t.Error(err)
- return
- }
- defer func() {
- tempMap := make(map[string]string)
- for k := range metadata {
- tempMap[k] = ""
- }
- err = accounts.Update(client, accounts.UpdateOpts{
- Metadata: tempMap,
- })
- if err != nil {
- t.Error(err)
- return
- }
- }()
-
- gr, err := accounts.Get(client, accounts.GetOpts{})
- if err != nil {
- t.Error(err)
- return
- }
- am := accounts.ExtractMetadata(gr)
- for k := range metadata {
- if am[k] != metadata[strings.Title(k)] {
- t.Errorf("Expected custom metadata with key: %s", k)
- return
- }
- }
-}
-
-func TestContainers(t *testing.T) {
- client, err := newClient()
- if err != nil {
- t.Error(err)
- return
- }
-
- cNames := make([]string, numContainers)
- for i := 0; i < numContainers; i++ {
- cNames[i] = tools.RandomString("test-container-", 8)
- }
-
- for i := 0; i < len(cNames); i++ {
- _, err := containers.Create(client, containers.CreateOpts{
- Name: cNames[i],
- })
- if err != nil {
- t.Error(err)
- return
- }
- }
- defer func() {
- for i := 0; i < len(cNames); i++ {
- err = containers.Delete(client, containers.DeleteOpts{
- Name: cNames[i],
- })
- if err != nil {
- t.Error(err)
- return
- }
- }
- }()
-
- cns := make([]string, 0, numContainers)
- pager := containers.List(client, containers.ListOpts{Full: false})
- err = pager.EachPage(func(page pagination.Page) (bool, error) {
- names, err := containers.ExtractNames(page)
- if err != nil {
- return false, err
- }
-
- cns = append(cns, names...)
-
- return true, nil
- })
- if err != nil {
- t.Fatal(err)
- return
- }
-
- if len(cns) != len(cNames) {
- t.Errorf("Expected %d names and got %d", len(cNames), len(cns))
- return
- }
-
- cis := make([]containers.Container, 0, numContainers)
- pager = containers.List(client, containers.ListOpts{Full: true})
- err = pager.EachPage(func(page pagination.Page) (bool, error) {
- cisPage, err := containers.ExtractInfo(page)
- if err != nil {
- return false, err
- }
-
- cis = append(cis, cisPage...)
-
- return true, nil
- })
-
- if len(cis) != len(cNames) {
- t.Errorf("Expected %d containers and got %d", len(cNames), len(cis))
- return
- }
-
- err = containers.Update(client, containers.UpdateOpts{
- Name: cNames[0],
- Metadata: metadata,
- })
- if err != nil {
- t.Error(err)
- return
- }
- defer func() {
- tempMap := make(map[string]string)
- for k := range metadata {
- tempMap[k] = ""
- }
- err = containers.Update(client, containers.UpdateOpts{
- Name: cNames[0],
- Metadata: tempMap,
- })
- if err != nil {
- t.Error(err)
- return
- }
- }()
-
- gr, err := containers.Get(client, containers.GetOpts{})
- if err != nil {
- t.Error(err)
- return
- }
- cm := containers.ExtractMetadata(gr)
- for k := range metadata {
- if cm[k] != metadata[strings.Title(k)] {
- t.Errorf("Expected custom metadata with key: %s", k)
- return
- }
- }
-}
-
-func TestObjects(t *testing.T) {
- client, err := newClient()
- if err != nil {
- t.Error(err)
- return
- }
-
- oNames := make([]string, numObjects)
- for i := 0; i < len(oNames); i++ {
- oNames[i] = tools.RandomString("test-object-", 8)
- }
-
- cName := tools.RandomString("test-container-", 8)
- _, err = containers.Create(client, containers.CreateOpts{
- Name: cName,
- })
- if err != nil {
- t.Error(err)
- return
- }
- defer func() {
- err = containers.Delete(client, containers.DeleteOpts{
- Name: cName,
- })
- if err != nil {
- t.Error(err)
- return
- }
- }()
-
- oContents := make([]*bytes.Buffer, numObjects)
- for i := 0; i < numObjects; i++ {
- oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10)))
- err = objects.Create(client, objects.CreateOpts{
- Container: cName,
- Name: oNames[i],
- Content: oContents[i],
- })
- if err != nil {
- t.Error(err)
- return
- }
- }
- defer func() {
- for i := 0; i < numObjects; i++ {
- err = objects.Delete(client, objects.DeleteOpts{
- Container: cName,
- Name: oNames[i],
- })
- }
- }()
-
- pager := objects.List(client, objects.ListOpts{Full: false, Container: cName})
- ons := make([]string, 0, len(oNames))
- err = pager.EachPage(func(page pagination.Page) (bool, error) {
- names, err := objects.ExtractNames(page)
- if err != nil {
- return false, err
- }
- ons = append(ons, names...)
-
- return true, nil
- })
- if err != nil {
- t.Error(err)
- return
- }
- if len(ons) != len(oNames) {
- t.Errorf("Expected %d names and got %d", len(oNames), len(ons))
- return
- }
-
- pager = objects.List(client, objects.ListOpts{Full: true, Container: cName})
- ois := make([]objects.Object, 0, len(oNames))
- err = pager.EachPage(func(page pagination.Page) (bool, error) {
- info, err := objects.ExtractInfo(page)
- if err != nil {
- return false, nil
- }
-
- ois = append(ois, info...)
-
- return true, nil
- })
- if err != nil {
- t.Error(err)
- return
- }
- if len(ois) != len(oNames) {
- t.Errorf("Expected %d containers and got %d", len(oNames), len(ois))
- return
- }
-
- err = objects.Copy(client, objects.CopyOpts{
- Container: cName,
- Name: oNames[0],
- NewContainer: cName,
- NewName: oNames[1],
- })
- if err != nil {
- t.Error(err)
- return
- }
-
- dr, err := objects.Download(client, objects.DownloadOpts{
- Container: cName,
- Name: oNames[1],
- })
- if err != nil {
- t.Error(err)
- return
- }
- o2Content, err := objects.ExtractContent(dr)
- if err != nil {
- t.Error(err)
- }
- dr, err = objects.Download(client, objects.DownloadOpts{
- Container: cName,
- Name: oNames[0],
- })
- if err != nil {
- t.Error(err)
- return
- }
- o1Content, err := objects.ExtractContent(dr)
- if err != nil {
- t.Error(err)
- return
- }
- if string(o2Content) != string(o1Content) {
- t.Errorf("Copy failed. Expected\n%s\nand got\n%s", string(o1Content), string(o2Content))
- return
- }
-
- err = objects.Update(client, objects.UpdateOpts{
- Container: cName,
- Name: oNames[0],
- Metadata: metadata,
- })
- if err != nil {
- t.Error(err)
- return
- }
- defer func() {
- tempMap := make(map[string]string)
- for k := range metadata {
- tempMap[k] = ""
- }
- err = objects.Update(client, objects.UpdateOpts{
- Container: cName,
- Name: oNames[0],
- Metadata: tempMap,
- })
- if err != nil {
- t.Error(err)
- return
- }
- }()
-
- gr, err := objects.Get(client, objects.GetOpts{})
- if err != nil {
- t.Error(err)
- return
- }
- om := objects.ExtractMetadata(gr)
- for k := range metadata {
- if om[k] != metadata[strings.Title(k)] {
- t.Errorf("Expected custom metadata with key: %s", k)
- return
- }
- }
-}
diff --git a/openstack/blockstorage/v1/apiversions/doc.go b/openstack/blockstorage/v1/apiversions/doc.go
new file mode 100644
index 0000000..c3c486f
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/doc.go
@@ -0,0 +1,3 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Cinder service.
+package apiversions
diff --git a/openstack/blockstorage/v1/apiversions/requests.go b/openstack/blockstorage/v1/apiversions/requests.go
new file mode 100644
index 0000000..b3a39f7
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests.go
@@ -0,0 +1,28 @@
+package apiversions
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// ListVersions lists all the Cinder API versions available to end-users.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return pagination.NewPager(c, listURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
+ return APIVersionPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, v string) GetResult {
+ var res GetResult
+ _, err := perigee.Request("GET", getURL(client, v), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ Results: &res.Resp,
+ })
+ res.Err = err
+ return res
+}
diff --git a/openstack/blockstorage/v1/apiversions/requests_test.go b/openstack/blockstorage/v1/apiversions/requests_test.go
new file mode 100644
index 0000000..c135722
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests_test.go
@@ -0,0 +1,156 @@
+package apiversions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestListVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `{
+ "versions": [
+ {
+ "status": "CURRENT",
+ "updated": "2012-01-04T11:33:21Z",
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v1/",
+ "rel": "self"
+ }
+ ]
+ },
+ {
+ "status": "CURRENT",
+ "updated": "2012-11-21T11:33:21Z",
+ "id": "v2.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v2/",
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+ }`)
+ })
+
+ count := 0
+
+ List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAPIVersions(page)
+ if err != nil {
+ t.Errorf("Failed to extract API versions: %v", err)
+ return false, err
+ }
+
+ expected := []APIVersion{
+ APIVersion{
+ ID: "v1.0",
+ Status: "CURRENT",
+ Updated: "2012-01-04T11:33:21Z",
+ },
+ APIVersion{
+ ID: "v2.0",
+ Status: "CURRENT",
+ Updated: "2012-11-21T11:33:21Z",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestAPIInfo(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `{
+ "version": {
+ "status": "CURRENT",
+ "updated": "2012-01-04T11:33:21Z",
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.volume+xml;version=1"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.volume+json;version=1"
+ }
+ ],
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v1/",
+ "rel": "self"
+ },
+ {
+ "href": "http://jorgew.github.com/block-storage-api/content/os-block-storage-1.0.pdf",
+ "type": "application/pdf",
+ "rel": "describedby"
+ },
+ {
+ "href": "http://docs.rackspacecloud.com/servers/api/v1.1/application.wadl",
+ "type": "application/vnd.sun.wadl+xml",
+ "rel": "describedby"
+ }
+ ]
+ }
+ }`)
+ })
+
+ actual, err := Get(ServiceClient(), "v1").Extract()
+ if err != nil {
+ t.Errorf("Failed to extract version: %v", err)
+ }
+
+ expected := APIVersion{
+ ID: "v1.0",
+ Status: "CURRENT",
+ Updated: "2012-01-04T11:33:21Z",
+ }
+
+ th.AssertEquals(t, actual.ID, expected.ID)
+ th.AssertEquals(t, actual.Status, expected.Status)
+ th.AssertEquals(t, actual.Updated, expected.Updated)
+}
diff --git a/openstack/blockstorage/v1/apiversions/results.go b/openstack/blockstorage/v1/apiversions/results.go
new file mode 100644
index 0000000..eeff132
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/results.go
@@ -0,0 +1,62 @@
+package apiversions
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// APIVersion represents an API version for Cinder.
+type APIVersion struct {
+ ID string `json:"id" mapstructure:"id"` // unique identifier
+ Status string `json:"status" mapstructure:"status"` // current status
+ Updated string `json:"updated" mapstructure:"updated"` // date last updated
+}
+
+// APIVersionPage is the page returned by a pager when traversing over a
+// collection of API versions.
+type APIVersionPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an APIVersionPage struct is empty.
+func (r APIVersionPage) IsEmpty() (bool, error) {
+ is, err := ExtractAPIVersions(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractAPIVersions takes a collection page, extracts all of the elements,
+// and returns them a slice of APIVersion structs. It is effectively a cast.
+func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) {
+ var resp struct {
+ Versions []APIVersion `mapstructure:"versions"`
+ }
+
+ err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Versions, nil
+}
+
+type GetResult struct {
+ gophercloud.CommonResult
+}
+
+func (r GetResult) Extract() (*APIVersion, error) {
+ var resp struct {
+ Version *APIVersion `mapstructure:"version"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Version, nil
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls.go b/openstack/blockstorage/v1/apiversions/urls.go
new file mode 100644
index 0000000..56f8260
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls.go
@@ -0,0 +1,15 @@
+package apiversions
+
+import (
+ "strings"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func getURL(c *gophercloud.ServiceClient, version string) string {
+ return c.ServiceURL(strings.TrimRight(version, "/") + "/")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("")
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls_test.go b/openstack/blockstorage/v1/apiversions/urls_test.go
new file mode 100644
index 0000000..37e9142
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls_test.go
@@ -0,0 +1,26 @@
+package apiversions
+
+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 TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "v1")
+ expected := endpoint + "v1/"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests.go b/openstack/blockstorage/v1/snapshots/requests.go
new file mode 100644
index 0000000..40b44d8
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -0,0 +1,130 @@
+package snapshots
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// CreateOpts contains options for creating a Snapshot. This object is passed to
+// the snapshots.Create function. For more information about these parameters,
+// see the Snapshot object.
+type CreateOpts struct {
+ Description string // OPTIONAL
+ Force bool // OPTIONAL
+ Metadata map[string]interface{} // OPTIONAL
+ Name string // OPTIONAL
+ VolumeID string // REQUIRED
+}
+
+// Create will create a new Snapshot based on the values in CreateOpts. To extract
+// the Snapshot object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+ type snapshot struct {
+ Description *string `json:"display_description,omitempty"`
+ Force bool `json:"force,omitempty"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+ Name *string `json:"display_name,omitempty"`
+ VolumeID *string `json:"volume_id,omitempty"`
+ }
+
+ type request struct {
+ Snapshot snapshot `json:"snapshot"`
+ }
+
+ reqBody := request{
+ Snapshot: snapshot{},
+ }
+
+ reqBody.Snapshot.Description = gophercloud.MaybeString(opts.Description)
+ reqBody.Snapshot.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Snapshot.VolumeID = gophercloud.MaybeString(opts.VolumeID)
+
+ reqBody.Snapshot.Force = opts.Force
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202, 204},
+ })
+ return err
+}
+
+// Get retrieves the Snapshot with the provided ID. To extract the Snapshot object
+// from the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// ListOpts hold options for listing Snapshots. It is passed to the
+// snapshots.List function.
+type ListOpts struct {
+ Name string `q:"display_name"`
+ Status string `q:"status"`
+ VolumeID string `q:"volume_id"`
+}
+
+// List returns Snapshots optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query.String()
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, url, createPage)
+}
+
+// UpdateOpts contain options for updating an existing Snapshot. This object is
+// passed to the snapshots.Update function. For more information about the
+// parameters, see the Snapshot object.
+type UpdateMetadataOpts struct {
+ Metadata map[string]interface{}
+}
+
+// Update will update the Snapshot with provided information. To extract the updated
+// Snapshot from the response, call the ExtractMetadata method on the UpdateResult.
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts *UpdateMetadataOpts) UpdateMetadataResult {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+ }
+
+ reqBody := request{}
+
+ reqBody.Metadata = opts.Metadata
+
+ var res UpdateMetadataResult
+
+ _, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests_test.go b/openstack/blockstorage/v1/snapshots/requests_test.go
new file mode 100644
index 0000000..d29cc0d
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests_test.go
@@ -0,0 +1,198 @@
+package snapshots
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "snapshots": [
+ {
+ "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "display_name": "snapshot-001"
+ },
+ {
+ "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+ "display_name": "snapshot-002"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractSnapshots(page)
+ if err != nil {
+ t.Errorf("Failed to extract snapshots: %v", err)
+ return false, err
+ }
+
+ expected := []Snapshot{
+ Snapshot{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "snapshot-001",
+ },
+ Snapshot{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "snapshot-002",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "snapshot": {
+ "display_name": "snapshot-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, v.Name, "snapshot-001")
+ th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "snapshot": {
+ "display_name": "snapshot-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "snapshot": {
+ "display_name": "snapshot-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "snapshot-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "snapshot-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestUpdateMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/123/metadata", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `
+ {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+ `)
+
+ fmt.Fprintf(w, `
+ {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+ `)
+ })
+
+ expected := map[string]interface{}{"key": "v1"}
+
+ options := &UpdateMetadataOpts{
+ Metadata: map[string]interface{}{
+ "key": "v1",
+ },
+ }
+ actual, err := UpdateMetadata(ServiceClient(), "123", options).ExtractMetadata()
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, actual, expected)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/snapshots/results.go b/openstack/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..9509bca
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,98 @@
+package snapshots
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Snapshot contains all the information associated with an OpenStack Snapshot.
+type Snapshot struct {
+ Status string `mapstructure:"status"` // currect status of the Snapshot
+ Name string `mapstructure:"display_name"` // display name
+ Attachments []string `mapstructure:"attachments"` // instances onto which the Snapshot is attached
+ AvailabilityZone string `mapstructure:"availability_zone"` // logical group
+ Bootable string `mapstructure:"bootable"` // is the Snapshot bootable
+ CreatedAt string `mapstructure:"created_at"` // date created
+ Description string `mapstructure:"display_discription"` // display description
+ VolumeType string `mapstructure:"volume_type"` // see VolumeType object for more information
+ SnapshotID string `mapstructure:"snapshot_id"` // ID of the Snapshot from which this Snapshot was created
+ SourceVolID string `mapstructure:"source_volid"` // ID of the Volume from which this Snapshot was created
+ Metadata map[string]string `mapstructure:"metadata"` // user-defined key-value pairs
+ ID string `mapstructure:"id"` // unique identifier
+ Size int `mapstructure:"size"` // size of the Snapshot, in GB
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+ commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Snapshots.
+func (r ListResult) IsEmpty() (bool, error) {
+ volumes, err := ExtractSnapshots(r)
+ if err != nil {
+ return true, err
+ }
+ return len(volumes) == 0, nil
+}
+
+// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call.
+func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) {
+ var response struct {
+ Snapshots []Snapshot `json:"snapshots"`
+ }
+
+ err := mapstructure.Decode(page.(ListResult).Body, &response)
+ return response.Snapshots, err
+}
+
+// UpdateMetadataResult contains the response body and error from an UpdateMetadata request.
+type UpdateMetadataResult struct {
+ commonResult
+}
+
+// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata.
+func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ m := r.Resp["metadata"].(map[string]interface{})
+
+ return m, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract will get the Snapshot object out of the commonResult object.
+func (r commonResult) Extract() (*Snapshot, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Snapshot *Snapshot `json:"snapshot"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("snapshots: Error decoding snapshots.commonResult: %v", err)
+ }
+ return res.Snapshot, nil
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls.go b/openstack/blockstorage/v1/snapshots/urls.go
new file mode 100644
index 0000000..4d635e8
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls.go
@@ -0,0 +1,27 @@
+package snapshots
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("snapshots")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("snapshots", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return deleteURL(c, id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return createURL(c)
+}
+
+func metadataURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("snapshots", id, "metadata")
+}
+
+func updateMetadataURL(c *gophercloud.ServiceClient, id string) string {
+ return metadataURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls_test.go b/openstack/blockstorage/v1/snapshots/urls_test.go
new file mode 100644
index 0000000..feacf7f
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls_test.go
@@ -0,0 +1,50 @@
+package snapshots
+
+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 TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "snapshots"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "snapshots"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+ actual := metadataURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo/metadata"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateMetadataURL(t *testing.T) {
+ actual := updateMetadataURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo/metadata"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/util.go b/openstack/blockstorage/v1/snapshots/util.go
new file mode 100644
index 0000000..b882875
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/util.go
@@ -0,0 +1,20 @@
+package snapshots
+
+import (
+ "github.com/rackspace/gophercloud"
+)
+
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+ return gophercloud.WaitFor(secs, func() (bool, error) {
+ current, err := Get(c, id).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ if current.Status == status {
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
diff --git a/openstack/blockstorage/v1/volumes/requests.go b/openstack/blockstorage/v1/volumes/requests.go
new file mode 100644
index 0000000..bca27db
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -0,0 +1,150 @@
+package volumes
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// CreateOpts contains options for creating a Volume. This object is passed to
+// the volumes.Create function. For more information about these parameters,
+// see the Volume object.
+type CreateOpts struct {
+ Availability string // OPTIONAL
+ Description string // OPTIONAL
+ Metadata map[string]string // OPTIONAL
+ Name string // OPTIONAL
+ Size int // REQUIRED
+ SnapshotID, SourceVolID, ImageID string // REQUIRED (one of them)
+ VolumeType string // OPTIONAL
+}
+
+// Create will create a new Volume based on the values in CreateOpts. To extract
+// the Volume object from the response, call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+
+ type volume struct {
+ Availability *string `json:"availability_zone,omitempty"`
+ Description *string `json:"display_description,omitempty"`
+ ImageID *string `json:"imageRef,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Name *string `json:"display_name,omitempty"`
+ Size *int `json:"size,omitempty"`
+ SnapshotID *string `json:"snapshot_id,omitempty"`
+ SourceVolID *string `json:"source_volid,omitempty"`
+ VolumeType *string `json:"volume_type,omitempty"`
+ }
+
+ type request struct {
+ Volume volume `json:"volume"`
+ }
+
+ reqBody := request{
+ Volume: volume{},
+ }
+
+ reqBody.Volume.Availability = gophercloud.MaybeString(opts.Availability)
+ reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
+ reqBody.Volume.ImageID = gophercloud.MaybeString(opts.ImageID)
+ reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Volume.Size = gophercloud.MaybeInt(opts.Size)
+ reqBody.Volume.SnapshotID = gophercloud.MaybeString(opts.SnapshotID)
+ reqBody.Volume.SourceVolID = gophercloud.MaybeString(opts.SourceVolID)
+ reqBody.Volume.VolumeType = gophercloud.MaybeString(opts.VolumeType)
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 201},
+ })
+ return res
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202, 204},
+ })
+ return err
+}
+
+// Get retrieves the Volume with the provided ID. To extract the Volume object from
+// the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// ListOpts holds options for listing Volumes. It is passed to the volumes.List
+// function.
+type ListOpts struct {
+ AllTenants bool `q:"all_tenants"` // admin-only option. Set it to true to see all tenant volumes.
+ Metadata map[string]string `q:"metadata"` // List only volumes that contain Metadata.
+ Name string `q:"name"` // List only volumes that have Name as the display name.
+ Status string `q:"status"` // List only volumes that have a status of Status.
+}
+
+// List returns Volumes optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query.String()
+ }
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, listURL(client), createPage)
+}
+
+// UpdateOpts contain options for updating an existing Volume. This object is passed
+// to the volumes.Update function. For more information about the parameters, see
+// the Volume object.
+type UpdateOpts struct {
+ Name string // OPTIONAL
+ Description string // OPTIONAL
+ Metadata map[string]string // OPTIONAL
+}
+
+// Update will update the Volume with provided information. To extract the updated
+// Volume from the response, call the Extract method on the UpdateResult.
+func Update(client *gophercloud.ServiceClient, id string, opts *UpdateOpts) UpdateResult {
+ type update struct {
+ Description *string `json:"display_description,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Name *string `json:"display_name,omitempty"`
+ }
+
+ type request struct {
+ Volume update `json:"volume"`
+ }
+
+ reqBody := request{
+ Volume: update{},
+ }
+
+ reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
+ reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
+
+ var res UpdateResult
+
+ _, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
diff --git a/openstack/blockstorage/v1/volumes/requests_test.go b/openstack/blockstorage/v1/volumes/requests_test.go
new file mode 100644
index 0000000..54ff91d
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests_test.go
@@ -0,0 +1,160 @@
+package volumes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "volumes": [
+ {
+ "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "display_name": "vol-001"
+ },
+ {
+ "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+ "display_name": "vol-002"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVolumes(page)
+ if err != nil {
+ t.Errorf("Failed to extract volumes: %v", err)
+ return false, err
+ }
+
+ expected := []Volume{
+ Volume{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "vol-001",
+ },
+ Volume{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "vol-002",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "volume": {
+ "display_name": "vol-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, v.Name, "vol-001")
+ th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "volume": {
+ "display_name": "vol-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "volume": {
+ "display_name": "vol-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "vol-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "vol-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
new file mode 100644
index 0000000..78c863f
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -0,0 +1,87 @@
+package volumes
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Volume contains all the information associated with an OpenStack Volume.
+type Volume struct {
+ Status string `mapstructure:"status"` // current status of the Volume
+ Name string `mapstructure:"display_name"` // display name
+ Attachments []string `mapstructure:"attachments"` // instances onto which the Volume is attached
+ AvailabilityZone string `mapstructure:"availability_zone"` // logical group
+ Bootable string `mapstructure:"bootable"` // is the volume bootable
+ CreatedAt string `mapstructure:"created_at"` // date created
+ Description string `mapstructure:"display_discription"` // display description
+ VolumeType string `mapstructure:"volume_type"` // see VolumeType object for more information
+ SnapshotID string `mapstructure:"snapshot_id"` // ID of the Snapshot from which the Volume was created
+ SourceVolID string `mapstructure:"source_volid"` // ID of the Volume from which the Volume was created
+ Metadata map[string]string `mapstructure:"metadata"` // user-defined key-value pairs
+ ID string `mapstructure:"id"` // unique identifier
+ Size int `mapstructure:"size"` // size of the Volume, in GB
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+ commonResult
+}
+
+// ListResult is a pagination.pager that is returned from a call to the List function.
+type ListResult struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volumes.
+func (r ListResult) IsEmpty() (bool, error) {
+ volumes, err := ExtractVolumes(r)
+ if err != nil {
+ return true, err
+ }
+ return len(volumes) == 0, nil
+}
+
+// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
+func ExtractVolumes(page pagination.Page) ([]Volume, error) {
+ var response struct {
+ Volumes []Volume `json:"volumes"`
+ }
+
+ err := mapstructure.Decode(page.(ListResult).Body, &response)
+ return response.Volumes, err
+}
+
+// UpdateResult contains the response body and error from an Update request.
+type UpdateResult struct {
+ commonResult
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract will get the Volume object out of the commonResult object.
+func (r commonResult) Extract() (*Volume, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Volume *Volume `json:"volume"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("volumes: Error decoding volumes.commonResult: %v", err)
+ }
+ return res.Volume, nil
+}
diff --git a/openstack/blockstorage/v1/volumes/urls.go b/openstack/blockstorage/v1/volumes/urls.go
new file mode 100644
index 0000000..29629a1
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls.go
@@ -0,0 +1,23 @@
+package volumes
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("volumes")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return createURL(c)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("volumes", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return deleteURL(c, id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return deleteURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumes/urls_test.go b/openstack/blockstorage/v1/volumes/urls_test.go
new file mode 100644
index 0000000..a95270e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls_test.go
@@ -0,0 +1,44 @@
+package volumes
+
+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 TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "volumes"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "volumes"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/volumes/util.go b/openstack/blockstorage/v1/volumes/util.go
new file mode 100644
index 0000000..0e2f16e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/util.go
@@ -0,0 +1,20 @@
+package volumes
+
+import (
+ "github.com/rackspace/gophercloud"
+)
+
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+ return gophercloud.WaitFor(secs, func() (bool, error) {
+ current, err := Get(c, id).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ if current.Status == status {
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests.go b/openstack/blockstorage/v1/volumetypes/requests.go
new file mode 100644
index 0000000..afe650d
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -0,0 +1,75 @@
+package volumetypes
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOpts are options for creating a volume type.
+type CreateOpts struct {
+ // OPTIONAL. See VolumeType.
+ ExtraSpecs map[string]interface{}
+ // OPTIONAL. See VolumeType.
+ Name string
+}
+
+// Create will create a new volume, optionally wih CreateOpts. To extract the
+// created volume type object, call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+ type volumeType struct {
+ ExtraSpecs map[string]interface{} `json:"extra_specs,omitempty"`
+ Name *string `json:"name,omitempty"`
+ }
+
+ type request struct {
+ VolumeType volumeType `json:"volume_type"`
+ }
+
+ reqBody := request{
+ VolumeType: volumeType{},
+ }
+
+ reqBody.VolumeType.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.VolumeType.ExtraSpecs = opts.ExtraSpecs
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
+
+// Delete will delete the volume type with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, err := perigee.Request("GET", getURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ Results: &res.Resp,
+ })
+ res.Err = err
+ return res
+}
+
+// List returns all volume types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, listURL(client), createPage)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests_test.go b/openstack/blockstorage/v1/volumetypes/requests_test.go
new file mode 100644
index 0000000..a9c6512
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests_test.go
@@ -0,0 +1,172 @@
+package volumetypes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "volume_types": [
+ {
+ "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "name": "vol-type-001",
+ "extra_specs": {
+ "capabilities": "gpu"
+ }
+ },
+ {
+ "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+ "name": "vol-type-002",
+ "extra_specs": {}
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVolumeTypes(page)
+ if err != nil {
+ t.Errorf("Failed to extract volume types: %v", err)
+ return false, err
+ }
+
+ expected := []VolumeType{
+ VolumeType{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "vol-type-001",
+ ExtraSpecs: map[string]interface{}{
+ "capabilities": "gpu",
+ },
+ },
+ VolumeType{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "vol-type-002",
+ ExtraSpecs: map[string]interface{}{},
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "volume_type": {
+ "name": "vol-type-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "extra_specs": {
+ "serverNumber": "2"
+ }
+ }
+}
+ `)
+ })
+
+ vt, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"})
+ th.AssertEquals(t, vt.Name, "vol-type-001")
+ th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "volume_type": {
+ "name": "vol-type-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "volume_type": {
+ "name": "vol-type-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "vol-type-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "vol-type-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/results.go b/openstack/blockstorage/v1/volumetypes/results.go
new file mode 100644
index 0000000..8e5932a
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,72 @@
+package volumetypes
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// VolumeType contains all information associated with an OpenStack Volume Type.
+type VolumeType struct {
+ ExtraSpecs map[string]interface{} `json:"extra_specs" mapstructure:"extra_specs"` // user-defined metadata
+ ID string `json:"id" mapstructure:"id"` // unique identifier
+ Name string `json:"name" mapstructure:"name"` // display name
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+ commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volume Types.
+func (r ListResult) IsEmpty() (bool, error) {
+ volumeTypes, err := ExtractVolumeTypes(r)
+ if err != nil {
+ return true, err
+ }
+ return len(volumeTypes) == 0, nil
+}
+
+// ExtractVolumeTypes extracts and returns Volume Types.
+func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) {
+ var response struct {
+ VolumeTypes []VolumeType `mapstructure:"volume_types"`
+ }
+
+ err := mapstructure.Decode(page.(ListResult).Body, &response)
+ return response.VolumeTypes, err
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract will get the Volume Type object out of the commonResult object.
+func (r commonResult) Extract() (*VolumeType, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Volume Type: %v", err)
+ }
+
+ return res.VolumeType, nil
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls.go b/openstack/blockstorage/v1/volumetypes/urls.go
new file mode 100644
index 0000000..cf8367b
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls.go
@@ -0,0 +1,19 @@
+package volumetypes
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("types")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return listURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("types", id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return getURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls_test.go b/openstack/blockstorage/v1/volumetypes/urls_test.go
new file mode 100644
index 0000000..44016e2
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls_test.go
@@ -0,0 +1,38 @@
+package volumetypes
+
+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 TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "types"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "types"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "types/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "types/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/client.go b/openstack/client.go
index eeeb809..1b057d0 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -175,15 +175,12 @@
v3Client.Endpoint = endpoint
}
- result, err := tokens3.Create(v3Client, options, nil)
+ token, err := tokens3.Create(v3Client, options, nil).Extract()
if err != nil {
return err
}
+ client.TokenID = token.ID
- client.TokenID, err = result.TokenID()
- if err != nil {
- return err
- }
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return v3endpointLocator(v3Client, opts)
}
@@ -313,3 +310,13 @@
}
return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
}
+
+// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
+func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("volume")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
+}
diff --git a/openstack/identity/v3/endpoints/requests.go b/openstack/identity/v3/endpoints/requests.go
index 186d0fc..eb52573 100644
--- a/openstack/identity/v3/endpoints/requests.go
+++ b/openstack/identity/v3/endpoints/requests.go
@@ -9,14 +9,6 @@
"github.com/rackspace/gophercloud/pagination"
)
-// maybeString returns nil for empty strings and nil for empty.
-func maybeString(original string) *string {
- if original != "" {
- return &original
- }
- return nil
-}
-
// EndpointOpts contains the subset of Endpoint attributes that should be used to create or update an Endpoint.
type EndpointOpts struct {
Availability gophercloud.Availability
@@ -28,7 +20,7 @@
// Create inserts a new Endpoint into the service catalog.
// Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required.
-func Create(client *gophercloud.ServiceClient, opts EndpointOpts) (*Endpoint, error) {
+func Create(client *gophercloud.ServiceClient, opts EndpointOpts) CreateResult {
// Redefined so that Region can be re-typed as a *string, which can be omitted from the JSON output.
type endpoint struct {
Interface string `json:"interface"`
@@ -42,22 +34,18 @@
Endpoint endpoint `json:"endpoint"`
}
- type response struct {
- Endpoint Endpoint `json:"endpoint"`
- }
-
// Ensure that EndpointOpts is fully populated.
if opts.Availability == "" {
- return nil, ErrAvailabilityRequired
+ return createErr(ErrAvailabilityRequired)
}
if opts.Name == "" {
- return nil, ErrNameRequired
+ return createErr(ErrNameRequired)
}
if opts.URL == "" {
- return nil, ErrURLRequired
+ return createErr(ErrURLRequired)
}
if opts.ServiceID == "" {
- return nil, ErrServiceIDRequired
+ return createErr(ErrServiceIDRequired)
}
// Populate the request body.
@@ -69,20 +57,16 @@
ServiceID: opts.ServiceID,
},
}
- reqBody.Endpoint.Region = maybeString(opts.Region)
+ reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
- var respBody response
- _, err := perigee.Request("POST", getListURL(client), perigee.Options{
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
- Results: &respBody,
+ Results: &result.Resp,
OkCodes: []int{201},
})
- if err != nil {
- return nil, err
- }
-
- return &respBody.Endpoint, nil
+ return result
}
// ListOpts allows finer control over the the endpoints returned by a List call.
@@ -114,13 +98,13 @@
return EndpointPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
- u := getListURL(client) + utils.BuildQuery(q)
+ u := listURL(client) + utils.BuildQuery(q)
return pagination.NewPager(client, u, createPage)
}
// Update changes an existing endpoint with new data.
// All fields are optional in the provided EndpointOpts.
-func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) (*Endpoint, error) {
+func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) UpdateResult {
type endpoint struct {
Interface *string `json:"interface,omitempty"`
Name *string `json:"name,omitempty"`
@@ -133,34 +117,26 @@
Endpoint endpoint `json:"endpoint"`
}
- type response struct {
- Endpoint Endpoint `json:"endpoint"`
- }
-
reqBody := request{Endpoint: endpoint{}}
- reqBody.Endpoint.Interface = maybeString(string(opts.Availability))
- reqBody.Endpoint.Name = maybeString(opts.Name)
- reqBody.Endpoint.Region = maybeString(opts.Region)
- reqBody.Endpoint.URL = maybeString(opts.URL)
- reqBody.Endpoint.ServiceID = maybeString(opts.ServiceID)
+ reqBody.Endpoint.Interface = gophercloud.MaybeString(string(opts.Availability))
+ reqBody.Endpoint.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
+ reqBody.Endpoint.URL = gophercloud.MaybeString(opts.URL)
+ reqBody.Endpoint.ServiceID = gophercloud.MaybeString(opts.ServiceID)
- var respBody response
- _, err := perigee.Request("PATCH", getEndpointURL(client, endpointID), perigee.Options{
+ var result UpdateResult
+ _, result.Err = perigee.Request("PATCH", endpointURL(client, endpointID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
- Results: &respBody,
+ Results: &result.Resp,
OkCodes: []int{200},
})
- if err != nil {
- return nil, err
- }
-
- return &respBody.Endpoint, nil
+ return result
}
// Delete removes an endpoint from the service catalog.
func Delete(client *gophercloud.ServiceClient, endpointID string) error {
- _, err := perigee.Request("DELETE", getEndpointURL(client, endpointID), perigee.Options{
+ _, err := perigee.Request("DELETE", endpointURL(client, endpointID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
diff --git a/openstack/identity/v3/endpoints/requests_test.go b/openstack/identity/v3/endpoints/requests_test.go
index 241d175..c30bd55 100644
--- a/openstack/identity/v3/endpoints/requests_test.go
+++ b/openstack/identity/v3/endpoints/requests_test.go
@@ -59,13 +59,13 @@
client := serviceClient()
- result, err := Create(client, EndpointOpts{
+ actual, err := Create(client, EndpointOpts{
Availability: gophercloud.AvailabilityPublic,
Name: "the-endiest-of-points",
Region: "underground",
URL: "https://1.2.3.4:9000/",
ServiceID: "asdfasdfasdfasdf",
- })
+ }).Extract()
if err != nil {
t.Fatalf("Unable to create an endpoint: %v", err)
}
@@ -79,8 +79,8 @@
URL: "https://1.2.3.4:9000/",
}
- if !reflect.DeepEqual(result, expected) {
- t.Errorf("Expected %#v, was %#v", expected, result)
+ if !reflect.DeepEqual(actual, expected) {
+ t.Errorf("Expected %#v, was %#v", expected, actual)
}
}
@@ -205,7 +205,7 @@
actual, err := Update(client, "12", EndpointOpts{
Name: "renamed",
Region: "somewhere-else",
- })
+ }).Extract()
if err != nil {
t.Fatalf("Unexpected error from Update: %v", err)
}
diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go
index 8da90f3..2dd2357 100644
--- a/openstack/identity/v3/endpoints/results.go
+++ b/openstack/identity/v3/endpoints/results.go
@@ -1,11 +1,51 @@
package endpoints
import (
+ "fmt"
+
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Endpoint, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Endpoint `json:"endpoint"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Endpoint: %v", err)
+ }
+
+ return &res.Endpoint, nil
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// createErr quickly wraps an error in a CreateResult.
+func createErr(err error) CreateResult {
+ return CreateResult{commonResult{gophercloud.CommonResult{Err: err}}}
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+ commonResult
+}
+
// Endpoint describes the entry point for another service's API.
type Endpoint struct {
ID string `mapstructure:"id" json:"id"`
diff --git a/openstack/identity/v3/endpoints/urls.go b/openstack/identity/v3/endpoints/urls.go
index 011cc01..547d7b1 100644
--- a/openstack/identity/v3/endpoints/urls.go
+++ b/openstack/identity/v3/endpoints/urls.go
@@ -2,10 +2,10 @@
import "github.com/rackspace/gophercloud"
-func getListURL(client *gophercloud.ServiceClient) string {
+func listURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("endpoints")
}
-func getEndpointURL(client *gophercloud.ServiceClient, endpointID string) string {
+func endpointURL(client *gophercloud.ServiceClient, endpointID string) string {
return client.ServiceURL("endpoints", endpointID)
}
diff --git a/openstack/identity/v3/endpoints/urls_test.go b/openstack/identity/v3/endpoints/urls_test.go
index fe1fb4a..0b183b7 100644
--- a/openstack/identity/v3/endpoints/urls_test.go
+++ b/openstack/identity/v3/endpoints/urls_test.go
@@ -8,7 +8,7 @@
func TestGetListURL(t *testing.T) {
client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
- url := getListURL(&client)
+ url := listURL(&client)
if url != "http://localhost:5000/v3/endpoints" {
t.Errorf("Unexpected list URL generated: [%s]", url)
}
@@ -16,7 +16,7 @@
func TestGetEndpointURL(t *testing.T) {
client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
- url := getEndpointURL(&client, "1234")
+ url := endpointURL(&client, "1234")
if url != "http://localhost:5000/v3/endpoints/1234" {
t.Errorf("Unexpected service URL generated: [%s]", url)
}
diff --git a/openstack/identity/v3/services/requests.go b/openstack/identity/v3/services/requests.go
index 405a9a6..7816aca 100644
--- a/openstack/identity/v3/services/requests.go
+++ b/openstack/identity/v3/services/requests.go
@@ -14,25 +14,21 @@
}
// Create adds a new service of the requested type to the catalog.
-func Create(client *gophercloud.ServiceClient, serviceType string) (*Service, error) {
+func Create(client *gophercloud.ServiceClient, serviceType string) CreateResult {
type request struct {
Type string `json:"type"`
}
req := request{Type: serviceType}
- var resp response
- _, err := perigee.Request("POST", getListURL(client), perigee.Options{
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
ReqBody: &req,
- Results: &resp,
+ Results: &result.Resp,
OkCodes: []int{201},
})
- if err != nil {
- return nil, err
- }
-
- return &resp.Service, nil
+ return result
}
// ListOpts allows you to query the List method.
@@ -54,7 +50,7 @@
if opts.PerPage != 0 {
q["perPage"] = strconv.Itoa(opts.PerPage)
}
- u := getListURL(client) + utils.BuildQuery(q)
+ u := listURL(client) + utils.BuildQuery(q)
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
return ServicePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
@@ -64,45 +60,38 @@
}
// Get returns additional information about a service, given its ID.
-func Get(client *gophercloud.ServiceClient, serviceID string) (*Service, error) {
- var resp response
- _, err := perigee.Request("GET", getServiceURL(client, serviceID), perigee.Options{
+func Get(client *gophercloud.ServiceClient, serviceID string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", serviceURL(client, serviceID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
- Results: &resp,
+ Results: &result.Resp,
OkCodes: []int{200},
})
- if err != nil {
- return nil, err
- }
- return &resp.Service, nil
+ return result
}
-// Update changes the service type of an existing service.s
-func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) (*Service, error) {
+// Update changes the service type of an existing service.
+func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) UpdateResult {
type request struct {
Type string `json:"type"`
}
req := request{Type: serviceType}
- var resp response
- _, err := perigee.Request("PATCH", getServiceURL(client, serviceID), perigee.Options{
+ var result UpdateResult
+ _, result.Err = perigee.Request("PATCH", serviceURL(client, serviceID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
ReqBody: &req,
- Results: &resp,
+ Results: &result.Resp,
OkCodes: []int{200},
})
- if err != nil {
- return nil, err
- }
-
- return &resp.Service, nil
+ return result
}
// Delete removes an existing service.
// It either deletes all associated endpoints, or fails until all endpoints are deleted.
func Delete(client *gophercloud.ServiceClient, serviceID string) error {
- _, err := perigee.Request("DELETE", getServiceURL(client, serviceID), perigee.Options{
+ _, err := perigee.Request("DELETE", serviceURL(client, serviceID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
diff --git a/openstack/identity/v3/services/requests_test.go b/openstack/identity/v3/services/requests_test.go
index 804f034..a3d345b 100644
--- a/openstack/identity/v3/services/requests_test.go
+++ b/openstack/identity/v3/services/requests_test.go
@@ -45,7 +45,7 @@
client := serviceClient()
- result, err := Create(client, "compute")
+ result, err := Create(client, "compute").Extract()
if err != nil {
t.Fatalf("Unexpected error from Create: %v", err)
}
@@ -161,7 +161,7 @@
client := serviceClient()
- result, err := Get(client, "12345")
+ result, err := Get(client, "12345").Extract()
if err != nil {
t.Fatalf("Error fetching service information: %v", err)
}
@@ -202,13 +202,13 @@
client := serviceClient()
- result, err := Update(client, "12345", "lasermagic")
+ result, err := Update(client, "12345", "lasermagic").Extract()
if err != nil {
t.Fatalf("Unable to update service: %v", err)
}
if result.ID != "12345" {
-
+ t.Fatalf("Expected ID 12345, was %s", result.ID)
}
}
diff --git a/openstack/identity/v3/services/results.go b/openstack/identity/v3/services/results.go
index cccea8e..b4e7bd2 100644
--- a/openstack/identity/v3/services/results.go
+++ b/openstack/identity/v3/services/results.go
@@ -1,11 +1,52 @@
package services
import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Service, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Service `json:"service"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Service: %v", err)
+ }
+
+ return &res.Service, nil
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult is the deferred result of a Get call.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+ commonResult
+}
+
// Service is the result of a list or information query.
type Service struct {
Description *string `json:"description,omitempty"`
diff --git a/openstack/identity/v3/services/urls.go b/openstack/identity/v3/services/urls.go
index 3556238..85443a4 100644
--- a/openstack/identity/v3/services/urls.go
+++ b/openstack/identity/v3/services/urls.go
@@ -2,10 +2,10 @@
import "github.com/rackspace/gophercloud"
-func getListURL(client *gophercloud.ServiceClient) string {
+func listURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("services")
}
-func getServiceURL(client *gophercloud.ServiceClient, serviceID string) string {
+func serviceURL(client *gophercloud.ServiceClient, serviceID string) string {
return client.ServiceURL("services", serviceID)
}
diff --git a/openstack/identity/v3/services/urls_test.go b/openstack/identity/v3/services/urls_test.go
index deded69..5a31b32 100644
--- a/openstack/identity/v3/services/urls_test.go
+++ b/openstack/identity/v3/services/urls_test.go
@@ -6,17 +6,17 @@
"github.com/rackspace/gophercloud"
)
-func TestGetListURL(t *testing.T) {
+func TestListURL(t *testing.T) {
client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
- url := getListURL(&client)
+ url := listURL(&client)
if url != "http://localhost:5000/v3/services" {
t.Errorf("Unexpected list URL generated: [%s]", url)
}
}
-func TestGetServiceURL(t *testing.T) {
+func TestServiceURL(t *testing.T) {
client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
- url := getServiceURL(&client, "1234")
+ url := serviceURL(&client, "1234")
if url != "http://localhost:5000/v3/services/1234" {
t.Errorf("Unexpected service URL generated: [%s]", url)
}
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
index ab4ae05..c8587b6 100644
--- a/openstack/identity/v3/tokens/requests.go
+++ b/openstack/identity/v3/tokens/requests.go
@@ -20,7 +20,7 @@
}
// Create authenticates and either generates a new token, or changes the Scope of an existing token.
-func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) (gophercloud.AuthResults, error) {
+func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
type domainReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
@@ -73,13 +73,13 @@
// Test first for unrecognized arguments.
if options.APIKey != "" {
- return nil, ErrAPIKeyProvided
+ return createErr(ErrAPIKeyProvided)
}
if options.TenantID != "" {
- return nil, ErrTenantIDProvided
+ return createErr(ErrTenantIDProvided)
}
if options.TenantName != "" {
- return nil, ErrTenantNameProvided
+ return createErr(ErrTenantNameProvided)
}
if options.Password == "" {
@@ -87,16 +87,16 @@
// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
// parameters.
if options.Username != "" {
- return nil, ErrUsernameWithToken
+ return createErr(ErrUsernameWithToken)
}
if options.UserID != "" {
- return nil, ErrUserIDWithToken
+ return createErr(ErrUserIDWithToken)
}
if options.DomainID != "" {
- return nil, ErrDomainIDWithToken
+ return createErr(ErrDomainIDWithToken)
}
if options.DomainName != "" {
- return nil, ErrDomainNameWithToken
+ return createErr(ErrDomainNameWithToken)
}
// Configure the request for Token authentication.
@@ -106,7 +106,7 @@
}
} else {
// If no password or token ID are available, authentication can't continue.
- return nil, ErrMissingPassword
+ return createErr(ErrMissingPassword)
}
} else {
// Password authentication.
@@ -114,23 +114,23 @@
// At least one of Username and UserID must be specified.
if options.Username == "" && options.UserID == "" {
- return nil, ErrUsernameOrUserID
+ return createErr(ErrUsernameOrUserID)
}
if options.Username != "" {
// If Username is provided, UserID may not be provided.
if options.UserID != "" {
- return nil, ErrUsernameOrUserID
+ return createErr(ErrUsernameOrUserID)
}
// Either DomainID or DomainName must also be specified.
if options.DomainID == "" && options.DomainName == "" {
- return nil, ErrDomainIDOrDomainName
+ return createErr(ErrDomainIDOrDomainName)
}
if options.DomainID != "" {
if options.DomainName != "" {
- return nil, ErrDomainIDOrDomainName
+ return createErr(ErrDomainIDOrDomainName)
}
// Configure the request for Username and Password authentication with a DomainID.
@@ -158,10 +158,10 @@
if options.UserID != "" {
// If UserID is specified, neither DomainID nor DomainName may be.
if options.DomainID != "" {
- return nil, ErrDomainIDWithUserID
+ return createErr(ErrDomainIDWithUserID)
}
if options.DomainName != "" {
- return nil, ErrDomainNameWithUserID
+ return createErr(ErrDomainNameWithUserID)
}
// Configure the request for UserID and Password authentication.
@@ -177,10 +177,10 @@
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
if scope.DomainID == "" && scope.DomainName == "" {
- return nil, ErrScopeDomainIDOrDomainName
+ return createErr(ErrScopeDomainIDOrDomainName)
}
if scope.ProjectID != "" {
- return nil, ErrScopeProjectIDOrProjectName
+ return createErr(ErrScopeProjectIDOrProjectName)
}
if scope.DomainID != "" {
@@ -205,10 +205,10 @@
} else if scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
if scope.DomainID != "" {
- return nil, ErrScopeProjectIDAlone
+ return createErr(ErrScopeProjectIDAlone)
}
if scope.DomainName != "" {
- return nil, ErrScopeProjectIDAlone
+ return createErr(ErrScopeProjectIDAlone)
}
// ProjectID
@@ -218,7 +218,7 @@
} else if scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if scope.DomainName != "" {
- return nil, ErrScopeDomainIDOrDomainName
+ return createErr(ErrScopeDomainIDOrDomainName)
}
// DomainID
@@ -226,51 +226,45 @@
Domain: &domainReq{ID: &scope.DomainID},
}
} else if scope.DomainName != "" {
- return nil, ErrScopeDomainName
+ return createErr(ErrScopeDomainName)
} else {
- return nil, ErrScopeEmpty
+ return createErr(ErrScopeEmpty)
}
}
- var result TokenCreateResult
- response, err := perigee.Request("POST", getTokenURL(c), perigee.Options{
+ var result CreateResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("POST", tokenURL(c), perigee.Options{
ReqBody: &req,
- Results: &result.response,
+ Results: &result.Resp,
OkCodes: []int{201},
})
- if err != nil {
- return nil, err
+ if result.Err != nil {
+ return result
}
-
- // Extract the token ID from the response, if present.
- result.tokenID = response.HttpResponse.Header.Get("X-Subject-Token")
-
- return &result, nil
+ result.header = response.HttpResponse.Header
+ return result
}
// Get validates and retrieves information about another token.
-func Get(c *gophercloud.ServiceClient, token string) (*TokenCreateResult, error) {
- var result TokenCreateResult
-
- response, err := perigee.Request("GET", getTokenURL(c), perigee.Options{
+func Get(c *gophercloud.ServiceClient, token string) GetResult {
+ var result GetResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("GET", tokenURL(c), perigee.Options{
MoreHeaders: subjectTokenHeaders(c, token),
- Results: &result.response,
+ Results: &result.Resp,
OkCodes: []int{200, 203},
})
-
- if err != nil {
- return nil, err
+ if result.Err != nil {
+ return result
}
-
- // Extract the token ID from the response, if present.
- result.tokenID = response.HttpResponse.Header.Get("X-Subject-Token")
-
- return &result, nil
+ result.header = response.HttpResponse.Header
+ return result
}
// Validate determines if a specified token is valid or not.
func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
- response, err := perigee.Request("HEAD", getTokenURL(c), perigee.Options{
+ response, err := perigee.Request("HEAD", tokenURL(c), perigee.Options{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{204, 404},
})
@@ -283,7 +277,7 @@
// Revoke immediately makes specified token invalid.
func Revoke(c *gophercloud.ServiceClient, token string) error {
- _, err := perigee.Request("DELETE", getTokenURL(c), perigee.Options{
+ _, err := perigee.Request("DELETE", tokenURL(c), perigee.Options{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{204},
})
diff --git a/openstack/identity/v3/tokens/requests_test.go b/openstack/identity/v3/tokens/requests_test.go
index d0813ac..367c73c 100644
--- a/openstack/identity/v3/tokens/requests_test.go
+++ b/openstack/identity/v3/tokens/requests_test.go
@@ -29,10 +29,14 @@
testhelper.TestJSONRequest(t, r, requestJSON)
w.WriteHeader(http.StatusCreated)
- fmt.Fprintf(w, `{}`)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
})
- _, err := Create(&client, options, scope)
+ _, err := Create(&client, options, scope).Extract()
if err != nil {
t.Errorf("Create returned an error: %v", err)
}
@@ -50,7 +54,7 @@
client.Provider.TokenID = "abcdef123456"
}
- _, err := Create(&client, options, scope)
+ _, err := Create(&client, options, scope).Extract()
if err == nil {
t.Errorf("Create did NOT return an error")
}
@@ -250,18 +254,21 @@
w.Header().Add("X-Subject-Token", "aaa111")
w.WriteHeader(http.StatusCreated)
- fmt.Fprintf(w, `{}`)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
})
options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"}
- result, err := Create(&client, options, nil)
+ token, err := Create(&client, options, nil).Extract()
if err != nil {
- t.Errorf("Create returned an error: %v", err)
+ t.Fatalf("Create returned an error: %v", err)
}
- token, _ := result.TokenID()
- if token != "aaa111" {
- t.Errorf("Expected token to be aaa111, but was %s", token)
+ if token.ID != "aaa111" {
+ t.Errorf("Expected token to be aaa111, but was %s", token.ID)
}
}
@@ -413,19 +420,14 @@
`)
})
- result, err := Get(&client, "abcdef12345")
+ token, err := Get(&client, "abcdef12345").Extract()
if err != nil {
t.Errorf("Info returned an error: %v", err)
}
- expires, err := result.ExpiresAt()
- if err != nil {
- t.Errorf("Error extracting token expiration time: %v", err)
- }
-
expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014")
- if expires != expected {
- t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), expires.Format(time.UnixDate))
+ if token.ExpiresAt != expected {
+ t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate))
}
}
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index 8e0f018..c970c67 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -1,46 +1,81 @@
package tokens
import (
+ "net/http"
"time"
"github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
)
// RFC3339Milli describes the time format used by identity API responses.
const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
-// TokenCreateResult contains the document structure returned from a Create call.
-type TokenCreateResult struct {
- response map[string]interface{}
- tokenID string
+// commonResult is the deferred result of a Create or a Get call.
+type commonResult struct {
+ gophercloud.CommonResult
+
+ // header stores the headers from the original HTTP response because token responses are returned in an X-Subject-Token header.
+ header http.Header
}
-// TokenID retrieves a token generated by a Create call from an token creation response.
-func (r *TokenCreateResult) TokenID() (string, error) {
- return r.tokenID, nil
-}
-
-// ExpiresAt retrieves the token expiration time.
-func (r *TokenCreateResult) ExpiresAt() (time.Time, error) {
- type tokenResp struct {
- ExpiresAt string `mapstructure:"expires_at"`
+// Extract interprets a commonResult as a Token.
+func (r commonResult) Extract() (*Token, error) {
+ if r.Err != nil {
+ return nil, r.Err
}
- type response struct {
- Token tokenResp `mapstructure:"token"`
+ var response struct {
+ Token struct {
+ ExpiresAt string `mapstructure:"expires_at"`
+ } `mapstructure:"token"`
}
- var resp response
- err := mapstructure.Decode(r.response, &resp)
+ var token Token
+
+ // Parse the token itself from the stored headers.
+ token.ID = r.header.Get("X-Subject-Token")
+
+ err := mapstructure.Decode(r.Resp, &response)
if err != nil {
- return time.Time{}, err
+ return nil, err
}
// Attempt to parse the timestamp.
- ts, err := time.Parse(RFC3339Milli, resp.Token.ExpiresAt)
+ token.ExpiresAt, err = time.Parse(RFC3339Milli, response.Token.ExpiresAt)
if err != nil {
- return time.Time{}, err
+ return nil, err
}
- return ts, nil
+ return &token, nil
+}
+
+// CreateResult is the deferred response from a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// createErr quickly creates a CreateResult that reports an error.
+func createErr(err error) CreateResult {
+ return CreateResult{
+ commonResult: commonResult{
+ CommonResult: gophercloud.CommonResult{Err: err},
+ header: nil,
+ },
+ }
+}
+
+// GetResult is the deferred response from a Get call.
+type GetResult struct {
+ commonResult
+}
+
+// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
+// Each Token is valid for a set length of time.
+type Token struct {
+ // ID is the issued token.
+ ID string
+
+ // ExpiresAt is the timestamp at which this token will no longer be accepted.
+ ExpiresAt time.Time
}
diff --git a/openstack/identity/v3/tokens/results_test.go b/openstack/identity/v3/tokens/results_test.go
deleted file mode 100644
index 669db61..0000000
--- a/openstack/identity/v3/tokens/results_test.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package tokens
-
-import (
- "testing"
- "time"
-)
-
-func TestTokenID(t *testing.T) {
- result := TokenCreateResult{tokenID: "1234"}
-
- token, _ := result.TokenID()
- if token != "1234" {
- t.Errorf("Expected tokenID of 1234, got %s", token)
- }
-}
-
-func TestExpiresAt(t *testing.T) {
- resp := map[string]interface{}{
- "token": map[string]string{
- "expires_at": "2013-02-02T18:30:59.000000Z",
- },
- }
-
- result := TokenCreateResult{
- tokenID: "1234",
- response: resp,
- }
-
- expected, _ := time.Parse(time.UnixDate, "Sat Feb 2 18:30:59 UTC 2013")
- actual, err := result.ExpiresAt()
- if err != nil {
- t.Errorf("Error extraction expiration time: %v", err)
- }
- if actual != expected {
- t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), actual.Format(time.UnixDate))
- }
-}
diff --git a/openstack/identity/v3/tokens/urls.go b/openstack/identity/v3/tokens/urls.go
index 5b47c02..360b60a 100644
--- a/openstack/identity/v3/tokens/urls.go
+++ b/openstack/identity/v3/tokens/urls.go
@@ -2,6 +2,6 @@
import "github.com/rackspace/gophercloud"
-func getTokenURL(c *gophercloud.ServiceClient) string {
+func tokenURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("auth", "tokens")
}
diff --git a/openstack/identity/v3/tokens/urls_test.go b/openstack/identity/v3/tokens/urls_test.go
index 5ff8bc6..549c398 100644
--- a/openstack/identity/v3/tokens/urls_test.go
+++ b/openstack/identity/v3/tokens/urls_test.go
@@ -14,7 +14,7 @@
client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()}
expected := testhelper.Endpoint() + "auth/tokens"
- actual := getTokenURL(&client)
+ actual := tokenURL(&client)
if actual != expected {
t.Errorf("Expected URL %s, but was %s", expected, actual)
}
diff --git a/openstack/storage/v1/accounts/accounts.go b/openstack/objectstorage/v1/accounts/accounts.go
similarity index 100%
rename from openstack/storage/v1/accounts/accounts.go
rename to openstack/objectstorage/v1/accounts/accounts.go
diff --git a/openstack/objectstorage/v1/accounts/accounts_test.go b/openstack/objectstorage/v1/accounts/accounts_test.go
new file mode 100644
index 0000000..2c2a84a
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/accounts_test.go
@@ -0,0 +1,19 @@
+package accounts
+
+import (
+ "net/http"
+ "reflect"
+ "testing"
+)
+
+func TestExtractAccountMetadata(t *testing.T) {
+ getResult := &http.Response{}
+
+ expected := map[string]string{}
+
+ actual := ExtractMetadata(getResult)
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected: %+v\nActual:%+v", expected, actual)
+ }
+}
diff --git a/openstack/objectstorage/v1/accounts/doc.go b/openstack/objectstorage/v1/accounts/doc.go
new file mode 100644
index 0000000..5c94e1a
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/doc.go
@@ -0,0 +1,5 @@
+/* The accounts package defines operations performed on an object-storage account.
+
+Reference: http://developer.openstack.org/api-ref-objectstorage-v1.html#storage_account_services
+*/
+package accounts
diff --git a/openstack/storage/v1/accounts/requests.go b/openstack/objectstorage/v1/accounts/requests.go
similarity index 87%
rename from openstack/storage/v1/accounts/requests.go
rename to openstack/objectstorage/v1/accounts/requests.go
index d5b623a..c738573 100644
--- a/openstack/storage/v1/accounts/requests.go
+++ b/openstack/objectstorage/v1/accounts/requests.go
@@ -22,7 +22,7 @@
h["X-Account-Meta-"+k] = v
}
- _, err := perigee.Request("POST", getAccountURL(c), perigee.Options{
+ _, err := perigee.Request("POST", accountURL(c), perigee.Options{
MoreHeaders: h,
OkCodes: []int{204},
})
@@ -38,7 +38,7 @@
h[k] = v
}
- resp, err := perigee.Request("HEAD", getAccountURL(c), perigee.Options{
+ resp, err := perigee.Request("HEAD", accountURL(c), perigee.Options{
MoreHeaders: h,
OkCodes: []int{204},
})
diff --git a/openstack/objectstorage/v1/accounts/requests_test.go b/openstack/objectstorage/v1/accounts/requests_test.go
new file mode 100644
index 0000000..e1450d4
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/requests_test.go
@@ -0,0 +1,55 @@
+package accounts
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenId = "abcabcabcabc"
+
+var metadata = map[string]string{"gophercloud-test": "accounts"}
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenId},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestUpdateAccount(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ err := Update(client, UpdateOpts{Metadata: metadata})
+ if err != nil {
+ t.Fatalf("Unable to update account: %v", err)
+ }
+}
+
+func TestGetAccount(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "HEAD")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ _, err := Get(client, GetOpts{})
+ if err != nil {
+ t.Fatalf("Unable to get account metadata: %v", err)
+ }
+}
diff --git a/openstack/objectstorage/v1/accounts/urls.go b/openstack/objectstorage/v1/accounts/urls.go
new file mode 100644
index 0000000..53b1343
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/urls.go
@@ -0,0 +1,8 @@
+package accounts
+
+import "github.com/rackspace/gophercloud"
+
+// accountURL returns the URI for making Account requests.
+func accountURL(c *gophercloud.ServiceClient) string {
+ return c.Endpoint
+}
diff --git a/openstack/objectstorage/v1/accounts/urls_test.go b/openstack/objectstorage/v1/accounts/urls_test.go
new file mode 100644
index 0000000..f127a5e
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/urls_test.go
@@ -0,0 +1,17 @@
+package accounts
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func TestAccountURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v3/",
+ }
+ url := accountURL(&client)
+ if url != "http://localhost:5000/v3/" {
+ t.Errorf("Unexpected service URL generated: [%s]", url)
+ }
+}
diff --git a/openstack/objectstorage/v1/containers/doc.go b/openstack/objectstorage/v1/containers/doc.go
new file mode 100644
index 0000000..9b6ac17
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/doc.go
@@ -0,0 +1,5 @@
+/* The containers package defines operations performed on an object-storage container.
+
+Reference: http://developer.openstack.org/api-ref-objectstorage-v1.html#storage_container_services
+*/
+package containers
diff --git a/openstack/objectstorage/v1/containers/requests.go b/openstack/objectstorage/v1/containers/requests.go
new file mode 100644
index 0000000..d772a43
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/requests.go
@@ -0,0 +1,161 @@
+package containers
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts is a structure that holds options for listing containers.
+type ListOpts struct {
+ Full bool
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ EndMarker string `q:"end_marker"`
+ Format string `q:"format"`
+ Prefix string `q:"prefix"`
+ Delimiter [1]byte `q:"delimiter"`
+}
+
+// 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 *ListOpts) pagination.Pager {
+ var headers map[string]string
+
+ url := accountURL(c)
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query.String()
+
+ if !opts.Full {
+ headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+ }
+ } else {
+ headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ p := ContainerPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ pager := pagination.NewPager(c, url, createPage)
+ pager.Headers = headers
+ return pager
+}
+
+// CreateOpts is a structure that holds parameters for creating a container.
+type CreateOpts struct {
+ Metadata map[string]string
+ ContainerRead string `h:"X-Container-Read"`
+ ContainerSyncTo string `h:"X-Container-Sync-To"`
+ ContainerSyncKey string `h:"X-Container-Sync-Key"`
+ ContainerWrite string `h:"X-Container-Write"`
+ ContentType string `h:"Content-Type"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+ IfNoneMatch string `h:"If-None-Match"`
+ VersionsLocation string `h:"X-Versions-Location"`
+}
+
+// Create is a function that creates a new container.
+func Create(c *gophercloud.ServiceClient, containerName string, opts *CreateOpts) CreateResult {
+ var res CreateResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+ }
+
+ resp, err := perigee.Request("PUT", containerURL(c, containerName), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{201, 204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// Delete is a function that deletes a container.
+func Delete(c *gophercloud.ServiceClient, containerName string) DeleteResult {
+ var res DeleteResult
+ resp, err := perigee.Request("DELETE", containerURL(c, containerName), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting a
+// container's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ ContainerRead string `h:"X-Container-Read"`
+ ContainerSyncTo string `h:"X-Container-Sync-To"`
+ ContainerSyncKey string `h:"X-Container-Sync-Key"`
+ 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"`
+}
+
+// Update is a function that creates, updates, or deletes a container's metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts *UpdateOpts) UpdateResult {
+ var res UpdateResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+ }
+
+ resp, err := perigee.Request("POST", containerURL(c, containerName), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// 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) GetResult {
+ var res GetResult
+ resp, err := perigee.Request("HEAD", containerURL(c, containerName), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
diff --git a/openstack/objectstorage/v1/containers/requests_test.go b/openstack/objectstorage/v1/containers/requests_test.go
new file mode 100644
index 0000000..1ab09a9
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/requests_test.go
@@ -0,0 +1,206 @@
+package containers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const (
+ tokenId = "abcabcabcabc"
+)
+
+var metadata = map[string]string{"gophercloud-test": "containers"}
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenId},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestListContainerInfo(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `[
+ {
+ "count": 0,
+ "bytes": 0,
+ "name": "janeausten"
+ },
+ {
+ "count": 1,
+ "bytes": 14,
+ "name": "marktwain"
+ }
+ ]`)
+ case "marktwain":
+ fmt.Fprintf(w, `[]`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ count := 0
+ List(client, &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractInfo(page)
+ if err != nil {
+ t.Errorf("Failed to extract container info: %v", err)
+ return false, err
+ }
+
+ expected := []Container{
+ Container{
+ Count: 0,
+ Bytes: 0,
+ Name: "janeausten",
+ },
+ Container{
+ Count: 1,
+ Bytes: 14,
+ Name: "marktwain",
+ },
+ }
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestListContainerNames(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "text/plain")
+
+ w.Header().Set("Content-Type", "text/plain")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, "janeausten\nmarktwain\n")
+ case "marktwain":
+ fmt.Fprintf(w, ``)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ count := 0
+ List(client, &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
+ }
+
+ expected := []string{"janeausten", "marktwain"}
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreateContainer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PUT")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ _, err := Create(client, "testContainer", nil).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error creating container: %v", err)
+ }
+}
+
+func TestDeleteContainer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ _, err := Delete(client, "testContainer").ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error deleting container: %v", err)
+ }
+}
+
+func TestUpateContainer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ _, err := Update(client, "testContainer", nil).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error updating container metadata: %v", err)
+ }
+}
+
+func TestGetContainer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "HEAD")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ _, err := Get(client, "testContainer").ExtractMetadata()
+ if err != nil {
+ t.Fatalf("Unexpected error getting container metadata: %v", err)
+ }
+}
diff --git a/openstack/objectstorage/v1/containers/results.go b/openstack/objectstorage/v1/containers/results.go
new file mode 100644
index 0000000..80425bd
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/results.go
@@ -0,0 +1,137 @@
+package containers
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type Container struct {
+ Bytes int `json:"bytes" mapstructure:"bytes"`
+ Count int `json:"count" mapstructure:"count"`
+ Name string `json:"name" mapstructure:"name"`
+}
+
+// ListResult is a *http.Response that is returned from a call to the List function.
+type ContainerPage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no container names.
+func (r ContainerPage) IsEmpty() (bool, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return true, err
+ }
+ return len(names) == 0, nil
+}
+
+// LastMarker returns the last container name in a ListResult.
+func (r ContainerPage) LastMarker() (string, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return "", err
+ }
+ if len(names) == 0 {
+ return "", nil
+ }
+ return names[len(names)-1], nil
+}
+
+// ExtractInfo is a function that takes a ListResult and returns the containers' information.
+func ExtractInfo(page pagination.Page) ([]Container, error) {
+ untyped := page.(ContainerPage).Body.([]interface{})
+ results := make([]Container, len(untyped))
+ for index, each := range untyped {
+ container := each.(map[string]interface{})
+ err := mapstructure.Decode(container, &results[index])
+ if err != nil {
+ return results, err
+ }
+ }
+ return results, nil
+}
+
+// ExtractNames is a function that takes a ListResult and returns the containers' names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+ casted := page.(ContainerPage)
+ ct := casted.Header.Get("Content-Type")
+
+ switch {
+ case strings.HasPrefix(ct, "application/json"):
+ parsed, err := ExtractInfo(page)
+ if err != nil {
+ return nil, err
+ }
+
+ names := make([]string, 0, len(parsed))
+ for _, container := range parsed {
+ names = append(names, container.Name)
+ }
+ return names, nil
+ case strings.HasPrefix(ct, "text/plain"):
+ names := make([]string, 0, 50)
+
+ body := string(page.(ContainerPage).Body.([]uint8))
+ for _, name := range strings.Split(body, "\n") {
+ if len(name) > 0 {
+ names = append(names, name)
+ }
+ }
+
+ return names, nil
+ default:
+ return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+ }
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ Resp *http.Response
+ Err error
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the container.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+ metadata := make(map[string]string)
+ for k, v := range gr.Resp.Header {
+ if strings.HasPrefix(k, "X-Container-Meta-") {
+ key := strings.TrimPrefix(k, "X-Container-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata, nil
+}
+
+type commonResult struct {
+ Resp *http.Response
+ Err error
+}
+
+func (cr commonResult) ExtractHeaders() (http.Header, error) {
+ var headers http.Header
+ if cr.Err != nil {
+ return headers, cr.Err
+ }
+
+ return cr.Resp.Header, nil
+}
+
+type CreateResult struct {
+ commonResult
+}
+
+type UpdateResult struct {
+ commonResult
+}
+
+type DeleteResult struct {
+ commonResult
+}
diff --git a/openstack/objectstorage/v1/containers/urls.go b/openstack/objectstorage/v1/containers/urls.go
new file mode 100644
index 0000000..2a06f95
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/urls.go
@@ -0,0 +1,13 @@
+package containers
+
+import "github.com/rackspace/gophercloud"
+
+// accountURL returns the URI used to list Containers.
+func accountURL(c *gophercloud.ServiceClient) string {
+ return c.Endpoint
+}
+
+// containerURL returns the URI for making Container requests.
+func containerURL(c *gophercloud.ServiceClient, container string) string {
+ return c.ServiceURL(container)
+}
diff --git a/openstack/objectstorage/v1/containers/urls_test.go b/openstack/objectstorage/v1/containers/urls_test.go
new file mode 100644
index 0000000..da37bf6
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/urls_test.go
@@ -0,0 +1,29 @@
+package containers
+
+import (
+ "testing"
+ "github.com/rackspace/gophercloud"
+)
+
+func TestAccountURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v1/",
+ }
+ expected := "http://localhost:5000/v1/"
+ actual := accountURL(&client)
+ if actual != expected {
+ t.Errorf("Unexpected service URL generated: [%s]", actual)
+ }
+
+}
+
+func TestContainerURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v1/",
+ }
+ expected := "http://localhost:5000/v1/testContainer"
+ actual := containerURL(&client, "testContainer")
+ if actual != expected {
+ t.Errorf("Unexpected service URL generated: [%s]", actual)
+ }
+}
diff --git a/openstack/objectstorage/v1/objects/doc.go b/openstack/objectstorage/v1/objects/doc.go
new file mode 100644
index 0000000..2a7461b
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/doc.go
@@ -0,0 +1,5 @@
+/* The objects package defines operations performed on an object-storage object.
+
+Reference: http://developer.openstack.org/api-ref-objectstorage-v1.html#storage_object_services
+*/
+package objects
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
new file mode 100644
index 0000000..bc21496
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -0,0 +1,317 @@
+package objects
+
+import (
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts is a structure that holds parameters for listing objects.
+type ListOpts struct {
+ Full bool
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ EndMarker string `q:"end_marker"`
+ Format string `q:"format"`
+ Prefix string `q:"prefix"`
+ Delimiter [1]byte `q:"delimiter"`
+ Path string `q:"path"`
+}
+
+// List is a function that retrieves all objects in a container. It also returns the details
+// for the container. To extract only the object information or names, pass the ListResult
+// response to the ExtractInfo or ExtractNames function, respectively.
+func List(c *gophercloud.ServiceClient, containerName string, opts *ListOpts) pagination.Pager {
+ var headers map[string]string
+
+ url := containerURL(c, containerName)
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ fmt.Printf("Error building query string: %v", err)
+ return pagination.Pager{Err: err}
+ }
+ url += query.String()
+
+ if !opts.Full {
+ headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+ }
+ } else {
+ headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ p := ObjectPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ pager := pagination.NewPager(c, url, createPage)
+ pager.Headers = headers
+ return pager
+}
+
+// DownloadOpts is a structure that holds parameters for downloading an object.
+type DownloadOpts struct {
+ IfMatch string `h:"If-Match"`
+ IfModifiedSince time.Time `h:"If-Modified-Since"`
+ IfNoneMatch string `h:"If-None-Match"`
+ IfUnmodifiedSince time.Time `h:"If-Unmodified-Since"`
+ Range string `h:"Range"`
+ Expires string `q:"expires"`
+ MultipartManifest string `q:"multipart-manifest"`
+ Signature string `q:"signature"`
+}
+
+// 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 *DownloadOpts) DownloadResult {
+ var res DownloadResult
+
+ url := objectURL(c, containerName, objectName)
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ url += query.String()
+ }
+
+ resp, err := perigee.Request("GET", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{200},
+ })
+ res.Err = err
+ res.Resp = &resp.HttpResponse
+ return res
+}
+
+// CreateOpts is a structure that holds parameters for creating an object.
+type CreateOpts 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"`
+ DeleteAfter int `h:"X-Delete-After"`
+ DeleteAt int `h:"X-Delete-At"`
+ DetectContentType string `h:"X-Detect-Content-Type"`
+ ETag string `h:"ETag"`
+ IfNoneMatch string `h:"If-None-Match"`
+ ObjectManifest string `h:"X-Object-Manifest"`
+ TransferEncoding string `h:"Transfer-Encoding"`
+ Expires string `q:"expires"`
+ MultipartManifest string `q:"multiple-manifest"`
+ Signature string `q:"signature"`
+}
+
+// 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 *CreateOpts) CreateResult {
+ var res CreateResult
+ var reqBody []byte
+
+ url := objectURL(c, containerName, objectName)
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ url += query.String()
+ }
+
+ if content != nil {
+ reqBody = make([]byte, 0)
+ _, err := content.Read(reqBody)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ }
+
+ resp, err := perigee.Request("PUT", url, perigee.Options{
+ ReqBody: reqBody,
+ MoreHeaders: h,
+ OkCodes: []int{201},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// 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"`
+ ContentType string `h:"Content-Type"`
+ Destination string `h:"Destination,required"`
+}
+
+// Copy is a function that copies one object to another.
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts *CopyOpts) CopyResult {
+ var res CopyResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts == nil {
+ res.Err = fmt.Errorf("Required CopyOpts field 'Destination' not set.")
+ return res
+ }
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+
+ url := objectURL(c, containerName, objectName)
+ resp, err := perigee.Request("COPY", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{201},
+ })
+ res.Resp = &resp.HttpResponse
+ return res
+}
+
+// DeleteOpts is a structure that holds parameters for deleting an object.
+type DeleteOpts struct {
+ MultipartManifest string `q:"multipart-manifest"`
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts *DeleteOpts) DeleteResult {
+ var res DeleteResult
+ url := objectURL(c, containerName, objectName)
+
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ url += query.String()
+ }
+
+ resp, err := perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// GetOpts is a structure that holds parameters for getting an object's metadata.
+type GetOpts struct {
+ Expires string `q:"expires"`
+ Signature string `q:"signature"`
+}
+
+// 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 *GetOpts) GetResult {
+ var res GetResult
+ url := objectURL(c, containerName, objectName)
+
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ url += query.String()
+ }
+
+ resp, err := perigee.Request("HEAD", url, perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 204},
+ })
+ res.Err = err
+ res.Resp = &resp.HttpResponse
+ return res
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
+// object's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ ContentDisposition string `h:"Content-Disposition"`
+ ContentEncoding string `h:"Content-Encoding"`
+ ContentType string `h:"Content-Type"`
+ DeleteAfter int `h:"X-Delete-After"`
+ DeleteAt int `h:"X-Delete-At"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+}
+
+// Update is a function that creates, updates, or deletes an object's metadata.
+func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts *UpdateOpts) UpdateResult {
+ var res UpdateResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+ }
+
+ url := objectURL(c, containerName, objectName)
+ resp, err := perigee.Request("POST", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
diff --git a/openstack/objectstorage/v1/objects/requests_test.go b/openstack/objectstorage/v1/objects/requests_test.go
new file mode 100644
index 0000000..15956cd
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/requests_test.go
@@ -0,0 +1,264 @@
+package objects
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const (
+ tokenId = "abcabcabcabc"
+)
+
+var metadata = map[string]string{"Gophercloud-Test": "objects"}
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenId},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestDownloadObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, "Successful download with Gophercloud")
+ })
+
+ client := serviceClient()
+ content, err := Download(client, "testContainer", "testObject", nil).ExtractContent()
+ if err != nil {
+ t.Fatalf("Unexpected error downloading object: %v", err)
+ }
+ if string(content) != "Successful download with Gophercloud" {
+ t.Errorf("Expected %s, got %s", "Successful download with Gophercloud", content)
+ }
+}
+
+func TestListObjectInfo(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `[
+ {
+ "hash": "451e372e48e0f6b1114fa0724aa79fa1",
+ "last_modified": "2009-11-10 23:00:00 +0000 UTC",
+ "bytes": 14,
+ "name": "goodbye",
+ "content_type": "application/octet-stream"
+ },
+ {
+ "hash": "451e372e48e0f6b1114fa0724aa79fa1",
+ "last_modified": "2009-11-10 23:00:00 +0000 UTC",
+ "bytes": 14,
+ "name": "hello",
+ "content_type": "application/octet-stream"
+ }
+ ]`)
+ case "hello":
+ fmt.Fprintf(w, `[]`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ count := 0
+ err := List(client, "testContainer", &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractInfo(page)
+ if err != nil {
+ t.Errorf("Failed to extract object info: %v", err)
+ return false, err
+ }
+
+ expected := []Object{
+ Object{
+ Hash: "451e372e48e0f6b1114fa0724aa79fa1",
+ LastModified: "2009-11-10 23:00:00 +0000 UTC",
+ Bytes: 14,
+ Name: "goodbye",
+ ContentType: "application/octet-stream",
+ },
+ Object{
+ Hash: "451e372e48e0f6b1114fa0724aa79fa1",
+ LastModified: "2009-11-10 23:00:00 +0000 UTC",
+ Bytes: 14,
+ Name: "hello",
+ ContentType: "application/octet-stream",
+ },
+ }
+
+ testhelper.CheckDeepEquals(t, actual, expected)
+
+ return true, nil
+ })
+ if err != nil {
+ t.Error(err)
+ }
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestListObjectNames(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "text/plain")
+
+ w.Header().Set("Content-Type", "text/plain")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, "hello\ngoodbye\n")
+ case "goodbye":
+ fmt.Fprintf(w, "")
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ count := 0
+ List(client, "testContainer", &ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNames(page)
+ if err != nil {
+ t.Errorf("Failed to extract object names: %v", err)
+ return false, err
+ }
+
+ expected := []string{"hello", "goodbye"}
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreateObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PUT")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ client := serviceClient()
+ content := bytes.NewBufferString("Did gyre and gimble in the wabe")
+ _, err := Create(client, "testContainer", "testObject", content, nil).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error creating object: %v", err)
+ }
+}
+
+func TestCopyObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "COPY")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "Destination", "/newTestContainer/newTestObject")
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ client := serviceClient()
+ _, err := Copy(client, "testContainer", "testObject", &CopyOpts{Destination: "/newTestContainer/newTestObject"}).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error copying object: %v", err)
+ }
+}
+
+func TestDeleteObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ _, err := Delete(client, "testContainer", "testObject", nil).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error deleting object: %v", err)
+ }
+}
+
+func TestUpateObjectMetadata(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "X-Object-Meta-Gophercloud-Test", "objects")
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := serviceClient()
+ _, err := Update(client, "testContainer", "testObject", &UpdateOpts{Metadata: metadata}).ExtractHeaders()
+ if err != nil {
+ t.Fatalf("Unexpected error updating object metadata: %v", err)
+ }
+}
+
+func TestGetObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "HEAD")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.Header().Add("X-Object-Meta-Gophercloud-Test", "objects")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ expected := metadata
+ actual, err := Get(client, "testContainer", "testObject", nil).ExtractMetadata()
+ if err != nil {
+ t.Fatalf("Unexpected error getting object metadata: %v", err)
+ }
+ testhelper.CheckDeepEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/objects/results.go b/openstack/objectstorage/v1/objects/results.go
new file mode 100644
index 0000000..aaeb040
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -0,0 +1,166 @@
+package objects
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Object is a structure that holds information related to a storage object.
+type Object struct {
+ Bytes int `json:"bytes" mapstructure:"bytes"`
+ ContentType string `json:"content_type" mapstructure:"content_type"`
+ Hash string `json:"hash" mapstructure:"hash"`
+ LastModified string `json:"last_modified" mapstructure:"last_modified"`
+ Name string `json:"name" mapstructure:"name"`
+}
+
+// ListResult is a single page of objects that is returned from a call to the List function.
+type ObjectPage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no object names.
+func (r ObjectPage) IsEmpty() (bool, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return true, err
+ }
+ return len(names) == 0, nil
+}
+
+// LastMarker returns the last object name in a ListResult.
+func (r ObjectPage) LastMarker() (string, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return "", err
+ }
+ if len(names) == 0 {
+ return "", nil
+ }
+ return names[len(names)-1], nil
+}
+
+// ExtractInfo is a function that takes a page of objects and returns their full information.
+func ExtractInfo(page pagination.Page) ([]Object, error) {
+ untyped := page.(ObjectPage).Body.([]interface{})
+ results := make([]Object, len(untyped))
+ for index, each := range untyped {
+ object := each.(map[string]interface{})
+ err := mapstructure.Decode(object, &results[index])
+ if err != nil {
+ return results, err
+ }
+ }
+ return results, nil
+}
+
+// ExtractNames is a function that takes a page of objects and returns only their names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+ casted := page.(ObjectPage)
+ ct := casted.Header.Get("Content-Type")
+ switch {
+ case strings.HasPrefix(ct, "application/json"):
+ parsed, err := ExtractInfo(page)
+ if err != nil {
+ return nil, err
+ }
+
+ names := make([]string, 0, len(parsed))
+ for _, object := range parsed {
+ names = append(names, object.Name)
+ }
+
+ return names, nil
+ case strings.HasPrefix(ct, "text/plain"):
+ names := make([]string, 0, 50)
+
+ body := string(page.(ObjectPage).Body.([]uint8))
+ for _, name := range strings.Split(body, "\n") {
+ if len(name) > 0 {
+ names = append(names, name)
+ }
+ }
+
+ return names, nil
+ case strings.HasPrefix(ct, "text/html"):
+ return []string{}, nil
+ default:
+ return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+ }
+}
+
+// DownloadResult is a *http.Response that is returned from a call to the Download function.
+type DownloadResult struct {
+ commonResult
+}
+
+// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
+// and returns the object's content.
+func (dr DownloadResult) ExtractContent() ([]byte, error) {
+ if dr.Err != nil {
+ return nil, nil
+ }
+ var body []byte
+ defer dr.Resp.Body.Close()
+ body, err := ioutil.ReadAll(dr.Resp.Body)
+ if err != nil {
+ return body, fmt.Errorf("Error trying to read DownloadResult body: %v", err)
+ }
+ return body, nil
+}
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult struct {
+ commonResult
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the object.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+ metadata := make(map[string]string)
+ for k, v := range gr.Resp.Header {
+ if strings.HasPrefix(k, "X-Object-Meta-") {
+ key := strings.TrimPrefix(k, "X-Object-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata, nil
+}
+
+type commonResult struct {
+ Resp *http.Response
+ Err error
+}
+
+func (cr commonResult) ExtractHeaders() (http.Header, error) {
+ var headers http.Header
+ if cr.Err != nil {
+ return headers, cr.Err
+ }
+
+ return cr.Resp.Header, nil
+}
+
+type CreateResult struct {
+ commonResult
+}
+
+type UpdateResult struct {
+ commonResult
+}
+
+type DeleteResult struct {
+ commonResult
+}
+
+type CopyResult struct {
+ commonResult
+}
diff --git a/openstack/objectstorage/v1/objects/urls.go b/openstack/objectstorage/v1/objects/urls.go
new file mode 100644
index 0000000..a377960
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/urls.go
@@ -0,0 +1,13 @@
+package objects
+
+import "github.com/rackspace/gophercloud"
+
+// objectURL returns the URI for making Object requests.
+func objectURL(c *gophercloud.ServiceClient, container, object string) string {
+ return c.ServiceURL(container, object)
+}
+
+// containerURL returns the URI for making Container requests.
+func containerURL(c *gophercloud.ServiceClient, container string) string {
+ return c.ServiceURL(container)
+}
diff --git a/openstack/objectstorage/v1/objects/urls_test.go b/openstack/objectstorage/v1/objects/urls_test.go
new file mode 100644
index 0000000..89d1cb1
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/urls_test.go
@@ -0,0 +1,28 @@
+package objects
+
+import (
+ "testing"
+ "github.com/rackspace/gophercloud"
+)
+
+func TestContainerURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v1/",
+ }
+ expected := "http://localhost:5000/v1/testContainer"
+ actual := containerURL(&client, "testContainer")
+ if actual != expected {
+ t.Errorf("Unexpected service URL generated: %s", actual)
+ }
+}
+
+func TestObjectURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v1/",
+ }
+ expected := "http://localhost:5000/v1/testContainer/testObject"
+ actual := objectURL(&client, "testContainer", "testObject")
+ if actual != expected {
+ t.Errorf("Unexpected service URL generated: %s", actual)
+ }
+}
diff --git a/openstack/storage/v1/accounts/urls.go b/openstack/storage/v1/accounts/urls.go
deleted file mode 100644
index ae78ff2..0000000
--- a/openstack/storage/v1/accounts/urls.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package accounts
-
-import "github.com/rackspace/gophercloud"
-
-// getAccountURL returns the URI for making Account requests.
-func getAccountURL(c *gophercloud.ServiceClient) string {
- return c.Endpoint
-}
diff --git a/openstack/storage/v1/containers/containers.go b/openstack/storage/v1/containers/containers.go
deleted file mode 100644
index 8fa19aa..0000000
--- a/openstack/storage/v1/containers/containers.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package containers
-
-import (
- "fmt"
- "strings"
-
- "github.com/rackspace/gophercloud/pagination"
-)
-
-// Container is a structure that holds information related to a storage container.
-type Container map[string]interface{}
-
-// ListOpts is a structure that holds parameters for listing containers.
-type ListOpts struct {
- Full bool
- Params map[string]string
-}
-
-// CreateOpts is a structure that holds parameters for creating a container.
-type CreateOpts struct {
- Name string
- Metadata map[string]string
- Headers map[string]string
-}
-
-// DeleteOpts is a structure that holds parameters for deleting a container.
-type DeleteOpts struct {
- Name string
- Params map[string]string
-}
-
-// UpdateOpts is a structure that holds parameters for updating, creating, or deleting a
-// container's metadata.
-type UpdateOpts struct {
- Name string
- Metadata map[string]string
- Headers map[string]string
-}
-
-// GetOpts is a structure that holds parameters for getting a container's metadata.
-type GetOpts struct {
- Name string
- Metadata map[string]string
-}
-
-// ExtractInfo is a function that takes a ListResult and returns the containers' information.
-func ExtractInfo(page pagination.Page) ([]Container, error) {
- untyped := page.(ListResult).Body.([]interface{})
- results := make([]Container, len(untyped))
- for index, each := range untyped {
- results[index] = Container(each.(map[string]interface{}))
- }
- return results, nil
-}
-
-// ExtractNames is a function that takes a ListResult and returns the containers' names.
-func ExtractNames(page pagination.Page) ([]string, error) {
- casted := page.(ListResult)
- ct := casted.Header.Get("Content-Type")
-
- switch {
- case strings.HasPrefix(ct, "application/json"):
- parsed, err := ExtractInfo(page)
- if err != nil {
- return nil, err
- }
-
- names := make([]string, 0, len(parsed))
- for _, container := range parsed {
- names = append(names, container["name"].(string))
- }
- return names, nil
- case strings.HasPrefix(ct, "text/plain"):
- names := make([]string, 0, 50)
-
- body := string(page.(ListResult).Body.([]uint8))
- for _, name := range strings.Split(body, "\n") {
- if len(name) > 0 {
- names = append(names, name)
- }
- }
-
- return names, nil
- default:
- return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
- }
-}
-
-// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
-// and returns the custom metadata associated with the container.
-func ExtractMetadata(gr GetResult) map[string]string {
- metadata := make(map[string]string)
- for k, v := range gr.Header {
- if strings.HasPrefix(k, "X-Container-Meta-") {
- key := strings.TrimPrefix(k, "X-Container-Meta-")
- metadata[key] = v[0]
- }
- }
- return metadata
-}
diff --git a/openstack/storage/v1/containers/requests.go b/openstack/storage/v1/containers/requests.go
deleted file mode 100644
index 2aa263a..0000000
--- a/openstack/storage/v1/containers/requests.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package containers
-
-import (
- "net/http"
-
- "github.com/racker/perigee"
- "github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/utils"
- "github.com/rackspace/gophercloud/pagination"
-)
-
-// ListResult is a *http.Response that is returned from a call to the List function.
-type ListResult struct {
- pagination.MarkerPageBase
-}
-
-// IsEmpty returns true if a ListResult contains no container names.
-func (r ListResult) IsEmpty() (bool, error) {
- names, err := ExtractNames(r)
- if err != nil {
- return true, err
- }
- return len(names) == 0, nil
-}
-
-// LastMarker returns the last container name in a ListResult.
-func (r ListResult) LastMarker() (string, error) {
- names, err := ExtractNames(r)
- if err != nil {
- return "", err
- }
- if len(names) == 0 {
- return "", nil
- }
- return names[len(names)-1], nil
-}
-
-// GetResult is a *http.Response that is returned from a call to the Get function.
-type GetResult *http.Response
-
-// List is a function that retrieves all objects in a container. It also returns the details
-// for the account. To extract just the container information or names, pass the ListResult
-// response to the ExtractInfo or ExtractNames function, respectively.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
- var headers map[string]string
-
- query := utils.BuildQuery(opts.Params)
-
- if !opts.Full {
- headers = map[string]string{"Content-Type": "text/plain"}
- }
-
- createPage := func(r pagination.LastHTTPResponse) pagination.Page {
- p := ListResult{pagination.MarkerPageBase{LastHTTPResponse: r}}
- p.MarkerPageBase.Owner = p
- return p
- }
-
- url := getAccountURL(c) + query
- pager := pagination.NewPager(c, url, createPage)
- pager.Headers = headers
- return pager
-}
-
-// Create is a function that creates a new container.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) (Container, error) {
- var ci Container
-
- h := c.Provider.AuthenticatedHeaders()
-
- for k, v := range opts.Headers {
- h[k] = v
- }
-
- for k, v := range opts.Metadata {
- h["X-Container-Meta-"+k] = v
- }
-
- url := getContainerURL(c, opts.Name)
- _, err := perigee.Request("PUT", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{201, 204},
- })
- if err == nil {
- ci = Container{
- "name": opts.Name,
- }
- }
- return ci, err
-}
-
-// Delete is a function that deletes a container.
-func Delete(c *gophercloud.ServiceClient, opts DeleteOpts) error {
- h := c.Provider.AuthenticatedHeaders()
-
- query := utils.BuildQuery(opts.Params)
-
- url := getContainerURL(c, opts.Name) + query
- _, err := perigee.Request("DELETE", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{204},
- })
- return err
-}
-
-// Update is a function that creates, updates, or deletes a container's metadata.
-func Update(c *gophercloud.ServiceClient, opts UpdateOpts) error {
- h := c.Provider.AuthenticatedHeaders()
-
- for k, v := range opts.Headers {
- h[k] = v
- }
-
- for k, v := range opts.Metadata {
- h["X-Container-Meta-"+k] = v
- }
-
- url := getContainerURL(c, opts.Name)
- _, err := perigee.Request("POST", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{204},
- })
- return err
-}
-
-// 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, opts GetOpts) (GetResult, error) {
- h := c.Provider.AuthenticatedHeaders()
-
- for k, v := range opts.Metadata {
- h["X-Container-Meta-"+k] = v
- }
-
- url := getContainerURL(c, opts.Name)
- resp, err := perigee.Request("HEAD", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{204},
- })
- return &resp.HttpResponse, err
-}
diff --git a/openstack/storage/v1/containers/urls.go b/openstack/storage/v1/containers/urls.go
deleted file mode 100644
index 4084bcc..0000000
--- a/openstack/storage/v1/containers/urls.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package containers
-
-import "github.com/rackspace/gophercloud"
-
-// getAccountURL returns the URI used to list Containers.
-func getAccountURL(c *gophercloud.ServiceClient) string {
- return c.Endpoint
-}
-
-// getContainerURL returns the URI for making Container requests.
-func getContainerURL(c *gophercloud.ServiceClient, container string) string {
- return c.ServiceURL(container)
-}
diff --git a/openstack/storage/v1/objects/objects.go b/openstack/storage/v1/objects/objects.go
deleted file mode 100644
index 4cf3d2d..0000000
--- a/openstack/storage/v1/objects/objects.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package objects
-
-import (
- "fmt"
- "io"
- "io/ioutil"
- "strings"
-
- "github.com/rackspace/gophercloud/pagination"
-)
-
-// Object is a structure that holds information related to a storage object.
-type Object map[string]interface{}
-
-// ListOpts is a structure that holds parameters for listing objects.
-type ListOpts struct {
- Container string
- Full bool
- Params map[string]string
-}
-
-// DownloadOpts is a structure that holds parameters for downloading an object.
-type DownloadOpts struct {
- Container string
- Name string
- Headers map[string]string
- Params map[string]string
-}
-
-// CreateOpts is a structure that holds parameters for creating an object.
-type CreateOpts struct {
- Container string
- Name string
- Content io.Reader
- Metadata map[string]string
- Headers map[string]string
- Params map[string]string
-}
-
-// CopyOpts is a structure that holds parameters for copying one object to another.
-type CopyOpts struct {
- Container string
- Name string
- NewContainer string
- NewName string
- Metadata map[string]string
- Headers map[string]string
-}
-
-// DeleteOpts is a structure that holds parameters for deleting an object.
-type DeleteOpts struct {
- Container string
- Name string
- Params map[string]string
-}
-
-// GetOpts is a structure that holds parameters for getting an object's metadata.
-type GetOpts struct {
- Container string
- Name string
- Headers map[string]string
- Params map[string]string
-}
-
-// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
-// object's metadata.
-type UpdateOpts struct {
- Container string
- Name string
- Metadata map[string]string
- Headers map[string]string
-}
-
-// ExtractInfo is a function that takes a page of objects and returns their full information.
-func ExtractInfo(page pagination.Page) ([]Object, error) {
- untyped := page.(ListResult).Body.([]interface{})
- results := make([]Object, len(untyped))
- for index, each := range untyped {
- results[index] = Object(each.(map[string]interface{}))
- }
- return results, nil
-}
-
-// ExtractNames is a function that takes a page of objects and returns only their names.
-func ExtractNames(page pagination.Page) ([]string, error) {
- casted := page.(ListResult)
- ct := casted.Header.Get("Content-Type")
-
- switch {
- case strings.HasPrefix(ct, "application/json"):
- parsed, err := ExtractInfo(page)
- if err != nil {
- return nil, err
- }
-
- names := make([]string, 0, len(parsed))
- for _, object := range parsed {
- names = append(names, object["name"].(string))
- }
- return names, nil
- case strings.HasPrefix(ct, "text/plain"):
- names := make([]string, 0, 50)
-
- body := string(page.(ListResult).Body.([]uint8))
- for _, name := range strings.Split(body, "\n") {
- if len(name) > 0 {
- names = append(names, name)
- }
- }
-
- return names, nil
- default:
- return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
- }
-}
-
-// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
-// and returns the object's content.
-func ExtractContent(dr DownloadResult) ([]byte, error) {
- var body []byte
- defer dr.Body.Close()
- body, err := ioutil.ReadAll(dr.Body)
- return body, err
-}
-
-// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
-// and returns the custom metadata associated with the object.
-func ExtractMetadata(gr GetResult) map[string]string {
- metadata := make(map[string]string)
- for k, v := range gr.Header {
- if strings.HasPrefix(k, "X-Object-Meta-") {
- key := strings.TrimPrefix(k, "X-Object-Meta-")
- metadata[key] = v[0]
- }
- }
- return metadata
-}
diff --git a/openstack/storage/v1/objects/requests.go b/openstack/storage/v1/objects/requests.go
deleted file mode 100644
index 67aba34..0000000
--- a/openstack/storage/v1/objects/requests.go
+++ /dev/null
@@ -1,190 +0,0 @@
-package objects
-
-import (
- "fmt"
- "net/http"
-
- "github.com/racker/perigee"
- "github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/utils"
- "github.com/rackspace/gophercloud/pagination"
-)
-
-// ListResult is a single page of objects that is returned from a call to the List function.
-type ListResult struct {
- pagination.MarkerPageBase
-}
-
-// IsEmpty returns true if a ListResult contains no object names.
-func (r ListResult) IsEmpty() (bool, error) {
- names, err := ExtractNames(r)
- if err != nil {
- return true, err
- }
- return len(names) == 0, nil
-}
-
-// LastMarker returns the last object name in a ListResult.
-func (r ListResult) LastMarker() (string, error) {
- names, err := ExtractNames(r)
- if err != nil {
- return "", err
- }
- if len(names) == 0 {
- return "", nil
- }
- return names[len(names)-1], nil
-}
-
-// DownloadResult is a *http.Response that is returned from a call to the Download function.
-type DownloadResult *http.Response
-
-// GetResult is a *http.Response that is returned from a call to the Get function.
-type GetResult *http.Response
-
-// List is a function that retrieves all objects in a container. It also returns the details
-// for the container. To extract only the object information or names, pass the ListResult
-// response to the ExtractInfo or ExtractNames function, respectively.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
- var headers map[string]string
-
- query := utils.BuildQuery(opts.Params)
-
- if !opts.Full {
- headers = map[string]string{"Content-Type": "text/plain"}
- }
-
- createPage := func(r pagination.LastHTTPResponse) pagination.Page {
- p := ListResult{pagination.MarkerPageBase{LastHTTPResponse: r}}
- p.MarkerPageBase.Owner = p
- return p
- }
-
- url := getContainerURL(c, opts.Container) + query
- pager := pagination.NewPager(c, url, createPage)
- pager.Headers = headers
- return pager
-}
-
-// 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, opts DownloadOpts) (DownloadResult, error) {
- h := c.Provider.AuthenticatedHeaders()
-
- for k, v := range opts.Headers {
- h[k] = v
- }
-
- query := utils.BuildQuery(opts.Params)
-
- url := getObjectURL(c, opts.Container, opts.Name) + query
- resp, err := perigee.Request("GET", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{200},
- })
- return &resp.HttpResponse, err
-}
-
-// Create is a function that creates a new object or replaces an existing object.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) error {
- var reqBody []byte
-
- h := c.Provider.AuthenticatedHeaders()
-
- for k, v := range opts.Headers {
- h[k] = v
- }
-
- for k, v := range opts.Metadata {
- h["X-Object-Meta-"+k] = v
- }
-
- query := utils.BuildQuery(opts.Params)
-
- content := opts.Content
- if content != nil {
- reqBody = make([]byte, 0)
- _, err := content.Read(reqBody)
- if err != nil {
- return err
- }
- }
-
- url := getObjectURL(c, opts.Container, opts.Name) + query
- _, err := perigee.Request("PUT", url, perigee.Options{
- ReqBody: reqBody,
- MoreHeaders: h,
- OkCodes: []int{201},
- })
- return err
-}
-
-// Copy is a function that copies one object to another.
-func Copy(c *gophercloud.ServiceClient, opts CopyOpts) error {
- h := c.Provider.AuthenticatedHeaders()
-
- for k, v := range opts.Metadata {
- h["X-Object-Meta-"+k] = v
- }
-
- h["Destination"] = fmt.Sprintf("/%s/%s", opts.NewContainer, opts.NewName)
-
- url := getObjectURL(c, opts.Container, opts.Name)
- _, err := perigee.Request("COPY", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{201},
- })
- return err
-}
-
-// Delete is a function that deletes an object.
-func Delete(c *gophercloud.ServiceClient, opts DeleteOpts) error {
- h := c.Provider.AuthenticatedHeaders()
-
- query := utils.BuildQuery(opts.Params)
-
- url := getObjectURL(c, opts.Container, opts.Name) + query
- _, err := perigee.Request("DELETE", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{204},
- })
- return err
-}
-
-// 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, opts GetOpts) (GetResult, error) {
- h := c.Provider.AuthenticatedHeaders()
-
- for k, v := range opts.Headers {
- h[k] = v
- }
-
- url := getObjectURL(c, opts.Container, opts.Name)
- resp, err := perigee.Request("HEAD", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{204},
- })
- return &resp.HttpResponse, err
-}
-
-// Update is a function that creates, updates, or deletes an object's metadata.
-func Update(c *gophercloud.ServiceClient, opts UpdateOpts) error {
- h := c.Provider.AuthenticatedHeaders()
-
- for k, v := range opts.Headers {
- h[k] = v
- }
-
- for k, v := range opts.Metadata {
- h["X-Object-Meta-"+k] = v
- }
-
- url := getObjectURL(c, opts.Container, opts.Name)
- _, err := perigee.Request("POST", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{202},
- })
- return err
-}
diff --git a/openstack/storage/v1/objects/urls.go b/openstack/storage/v1/objects/urls.go
deleted file mode 100644
index 5a52aed..0000000
--- a/openstack/storage/v1/objects/urls.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package objects
-
-import "github.com/rackspace/gophercloud"
-
-// getObjectURL returns the URI for making Object requests.
-func getObjectURL(c *gophercloud.ServiceClient, container, object string) string {
- return c.ServiceURL(container, object)
-}
-
-// getContainerURL returns the URI for making Container requests.
-func getContainerURL(c *gophercloud.ServiceClient, container string) string {
- return c.ServiceURL(container)
-}
diff --git a/pagination/pager.go b/pagination/pager.go
index 806d98a..22d6d84 100644
--- a/pagination/pager.go
+++ b/pagination/pager.go
@@ -35,6 +35,8 @@
createPage func(r LastHTTPResponse) Page
+ Err error
+
// Headers supplies additional HTTP headers to populate on each paged request.
Headers map[string]string
}
@@ -66,6 +68,9 @@
// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
// Return "false" from the handler to prematurely stop iterating.
func (p Pager) EachPage(handler func(Page) (bool, error)) error {
+ if p.Err != nil {
+ return p.Err
+ }
currentURL := p.initialURL
for {
currentPage, err := p.fetchNextPage(currentURL)
diff --git a/params.go b/params.go
index f11a29d..10aefea 100644
--- a/params.go
+++ b/params.go
@@ -1,5 +1,14 @@
package gophercloud
+import (
+ "fmt"
+ "net/url"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
// MaybeString takes a string that might be a zero-value, and either returns a
// pointer to its address or a nil value (i.e. empty pointer). This is useful
// for converting zero values in options structs when the end-user hasn't
@@ -11,3 +20,149 @@
}
return nil
}
+
+func MaybeInt(original int) *int {
+ if original != 0 {
+ return &original
+ }
+ return nil
+}
+
+var t time.Time
+
+func isZero(v reflect.Value) bool {
+ switch v.Kind() {
+ case reflect.Func, reflect.Map, reflect.Slice:
+ return v.IsNil()
+ case reflect.Array:
+ z := true
+ for i := 0; i < v.Len(); i++ {
+ z = z && isZero(v.Index(i))
+ }
+ return z
+ case reflect.Struct:
+ if v.Type() == reflect.TypeOf(t) {
+ if v.Interface().(time.Time).IsZero() {
+ return true
+ }
+ return false
+ }
+ z := true
+ for i := 0; i < v.NumField(); i++ {
+ z = z && isZero(v.Field(i))
+ }
+ return z
+ }
+ // Compare other types directly:
+ z := reflect.Zero(v.Type())
+ return v.Interface() == z.Interface()
+}
+
+func BuildQueryString(opts interface{}) (*url.URL, error) {
+ optsValue := reflect.ValueOf(opts)
+ if optsValue.Kind() == reflect.Ptr {
+ optsValue = optsValue.Elem()
+ }
+
+ optsType := reflect.TypeOf(opts)
+ if optsType.Kind() == reflect.Ptr {
+ optsType = optsType.Elem()
+ }
+
+ optsSlice := make([]string, 0)
+ if optsValue.Kind() == reflect.Struct {
+ for i := 0; i < optsValue.NumField(); i++ {
+ v := optsValue.Field(i)
+ f := optsType.Field(i)
+ qTag := f.Tag.Get("q")
+
+ // if the field has a 'q' tag, it goes in the query string
+ if qTag != "" {
+ tags := strings.Split(qTag, ",")
+
+ // if the field is set, add it to the slice of query pieces
+ if !isZero(v) {
+ switch v.Kind() {
+ case reflect.String:
+ optsSlice = append(optsSlice, tags[0]+"="+v.String())
+ case reflect.Int:
+ optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatInt(v.Int(), 10))
+ case reflect.Bool:
+ optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatBool(v.Bool()))
+ }
+ } else {
+ // Otherwise, the field is not set.
+ if len(tags) == 2 && tags[1] == "required" {
+ // And the field is required. Return an error.
+ return nil, fmt.Errorf("Required query parameter [%s] not set.", f.Name)
+ }
+ }
+ }
+
+ }
+ // URL encode the string for safety.
+ s := strings.Join(optsSlice, "&")
+ if s != "" {
+ s = "?" + s
+ }
+ u, err := url.Parse(s)
+ if err != nil {
+ return nil, err
+ }
+ return u, nil
+ }
+ // Return an error if the underlying type of 'opts' isn't a struct.
+ return nil, fmt.Errorf("Options type is not a struct.")
+}
+
+func BuildRequestBody(opts interface{}) (map[string]interface{}, error) {
+ return nil, nil
+}
+
+func BuildHeaders(opts interface{}) (map[string]string, error) {
+ optsValue := reflect.ValueOf(opts)
+ if optsValue.Kind() == reflect.Ptr {
+ optsValue = optsValue.Elem()
+ }
+
+ optsType := reflect.TypeOf(opts)
+ if optsType.Kind() == reflect.Ptr {
+ optsType = optsType.Elem()
+ }
+
+ optsMap := make(map[string]string)
+ if optsValue.Kind() == reflect.Struct {
+ for i := 0; i < optsValue.NumField(); i++ {
+ v := optsValue.Field(i)
+ f := optsType.Field(i)
+ hTag := f.Tag.Get("h")
+
+ // if the field has a 'h' tag, it goes in the header
+ if hTag != "" {
+ tags := strings.Split(hTag, ",")
+
+ // if the field is set, add it to the slice of query pieces
+ if !isZero(v) {
+ switch v.Kind() {
+ case reflect.String:
+ optsMap[tags[0]] = v.String()
+ case reflect.Int:
+ optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
+ case reflect.Bool:
+ optsMap[tags[0]] = strconv.FormatBool(v.Bool())
+ }
+ } else {
+ // Otherwise, the field is not set.
+ if len(tags) == 2 && tags[1] == "required" {
+ // And the field is required. Return an error.
+ return optsMap, fmt.Errorf("Required header not set.")
+ }
+ }
+ }
+
+ }
+ return optsMap, nil
+ }
+ // Return an error if the underlying type of 'opts' isn't a struct.
+ return optsMap, fmt.Errorf("Options type is not a struct.")
+}
diff --git a/params_test.go b/params_test.go
new file mode 100644
index 0000000..03eaefc
--- /dev/null
+++ b/params_test.go
@@ -0,0 +1,59 @@
+package gophercloud
+
+import (
+ "net/url"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMaybeStringWithNonEmptyString(t *testing.T) {
+ testString := "carol"
+ expected := &testString
+ actual := MaybeString("carol")
+ th.CheckDeepEquals(t, actual, expected)
+}
+
+func TestMaybeStringWithEmptyString(t *testing.T) {
+ var expected *string
+ actual := MaybeString("")
+ th.CheckDeepEquals(t, actual, expected)
+}
+
+func TestBuildQueryStringWithPointerToStruct(t *testing.T) {
+ expected := &url.URL{
+ RawQuery: "j=2&r=red",
+ }
+
+ type Opts struct {
+ J int `q:"j"`
+ R string `q:"r"`
+ C bool
+ }
+
+ opts := Opts{J: 2, R: "red"}
+
+ actual, err := BuildQueryString(&opts)
+ if err != nil {
+ t.Errorf("Error building query string: %v", err)
+ }
+
+ th.CheckDeepEquals(t, actual, expected)
+}
+
+func TestBuildQueryStringWithoutRequiredFieldSet(t *testing.T) {
+ type Opts struct {
+ J int `q:"j"`
+ R string `q:"r,required"`
+ C bool
+ }
+
+ opts := Opts{J: 2, C: true}
+
+ _, err := BuildQueryString(&opts)
+ if err == nil {
+ t.Error("Unexpected result: There should be an error thrown when a required field isn't set.")
+ }
+
+ t.Log(err)
+}
diff --git a/testhelper/convenience.go b/testhelper/convenience.go
index 9cf23a7..85cb9ec 100644
--- a/testhelper/convenience.go
+++ b/testhelper/convenience.go
@@ -1,37 +1,61 @@
package testhelper
import (
+ "fmt"
+ "path/filepath"
"reflect"
+ "runtime"
"testing"
)
+func prefix() string {
+ _, file, line, _ := runtime.Caller(3)
+ return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line)
+}
+
+func green(str interface{}) string {
+ return fmt.Sprintf("\033[0m\033[1;32m%#v\033[0m\033[1;31m", str)
+}
+
+func yellow(str interface{}) string {
+ return fmt.Sprintf("\033[0m\033[1;33m%#v\033[0m\033[1;31m", str)
+}
+
+func logFatal(t *testing.T, str string) {
+ t.Fatalf("\033[1;31m%s %s\033[0m", prefix(), str)
+}
+
+func logError(t *testing.T, str string) {
+ t.Errorf("\033[1;31m%s %s\033[0m", prefix(), str)
+}
+
// AssertEquals compares two arbitrary values and performs a comparison. If the
// comparison fails, a fatal error is raised that will fail the test
func AssertEquals(t *testing.T, expected, actual interface{}) {
if expected != actual {
- t.Fatalf("Expected [%#v] but got [%#v]", expected, actual)
+ logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
}
}
// CheckEquals is similar to AssertEquals, except with a non-fatal error
func CheckEquals(t *testing.T, expected, actual interface{}) {
if expected != actual {
- t.Errorf("Expected [%#v] but got [%#v]", expected, actual)
+ logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
}
}
// AssertDeepEquals - like Equals - performs a comparison - but on more complex
// structures that requires deeper inspection
-func AssertDeepEquals(t *testing.T, actual, expected interface{}) {
- if !reflect.DeepEqual(actual, expected) {
- t.Fatalf("Expected %#v but got %#v", expected, actual)
+func AssertDeepEquals(t *testing.T, expected, actual interface{}) {
+ if !reflect.DeepEqual(expected, actual) {
+ logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
}
}
// CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error
-func CheckDeepEquals(t *testing.T, actual, expected interface{}) {
- if !reflect.DeepEqual(actual, expected) {
- t.Errorf("Expected %#v but got %#v", expected, actual)
+func CheckDeepEquals(t *testing.T, expected, actual interface{}) {
+ if !reflect.DeepEqual(expected, actual) {
+ logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
}
}
@@ -39,13 +63,13 @@
// an actual error
func AssertNoErr(t *testing.T, e error) {
if e != nil {
- t.Fatalf("Unexpected error: %#v", e)
+ logFatal(t, fmt.Sprintf("unexpected error %s", yellow(e)))
}
}
// CheckNoErr is similar to AssertNoErr, except with a non-fatal error
func CheckNoErr(t *testing.T, e error) {
if e != nil {
- t.Errorf("Unexpected error: %#v", e)
+ logError(t, fmt.Sprintf("unexpected error %s", yellow(e)))
}
}
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..c424759
--- /dev/null
+++ b/util.go
@@ -0,0 +1,22 @@
+package gophercloud
+
+import (
+ "fmt"
+ "time"
+)
+
+// WaitFor polls a predicate function once per second up to secs times to wait for a certain state to arrive.
+func WaitFor(secs int, predicate func() (bool, error)) error {
+ for i := 0; i < secs; i++ {
+ time.Sleep(1 * time.Second)
+
+ satisfied, err := predicate()
+ if err != nil {
+ return err
+ }
+ if satisfied {
+ return nil
+ }
+ }
+ return fmt.Errorf("Time out in WaitFor.")
+}