Merge pull request #238 from jamiehannaford/server-rebuild-opts
Introducing RebuildOpts
diff --git a/.travis.yml b/.travis.yml
index 9c37aef..cf4f8ca 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,11 @@
language: go
install:
- - go get -v ./...
+ - go get -v -tags 'fixtures acceptance' ./...
go:
- 1.1
- 1.2
- tip
+script: script/cibuild
after_success:
- go get code.google.com/p/go.tools/cmd/cover
- go get github.com/axw/gocov/gocov
diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go
index ca810bc..9c8e322 100644
--- a/acceptance/openstack/compute/v2/flavors_test.go
+++ b/acceptance/openstack/compute/v2/flavors_test.go
@@ -17,7 +17,7 @@
t.Logf("ID\tRegion\tName\tStatus\tCreated")
- pager := flavors.List(client, flavors.ListFilterOptions{})
+ pager := flavors.List(client, nil)
count, pages := 0, 0
pager.EachPage(func(page pagination.Page) (bool, error) {
t.Logf("---")
diff --git a/acceptance/openstack/compute/v2/images_test.go b/acceptance/openstack/compute/v2/images_test.go
index 7fca3ec..6166fc8 100644
--- a/acceptance/openstack/compute/v2/images_test.go
+++ b/acceptance/openstack/compute/v2/images_test.go
@@ -17,7 +17,7 @@
t.Logf("ID\tRegion\tName\tStatus\tCreated")
- pager := images.List(client)
+ pager := images.ListDetail(client, nil)
count, pages := 0, 0
pager.EachPage(func(page pagination.Page) (bool, error) {
pages++
diff --git a/acceptance/openstack/networking/v2/extensions/layer3_test.go b/acceptance/openstack/networking/v2/extensions/layer3_test.go
index 1289113..63e0be3 100644
--- a/acceptance/openstack/networking/v2/extensions/layer3_test.go
+++ b/acceptance/openstack/networking/v2/extensions/layer3_test.go
@@ -219,7 +219,7 @@
}
func updateRouter(t *testing.T, routerID string) {
- r, err := routers.Update(base.Client, routerID, routers.UpdateOpts{
+ _, err := routers.Update(base.Client, routerID, routers.UpdateOpts{
Name: "another_name",
}).Extract()
diff --git a/acceptance/openstack/networking/v2/extensions/provider_test.go b/acceptance/openstack/networking/v2/extensions/provider_test.go
index dc21ae5..f10c9d9 100644
--- a/acceptance/openstack/networking/v2/extensions/provider_test.go
+++ b/acceptance/openstack/networking/v2/extensions/provider_test.go
@@ -6,24 +6,25 @@
"strconv"
"testing"
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
)
func TestNetworkCRUDOperations(t *testing.T) {
- Setup(t)
- defer Teardown()
+ base.Setup(t)
+ defer base.Teardown()
// Create a network
- n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: true}).Extract()
+ n, err := networks.Create(base.Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Name, "sample_network")
th.AssertEquals(t, n.AdminStateUp, true)
networkID := n.ID
// List networks
- pager := networks.List(Client, networks.ListOpts{Limit: 2})
+ pager := networks.List(base.Client, networks.ListOpts{Limit: 2})
err = pager.EachPage(func(page pagination.Page) (bool, error) {
t.Logf("--- Page ---")
@@ -43,7 +44,7 @@
if networkID == "" {
t.Fatalf("In order to retrieve a network, the NetworkID must be set")
}
- n, err = networks.Get(Client, networkID).Extract()
+ n, err = networks.Get(base.Client, networkID).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Status, "ACTIVE")
th.AssertDeepEquals(t, n.Subnets, []string{})
@@ -53,12 +54,12 @@
th.AssertEquals(t, n.ID, networkID)
// Update network
- n, err = networks.Update(Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract()
+ n, err = networks.Update(base.Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Name, "new_network_name")
// Delete network
- res := networks.Delete(Client, networkID)
+ res := networks.Delete(base.Client, networkID)
th.AssertNoErr(t, res.Err)
}
diff --git a/acceptance/openstack/networking/v2/extensions/security_test.go b/acceptance/openstack/networking/v2/extensions/security_test.go
index 522219a..16ecca4 100644
--- a/acceptance/openstack/networking/v2/extensions/security_test.go
+++ b/acceptance/openstack/networking/v2/extensions/security_test.go
@@ -120,18 +120,6 @@
t.Logf("Deleted security group %s", groupID)
}
-func deletePort(t *testing.T, portID string) {
- res := ports.Delete(base.Client, portID)
- th.AssertNoErr(t, res.Err)
- t.Logf("Deleted port %s", portID)
-}
-
-func deleteNetwork(t *testing.T, networkID string) {
- res := networks.Delete(base.Client, networkID)
- th.AssertNoErr(t, res.Err)
- t.Logf("Deleted network %s", networkID)
-}
-
func createSecRule(t *testing.T, groupID string) string {
r, err := rules.Create(base.Client, rules.CreateOpts{
Direction: "ingress",
diff --git a/acceptance/openstack/networking/v2/network_test.go b/acceptance/openstack/networking/v2/network_test.go
index f00edba..be8a3a1 100644
--- a/acceptance/openstack/networking/v2/network_test.go
+++ b/acceptance/openstack/networking/v2/network_test.go
@@ -16,7 +16,7 @@
defer Teardown()
// Create a network
- n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: true}).Extract()
+ n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract()
th.AssertNoErr(t, err)
defer networks.Delete(Client, n.ID)
th.AssertEquals(t, n.Name, "sample_network")
diff --git a/acceptance/openstack/networking/v2/port_test.go b/acceptance/openstack/networking/v2/port_test.go
index 363a7fe..7f22dbd 100644
--- a/acceptance/openstack/networking/v2/port_test.go
+++ b/acceptance/openstack/networking/v2/port_test.go
@@ -97,18 +97,17 @@
}
func createNetwork() (string, error) {
- res, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: true}).Extract()
+ res, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract()
return res.ID, err
}
func createSubnet(networkID string) (string, error) {
- enable := false
s, err := subnets.Create(Client, subnets.CreateOpts{
NetworkID: networkID,
CIDR: "192.168.199.0/24",
IPVersion: subnets.IPv4,
Name: "my_subnet",
- EnableDHCP: &enable,
+ EnableDHCP: subnets.Down,
}).Extract()
return s.ID, err
}
diff --git a/acceptance/openstack/networking/v2/subnet_test.go b/acceptance/openstack/networking/v2/subnet_test.go
index 2758e2a..097a303 100644
--- a/acceptance/openstack/networking/v2/subnet_test.go
+++ b/acceptance/openstack/networking/v2/subnet_test.go
@@ -38,7 +38,7 @@
// Setup network
t.Log("Setting up network")
- n, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: true}).Extract()
+ n, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract()
th.AssertNoErr(t, err)
networkID := n.ID
defer networks.Delete(Client, networkID)
diff --git a/acceptance/rackspace/client_test.go b/acceptance/rackspace/client_test.go
new file mode 100644
index 0000000..e68aef8
--- /dev/null
+++ b/acceptance/rackspace/client_test.go
@@ -0,0 +1,29 @@
+// +build acceptance
+
+package rackspace
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/rackspace"
+)
+
+func TestAuthenticatedClient(t *testing.T) {
+ // Obtain credentials from the environment.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ t.Fatalf("Unable to acquire credentials: %v", err)
+ }
+
+ client, err := rackspace.AuthenticatedClient(ao)
+ if err != nil {
+ t.Fatalf("Unable to authenticate: %v", err)
+ }
+
+ if client.TokenID == "" {
+ t.Errorf("No token ID assigned to the client")
+ }
+
+ t.Logf("Client successfully acquired a token: %v", client.TokenID)
+}
diff --git a/acceptance/rackspace/identity/v2/extension_test.go b/acceptance/rackspace/identity/v2/extension_test.go
new file mode 100644
index 0000000..a50e015
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/extension_test.go
@@ -0,0 +1,54 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ extensions2 "github.com/rackspace/gophercloud/rackspace/identity/v2/extensions"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestExtensions(t *testing.T) {
+ service := authenticatedClient(t)
+
+ t.Logf("Extensions available on this identity endpoint:")
+ count := 0
+ var chosen string
+ err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page %02d ---", count)
+
+ extensions, err := extensions2.ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+
+ for i, ext := range extensions {
+ if chosen == "" {
+ chosen = ext.Alias
+ }
+
+ t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace)
+ t.Logf(" alias=[%s] updated=[%s]", ext.Alias, ext.Updated)
+ t.Logf(" description=[%s]", ext.Description)
+ }
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if chosen == "" {
+ t.Logf("No extensions found.")
+ return
+ }
+
+ ext, err := extensions2.Get(service, chosen).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Detail for extension [%s]:", chosen)
+ t.Logf(" name=[%s]", ext.Name)
+ t.Logf(" namespace=[%s]", ext.Namespace)
+ t.Logf(" alias=[%s]", ext.Alias)
+ t.Logf(" updated=[%s]", ext.Updated)
+ t.Logf(" description=[%s]", ext.Description)
+}
diff --git a/acceptance/rackspace/identity/v2/identity_test.go b/acceptance/rackspace/identity/v2/identity_test.go
new file mode 100644
index 0000000..019a9e6
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/identity_test.go
@@ -0,0 +1,51 @@
+// +build acceptance
+
+package v2
+
+import (
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions {
+ // Obtain credentials from the environment.
+ options := gophercloud.AuthOptions{
+ Username: os.Getenv("RS_USERNAME"),
+ APIKey: os.Getenv("RS_APIKEY"),
+ }
+
+ if options.Username == "" {
+ t.Fatal("Please provide a Rackspace username as RS_USERNAME.")
+ }
+ if options.APIKey == "" {
+ t.Fatal("Please provide a Rackspace API key as RS_APIKEY.")
+ }
+
+ return options
+}
+
+func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
+ ao := rackspaceAuthOptions(t)
+
+ provider, err := rackspace.NewClient(ao.IdentityEndpoint)
+ th.AssertNoErr(t, err)
+
+ if auth {
+ err = rackspace.Authenticate(provider, ao)
+ th.AssertNoErr(t, err)
+ }
+
+ return rackspace.NewIdentityV2(provider)
+}
+
+func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+ return createClient(t, false)
+}
+
+func authenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+ return createClient(t, true)
+}
diff --git a/acceptance/rackspace/identity/v2/tenant_test.go b/acceptance/rackspace/identity/v2/tenant_test.go
new file mode 100644
index 0000000..6081a49
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/tenant_test.go
@@ -0,0 +1,37 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ rstenants "github.com/rackspace/gophercloud/rackspace/identity/v2/tenants"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTenants(t *testing.T) {
+ service := authenticatedClient(t)
+
+ t.Logf("Tenants available to the currently issued token:")
+ count := 0
+ err := rstenants.List(service, nil).EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page %02d ---", count)
+
+ tenants, err := rstenants.ExtractTenants(page)
+ th.AssertNoErr(t, err)
+
+ for i, tenant := range tenants {
+ t.Logf("[%02d] id=[%s]", i, tenant.ID)
+ t.Logf(" name=[%s] enabled=[%v]", i, tenant.Name, tenant.Enabled)
+ t.Logf(" description=[%s]", tenant.Description)
+ }
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ if count == 0 {
+ t.Errorf("No tenants listed for your current token.")
+ }
+}
diff --git a/acceptance/rackspace/pkg.go b/acceptance/rackspace/pkg.go
new file mode 100644
index 0000000..5d17b32
--- /dev/null
+++ b/acceptance/rackspace/pkg.go
@@ -0,0 +1 @@
+package rackspace
diff --git a/endpoint_search_test.go b/endpoint_search_test.go
new file mode 100644
index 0000000..3457453
--- /dev/null
+++ b/endpoint_search_test.go
@@ -0,0 +1,19 @@
+package gophercloud
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestApplyDefaultsToEndpointOpts(t *testing.T) {
+ eo := EndpointOpts{Availability: AvailabilityPublic}
+ eo.ApplyDefaults("compute")
+ expected := EndpointOpts{Availability: AvailabilityPublic, Type: "compute"}
+ th.CheckDeepEquals(t, expected, eo)
+
+ eo = EndpointOpts{Type: "compute"}
+ eo.ApplyDefaults("object-store")
+ expected = EndpointOpts{Availability: AvailabilityPublic, Type: "compute"}
+ th.CheckDeepEquals(t, expected, eo)
+}
diff --git a/openstack/blockstorage/v1/apiversions/results.go b/openstack/blockstorage/v1/apiversions/results.go
index c4ac157..ea2f7f5 100644
--- a/openstack/blockstorage/v1/apiversions/results.go
+++ b/openstack/blockstorage/v1/apiversions/results.go
@@ -37,11 +37,8 @@
}
err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Versions, nil
+ return resp.Versions, err
}
// GetResult represents the result of a get operation.
@@ -56,9 +53,6 @@
}
err := mapstructure.Decode(r.Resp, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Version, nil
+ return resp.Version, err
}
diff --git a/openstack/blockstorage/v1/snapshots/requests.go b/openstack/blockstorage/v1/snapshots/requests.go
index f3180d7..7fac925 100644
--- a/openstack/blockstorage/v1/snapshots/requests.go
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -1,50 +1,74 @@
package snapshots
import (
+ "fmt"
+
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/racker/perigee"
)
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToSnapshotCreateMap() (map[string]interface{}, error)
+}
+
// 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
+ // OPTIONAL
+ Description string
+ // OPTIONAL
+ Force bool
+ // OPTIONAL
+ Metadata map[string]interface{}
+ // OPTIONAL
+ Name string
+ // REQUIRED
+ VolumeID string
}
-// 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
+// ToSnapshotCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.VolumeID == "" {
+ return nil, fmt.Errorf("Required CreateOpts field 'VolumeID' not set.")
+ }
+ s["volume_id"] = opts.VolumeID
+
+ if opts.Description != "" {
+ s["display_description"] = opts.Description
+ }
+ if opts.Force == true {
+ s["force"] = opts.Force
+ }
+ if opts.Metadata != nil {
+ s["metadata"] = opts.Metadata
+ }
+ if opts.Name != "" {
+ s["display_name"] = opts.Name
+ }
+
+ return map[string]interface{}{"snapshot": s}, nil
+}
+
+// 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
-
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
+
+ reqBody, err := opts.ToSnapshotCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{200, 201},
@@ -63,8 +87,8 @@
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.
+// 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{
@@ -75,6 +99,12 @@
return res
}
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+ ToSnapshotListQuery() (string, error)
+}
+
// ListOpts hold options for listing Snapshots. It is passed to the
// snapshots.List function.
type ListOpts struct {
@@ -83,15 +113,25 @@
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 {
+// ToSnapshotListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToSnapshotListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List returns Snapshots optionally limited by the conditions provided in
+// ListOpts.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(client)
if opts != nil {
- query, err := gophercloud.BuildQueryString(opts)
+ query, err := opts.ToSnapshotListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
- url += query.String()
+ url += query
}
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
@@ -100,6 +140,12 @@
return pagination.NewPager(client, url, createPage)
}
+// UpdateMetadataOptsBuilder allows extensions to add additional parameters to
+// the Update request.
+type UpdateMetadataOptsBuilder interface {
+ ToSnapshotUpdateMetadataMap() (map[string]interface{}, error)
+}
+
// UpdateMetadataOpts 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.
@@ -107,20 +153,30 @@
Metadata map[string]interface{}
}
+// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of
+// an UpdateMetadataOpts.
+func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) {
+ v := make(map[string]interface{})
+
+ if opts.Metadata != nil {
+ v["metadata"] = opts.Metadata
+ }
+
+ return v, nil
+}
+
// UpdateMetadata will update the Snapshot with provided information. To
// extract the updated Snapshot from the response, call the ExtractMetadata
// method on the UpdateMetadataResult.
-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
-
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
var res UpdateMetadataResult
+ reqBody, err := opts.ToSnapshotUpdateMetadataMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
_, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{200},
diff --git a/openstack/blockstorage/v1/snapshots/requests_test.go b/openstack/blockstorage/v1/snapshots/requests_test.go
index d29cc0d..ddfa81b 100644
--- a/openstack/blockstorage/v1/snapshots/requests_test.go
+++ b/openstack/blockstorage/v1/snapshots/requests_test.go
@@ -119,6 +119,7 @@
th.TestJSONRequest(t, r, `
{
"snapshot": {
+ "volume_id": "1234",
"display_name": "snapshot-001"
}
}
@@ -130,6 +131,7 @@
fmt.Fprintf(w, `
{
"snapshot": {
+ "volume_id": "1234",
"display_name": "snapshot-001",
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
}
@@ -137,10 +139,11 @@
`)
})
- options := &CreateOpts{Name: "snapshot-001"}
+ options := &CreateOpts{VolumeID: "1234", Name: "snapshot-001"}
n, err := Create(ServiceClient(), options).Extract()
th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.VolumeID, "1234")
th.AssertEquals(t, n.Name, "snapshot-001")
th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
}
diff --git a/openstack/blockstorage/v1/snapshots/results.go b/openstack/blockstorage/v1/snapshots/results.go
index 9509bca..dc94a32 100644
--- a/openstack/blockstorage/v1/snapshots/results.go
+++ b/openstack/blockstorage/v1/snapshots/results.go
@@ -1,8 +1,6 @@
package snapshots
import (
- "fmt"
-
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -11,19 +9,32 @@
// 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
+ // Currect status of the Snapshot.
+ Status string `mapstructure:"status"`
+ // Display name.
+ Name string `mapstructure:"display_name"`
+ // Instances onto which the Snapshot is attached.
+ Attachments []string `mapstructure:"attachments"`
+ // Logical group.
+ AvailabilityZone string `mapstructure:"availability_zone"`
+ // Is the Snapshot bootable?
+ Bootable string `mapstructure:"bootable"`
+ // Date created.
+ CreatedAt string `mapstructure:"created_at"`
+ // Display description.
+ Description string `mapstructure:"display_discription"`
+ // See VolumeType object for more information.
+ VolumeType string `mapstructure:"volume_type"`
+ // ID of the Snapshot from which this Snapshot was created.
+ SnapshotID string `mapstructure:"snapshot_id"`
+ // ID of the Volume from which this Snapshot was created.
+ VolumeID string `mapstructure:"volume_id"`
+ // User-defined key-value pairs.
+ Metadata map[string]string `mapstructure:"metadata"`
+ // Unique identifier.
+ ID string `mapstructure:"id"`
+ // Size of the Snapshot, in GB.
+ Size int `mapstructure:"size"`
}
// CreateResult contains the response body and error from a Create request.
@@ -91,8 +102,6 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("snapshots: Error decoding snapshots.commonResult: %v", err)
- }
- return res.Snapshot, nil
+
+ return res.Snapshot, err
}
diff --git a/openstack/blockstorage/v1/snapshots/util_test.go b/openstack/blockstorage/v1/snapshots/util_test.go
new file mode 100644
index 0000000..46b452e
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/util_test.go
@@ -0,0 +1,37 @@
+package snapshots
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitForStatus(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/1234", func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(2 * time.Second)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "snapshot": {
+ "display_name": "snapshot-001",
+ "id": "1234",
+ "status":"available"
+ }
+ }`)
+ })
+
+ err := WaitForStatus(ServiceClient(), "1234", "available", 0)
+ if err == nil {
+ t.Errorf("Expected error: 'Time Out in WaitFor'")
+ }
+
+ err = WaitForStatus(ServiceClient(), "1234", "available", 3)
+ th.CheckNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumes/requests.go b/openstack/blockstorage/v1/volumes/requests.go
index bca27db..042a33e 100644
--- a/openstack/blockstorage/v1/volumes/requests.go
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -1,59 +1,90 @@
package volumes
import (
+ "fmt"
+
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/racker/perigee"
)
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToVolumeCreateMap() (map[string]interface{}, error)
+}
+
// 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
+ // OPTIONAL
+ Availability string
+ // OPTIONAL
+ Description string
+ // OPTIONAL
+ Metadata map[string]string
+ // OPTIONAL
+ Name string
+ // REQUIRED
+ Size int
+ // OPTIONAL
+ SnapshotID, SourceVolID, ImageID string
+ // OPTIONAL
+ VolumeType string
+}
+
+// ToVolumeCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
+ v := make(map[string]interface{})
+
+ if opts.Size == 0 {
+ return nil, fmt.Errorf("Required CreateOpts field 'Size' not set.")
+ }
+ v["size"] = opts.Size
+
+ if opts.Availability != "" {
+ v["availability_zone"] = opts.Availability
+ }
+ if opts.Description != "" {
+ v["display_description"] = opts.Description
+ }
+ if opts.ImageID != "" {
+ v["imageRef"] = opts.ImageID
+ }
+ if opts.Metadata != nil {
+ v["metadata"] = opts.Metadata
+ }
+ if opts.Name != "" {
+ v["display_name"] = opts.Name
+ }
+ if opts.SourceVolID != "" {
+ v["source_volid"] = opts.SourceVolID
+ }
+ if opts.SnapshotID != "" {
+ v["snapshot_id"] = opts.SnapshotID
+ }
+ if opts.VolumeType != "" {
+ v["volume_type"] = opts.VolumeType
+ }
+
+ return map[string]interface{}{"volume": v}, nil
}
// 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)
-
+// the Volume object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
+
+ reqBody, err := opts.ToVolumeCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
@@ -72,8 +103,8 @@
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.
+// 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{
@@ -84,62 +115,97 @@
return res
}
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+ ToVolumeListQuery() (string, error)
+}
+
// 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.
+ // admin-only option. Set it to true to see all tenant volumes.
+ AllTenants bool `q:"all_tenants"`
+ // List only volumes that contain Metadata.
+ Metadata map[string]string `q:"metadata"`
+ // List only volumes that have Name as the display name.
+ Name string `q:"name"`
+ // List only volumes that have a status of Status.
+ Status string `q:"status"`
+}
+
+// ToVolumeListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToVolumeListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
}
// List returns Volumes optionally limited by the conditions provided in ListOpts.
-func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(client)
if opts != nil {
- query, err := gophercloud.BuildQueryString(opts)
+ query, err := opts.ToVolumeListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
- url += query.String()
+ url += query
}
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
return ListResult{pagination.SinglePageBase(r)}
}
- return pagination.NewPager(client, listURL(client), createPage)
+ return pagination.NewPager(client, url, createPage)
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToVolumeUpdateMap() (map[string]interface{}, error)
}
// 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
+ // OPTIONAL
+ Name string
+ // OPTIONAL
+ Description string
+ // OPTIONAL
+ Metadata map[string]string
+}
+
+// ToVolumeUpdateMap assembles a request body based on the contents of an
+// UpdateOpts.
+func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
+ v := make(map[string]interface{})
+
+ if opts.Description != "" {
+ v["display_description"] = opts.Description
+ }
+ if opts.Metadata != nil {
+ v["metadata"] = opts.Metadata
+ }
+ if opts.Name != "" {
+ v["display_name"] = opts.Name
+ }
+
+ return map[string]interface{}{"volume": v}, nil
}
// 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)
-
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
+ reqBody, err := opts.ToVolumeUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
_, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{200},
diff --git a/openstack/blockstorage/v1/volumes/requests_test.go b/openstack/blockstorage/v1/volumes/requests_test.go
index 54ff91d..7cd37d5 100644
--- a/openstack/blockstorage/v1/volumes/requests_test.go
+++ b/openstack/blockstorage/v1/volumes/requests_test.go
@@ -119,7 +119,7 @@
th.TestJSONRequest(t, r, `
{
"volume": {
- "display_name": "vol-001"
+ "size": 4
}
}
`)
@@ -130,18 +130,18 @@
fmt.Fprintf(w, `
{
"volume": {
- "display_name": "vol-001",
+ "size": 4,
"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
}
}
`)
})
- options := &CreateOpts{Name: "vol-001"}
+ options := &CreateOpts{Size: 4}
n, err := Create(ServiceClient(), options).Extract()
th.AssertNoErr(t, err)
- th.AssertEquals(t, n.Name, "vol-001")
+ th.AssertEquals(t, n.Size, 4)
th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
}
@@ -158,3 +158,27 @@
err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
th.AssertNoErr(t, err)
}
+
+func TestUpdate(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, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "volume": {
+ "display_name": "vol-002",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+ }
+ `)
+ })
+
+ options := &UpdateOpts{Name: "vol-002"}
+ v, err := Update(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, "vol-002", v.Name)
+}
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
index 78c863f..78eb6c1 100644
--- a/openstack/blockstorage/v1/volumes/results.go
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -1,8 +1,6 @@
package volumes
import (
- "fmt"
-
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -80,8 +78,6 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("volumes: Error decoding volumes.commonResult: %v", err)
- }
- return res.Volume, nil
+
+ return res.Volume, err
}
diff --git a/openstack/blockstorage/v1/volumes/util_test.go b/openstack/blockstorage/v1/volumes/util_test.go
new file mode 100644
index 0000000..7de1326
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/util_test.go
@@ -0,0 +1,37 @@
+package volumes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitForStatus(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes/1234", func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(2 * time.Second)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "volume": {
+ "display_name": "vol-001",
+ "id": "1234",
+ "status":"available"
+ }
+ }`)
+ })
+
+ err := WaitForStatus(ServiceClient(), "1234", "available", 0)
+ if err == nil {
+ t.Errorf("Expected error: 'Time Out in WaitFor'")
+ }
+
+ err = WaitForStatus(ServiceClient(), "1234", "available", 3)
+ th.CheckNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests.go b/openstack/blockstorage/v1/volumetypes/requests.go
index afe650d..d4f880f 100644
--- a/openstack/blockstorage/v1/volumetypes/requests.go
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -6,6 +6,12 @@
"github.com/rackspace/gophercloud/pagination"
)
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToVolumeTypeCreateMap() (map[string]interface{}, error)
+}
+
// CreateOpts are options for creating a volume type.
type CreateOpts struct {
// OPTIONAL. See VolumeType.
@@ -14,26 +20,31 @@
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"`
+// ToVolumeTypeCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToVolumeTypeCreateMap() (map[string]interface{}, error) {
+ vt := make(map[string]interface{})
+
+ if opts.ExtraSpecs != nil {
+ vt["extra_specs"] = opts.ExtraSpecs
+ }
+ if opts.Name != "" {
+ vt["name"] = opts.Name
}
- type request struct {
- VolumeType volumeType `json:"volume_type"`
- }
+ return map[string]interface{}{"volume_type": vt}, nil
+}
- reqBody := request{
- VolumeType: volumeType{},
- }
-
- reqBody.VolumeType.Name = gophercloud.MaybeString(opts.Name)
- reqBody.VolumeType.ExtraSpecs = opts.ExtraSpecs
-
+// Create will create a new volume. To extract the created volume type object,
+// call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
+
+ reqBody, err := opts.ToVolumeTypeCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{200, 201},
diff --git a/openstack/blockstorage/v1/volumetypes/results.go b/openstack/blockstorage/v1/volumetypes/results.go
index 8e5932a..77cc1f5 100644
--- a/openstack/blockstorage/v1/volumetypes/results.go
+++ b/openstack/blockstorage/v1/volumetypes/results.go
@@ -1,8 +1,6 @@
package volumetypes
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -64,9 +62,6 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Volume Type: %v", err)
- }
- return res.VolumeType, nil
+ return res.VolumeType, err
}
diff --git a/openstack/client.go b/openstack/client.go
index f3638be..97556d6 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -3,15 +3,11 @@
import (
"fmt"
"net/url"
- "strings"
"github.com/rackspace/gophercloud"
tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
- endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
- services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
"github.com/rackspace/gophercloud/openstack/utils"
- "github.com/rackspace/gophercloud/pagination"
)
const (
@@ -32,8 +28,8 @@
u.Path, u.RawQuery, u.Fragment = "", "", ""
base := u.String()
- endpoint = normalizeURL(endpoint)
- base = normalizeURL(base)
+ endpoint = gophercloud.NormalizeURL(endpoint)
+ base = gophercloud.NormalizeURL(base)
if hadPath {
return &gophercloud.ProviderClient{
@@ -99,7 +95,7 @@
v2Client.Endpoint = endpoint
}
- result := tokens2.Create(v2Client, options)
+ result := tokens2.Create(v2Client, tokens2.AuthOptions{AuthOptions: options})
token, err := result.ExtractToken()
if err != nil {
@@ -113,50 +109,12 @@
client.TokenID = token.ID
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
- return v2endpointLocator(catalog, opts)
+ return V2EndpointURL(catalog, opts)
}
return nil
}
-func v2endpointLocator(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
- // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
- var endpoints = make([]tokens2.Endpoint, 0, 1)
- for _, entry := range catalog.Entries {
- if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
- for _, endpoint := range entry.Endpoints {
- if opts.Region == "" || endpoint.Region == opts.Region {
- endpoints = append(endpoints, endpoint)
- }
- }
- }
- }
-
- // Report an error if the options were ambiguous.
- if len(endpoints) == 0 {
- return "", gophercloud.ErrEndpointNotFound
- }
- if len(endpoints) > 1 {
- return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
- }
-
- // Extract the appropriate URL from the matching Endpoint.
- for _, endpoint := range endpoints {
- switch opts.Availability {
- case gophercloud.AvailabilityPublic:
- return normalizeURL(endpoint.PublicURL), nil
- case gophercloud.AvailabilityInternal:
- return normalizeURL(endpoint.InternalURL), nil
- case gophercloud.AvailabilityAdmin:
- return normalizeURL(endpoint.AdminURL), nil
- default:
- return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
- }
- }
-
- return "", gophercloud.ErrEndpointNotFound
-}
-
// AuthenticateV3 explicitly authenticates against the identity v3 service.
func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
return v3auth(client, "", options)
@@ -176,85 +134,12 @@
client.TokenID = token.ID
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
- return v3endpointLocator(v3Client, opts)
+ return V3EndpointURL(v3Client, opts)
}
return nil
}
-func v3endpointLocator(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) {
- // Discover the service we're interested in.
- var services = make([]services3.Service, 0, 1)
- servicePager := services3.List(v3Client, services3.ListOpts{ServiceType: opts.Type})
- err := servicePager.EachPage(func(page pagination.Page) (bool, error) {
- part, err := services3.ExtractServices(page)
- if err != nil {
- return false, err
- }
-
- for _, service := range part {
- if service.Name == opts.Name {
- services = append(services, service)
- }
- }
-
- return true, nil
- })
- if err != nil {
- return "", err
- }
-
- if len(services) == 0 {
- return "", gophercloud.ErrServiceNotFound
- }
- if len(services) > 1 {
- return "", fmt.Errorf("Discovered %d matching services: %#v", len(services), services)
- }
- service := services[0]
-
- // Enumerate the endpoints available for this service.
- var endpoints []endpoints3.Endpoint
- endpointPager := endpoints3.List(v3Client, endpoints3.ListOpts{
- Availability: opts.Availability,
- ServiceID: service.ID,
- })
- err = endpointPager.EachPage(func(page pagination.Page) (bool, error) {
- part, err := endpoints3.ExtractEndpoints(page)
- if err != nil {
- return false, err
- }
-
- for _, endpoint := range part {
- if opts.Region == "" || endpoint.Region == opts.Region {
- endpoints = append(endpoints, endpoint)
- }
- }
-
- return true, nil
- })
- if err != nil {
- return "", err
- }
-
- if len(endpoints) == 0 {
- return "", gophercloud.ErrEndpointNotFound
- }
- if len(endpoints) > 1 {
- return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
- }
- endpoint := endpoints[0]
-
- return normalizeURL(endpoint.URL), nil
-}
-
-// normalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
-func normalizeURL(url string) string {
- if !strings.HasSuffix(url, "/") {
- return url + "/"
- }
- return url
-}
-
// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service.
func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
v2Endpoint := client.IdentityBase + "v2.0/"
diff --git a/openstack/common/extensions/fixtures.go b/openstack/common/extensions/fixtures.go
new file mode 100644
index 0000000..0ed7de9
--- /dev/null
+++ b/openstack/common/extensions/fixtures.go
@@ -0,0 +1,91 @@
+// +build fixtures
+
+package extensions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Extension results.
+const ListOutput = `
+{
+ "extensions": [
+ {
+ "updated": "2013-01-20T00:00:00-00:00",
+ "name": "Neutron Service Type Management",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+ "alias": "service-type",
+ "description": "API for retrieving service providers for Neutron advanced services"
+ }
+ ]
+}`
+
+// GetOutput provides a single Extension result.
+const GetOutput = `
+{
+ "extension": {
+ "updated": "2013-02-03T10:00:00-00:00",
+ "name": "agent",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
+ "alias": "agent",
+ "description": "The agent management extension."
+ }
+}
+`
+
+// ListedExtension is the Extension that should be parsed from ListOutput.
+var ListedExtension = Extension{
+ Updated: "2013-01-20T00:00:00-00:00",
+ Name: "Neutron Service Type Management",
+ Links: []interface{}{},
+ Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+ Alias: "service-type",
+ Description: "API for retrieving service providers for Neutron advanced services",
+}
+
+// ExpectedExtensions is a slice containing the Extension that should be parsed from ListOutput.
+var ExpectedExtensions = []Extension{ListedExtension}
+
+// SingleExtension is the Extension that should be parsed from GetOutput.
+var SingleExtension = &Extension{
+ Updated: "2013-02-03T10:00:00-00:00",
+ Name: "agent",
+ Links: []interface{}{},
+ Namespace: "http://docs.openstack.org/ext/agent/api/v2.0",
+ Alias: "agent",
+ Description: "The agent management extension.",
+}
+
+// HandleListExtensionsSuccessfully creates an HTTP handler at `/extensions` on the test handler
+// mux that response with a list containing a single tenant.
+func HandleListExtensionsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+
+ fmt.Fprintf(w, ListOutput)
+ })
+}
+
+// HandleGetExtensionSuccessfully creates an HTTP handler at `/extensions/agent` that responds with
+// a JSON payload corresponding to SingleExtension.
+func HandleGetExtensionSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, GetOutput)
+ })
+}
diff --git a/openstack/common/extensions/requests_test.go b/openstack/common/extensions/requests_test.go
index b0f655a..6550283 100644
--- a/openstack/common/extensions/requests_test.go
+++ b/openstack/common/extensions/requests_test.go
@@ -1,113 +1,38 @@
package extensions
import (
- "fmt"
- "net/http"
"testing"
- "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
)
-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("/extensions", 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")
-
- fmt.Fprintf(w, `
-{
- "extensions": [
- {
- "updated": "2013-01-20T00:00:00-00:00",
- "name": "Neutron Service Type Management",
- "links": [],
- "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
- "alias": "service-type",
- "description": "API for retrieving service providers for Neutron advanced services"
- }
- ]
-}
- `)
- })
+ HandleListExtensionsSuccessfully(t)
count := 0
- List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
count++
actual, err := ExtractExtensions(page)
- if err != nil {
- t.Errorf("Failed to extract extensions: %v", err)
- }
-
- expected := []Extension{
- Extension{
- Updated: "2013-01-20T00:00:00-00:00",
- Name: "Neutron Service Type Management",
- Links: []interface{}{},
- Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
- Alias: "service-type",
- Description: "API for retrieving service providers for Neutron advanced services",
- },
- }
-
- th.AssertDeepEquals(t, expected, actual)
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, ExpectedExtensions, actual)
return true, nil
})
- if count != 1 {
- t.Errorf("Expected 1 page, got %d", count)
- }
+ th.CheckEquals(t, 1, count)
}
func TestGet(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
+ HandleGetExtensionSuccessfully(t)
- th.Mux.HandleFunc("/v2.0/extensions/agent", 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, `
-{
- "extension": {
- "updated": "2013-02-03T10:00:00-00:00",
- "name": "agent",
- "links": [],
- "namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
- "alias": "agent",
- "description": "The agent management extension."
- }
-}
- `)
-
- ext, err := Get(ServiceClient(), "agent").Extract()
- th.AssertNoErr(t, err)
-
- th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
- th.AssertEquals(t, ext.Name, "agent")
- th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
- th.AssertEquals(t, ext.Alias, "agent")
- th.AssertEquals(t, ext.Description, "The agent management extension.")
- })
+ actual, err := Get(client.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, SingleExtension, actual)
}
diff --git a/openstack/common/extensions/results.go b/openstack/common/extensions/results.go
index 9319018..4827072 100755
--- a/openstack/common/extensions/results.go
+++ b/openstack/common/extensions/results.go
@@ -1,8 +1,6 @@
package extensions
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -25,21 +23,18 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding OpenStack extension: %v", err)
- }
- return res.Extension, nil
+ return res.Extension, err
}
// Extension is a struct that represents an OpenStack extension.
type Extension struct {
- Updated string `json:"updated"`
- Name string `json:"name"`
- Links []interface{} `json:"links"`
- Namespace string `json:"namespace"`
- Alias string `json:"alias"`
- Description string `json:"description"`
+ Updated string `json:"updated" mapstructure:"updated"`
+ Name string `json:"name" mapstructure:"name"`
+ Links []interface{} `json:"links" mapstructure:"links"`
+ Namespace string `json:"namespace" mapstructure:"namespace"`
+ Alias string `json:"alias" mapstructure:"alias"`
+ Description string `json:"description" mapstructure:"description"`
}
// ExtensionPage is the page returned by a pager when traversing over a collection of extensions.
@@ -65,9 +60,6 @@
}
err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Extensions, nil
+ return resp.Extensions, err
}
diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go
index 47eb172..7af11fc 100644
--- a/openstack/compute/v2/flavors/requests.go
+++ b/openstack/compute/v2/flavors/requests.go
@@ -6,41 +6,65 @@
"github.com/rackspace/gophercloud/pagination"
)
-// ListFilterOptions helps control the results returned by the List() function.
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToFlavorListQuery() (string, error)
+}
+
+// ListOpts helps control the results returned by the List() function.
// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20.
// Typically, software will use the last ID of the previous call to List to set the Marker for the current call.
-type ListFilterOptions struct {
+type ListOpts struct {
// ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided.
- ChangesSince string
+ ChangesSince string `q:"changes-since"`
// MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria.
- MinDisk, MinRAM int
+ MinDisk int `q:"minDisk"`
+ MinRAM int `q:"minRam"`
// Marker and Limit control paging.
// Marker instructs List where to start listing from.
- Marker string
+ Marker string `q:"marker"`
// Limit instructs List to refrain from sending excessively large lists of flavors.
- Limit int
+ Limit int `q:"limit"`
+}
+
+// ToFlavorListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToFlavorListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
}
// List instructs OpenStack to provide a list of flavors.
// You may provide criteria by which List curtails its results for easier processing.
-// See ListFilterOptions for more details.
-func List(client *gophercloud.ServiceClient, lfo ListFilterOptions) pagination.Pager {
+// See ListOpts for more details.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := opts.ToFlavorListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
return FlavorPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
- return pagination.NewPager(client, listURL(client, lfo), createPage)
+ return pagination.NewPager(client, url, createPage)
}
// Get instructs OpenStack to provide details on a single flavor, identified by its ID.
// Use ExtractFlavor to convert its result into a Flavor.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var gr GetResult
- gr.Err = perigee.Get(flavorURL(client, id), perigee.Options{
+ gr.Err = perigee.Get(getURL(client, id), perigee.Options{
Results: &gr.Resp,
MoreHeaders: client.Provider.AuthenticatedHeaders(),
})
diff --git a/openstack/compute/v2/flavors/requests_test.go b/openstack/compute/v2/flavors/requests_test.go
index e1b6b4f..bc9b82e 100644
--- a/openstack/compute/v2/flavors/requests_test.go
+++ b/openstack/compute/v2/flavors/requests_test.go
@@ -6,27 +6,20 @@
"reflect"
"testing"
- "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
- "github.com/rackspace/gophercloud/testhelper"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
)
const tokenID = "blerb"
-func serviceClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{
- Provider: &gophercloud.ProviderClient{TokenID: tokenID},
- Endpoint: testhelper.Endpoint(),
- }
-}
-
func TestListFlavors(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
r.ParseForm()
@@ -58,7 +51,7 @@
}
]
}
- `, testhelper.Server.URL)
+ `, th.Server.URL)
case "2":
fmt.Fprintf(w, `{ "flavors": [] }`)
default:
@@ -66,9 +59,8 @@
}
})
- client := serviceClient()
pages := 0
- err := List(client, ListFilterOptions{}).EachPage(func(page pagination.Page) (bool, error) {
+ err := List(fake.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
pages++
actual, err := ExtractFlavors(page)
@@ -96,12 +88,12 @@
}
func TestGetFlavor(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `
@@ -118,8 +110,7 @@
`)
})
- client := serviceClient()
- actual, err := Get(client, "12345").Extract()
+ actual, err := Get(fake.ServiceClient(), "12345").Extract()
if err != nil {
t.Fatalf("Unable to get flavor: %v", err)
}
diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go
index 68c8f58..1e274e3 100644
--- a/openstack/compute/v2/flavors/results.go
+++ b/openstack/compute/v2/flavors/results.go
@@ -78,12 +78,8 @@
// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
func (p FlavorPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"flavors_links"`
+ Links []gophercloud.Link `mapstructure:"flavors_links"`
}
var r resp
@@ -92,17 +88,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) {
diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go
index 9e5b562..683c107 100644
--- a/openstack/compute/v2/flavors/urls.go
+++ b/openstack/compute/v2/flavors/urls.go
@@ -1,37 +1,13 @@
package flavors
import (
- "fmt"
- "net/url"
- "strconv"
-
"github.com/rackspace/gophercloud"
)
-func listURL(client *gophercloud.ServiceClient, lfo ListFilterOptions) string {
- v := url.Values{}
- if lfo.ChangesSince != "" {
- v.Set("changes-since", lfo.ChangesSince)
- }
- if lfo.MinDisk != 0 {
- v.Set("minDisk", strconv.Itoa(lfo.MinDisk))
- }
- if lfo.MinRAM != 0 {
- v.Set("minRam", strconv.Itoa(lfo.MinRAM))
- }
- if lfo.Marker != "" {
- v.Set("marker", lfo.Marker)
- }
- if lfo.Limit != 0 {
- v.Set("limit", strconv.Itoa(lfo.Limit))
- }
- tail := ""
- if len(v) > 0 {
- tail = fmt.Sprintf("?%s", v.Encode())
- }
- return client.ServiceURL("flavors", "detail") + tail
+func getURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("flavors", id)
}
-func flavorURL(client *gophercloud.ServiceClient, id string) string {
- return client.ServiceURL("flavors", id)
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("flavors", "detail")
}
diff --git a/openstack/compute/v2/flavors/urls_test.go b/openstack/compute/v2/flavors/urls_test.go
new file mode 100644
index 0000000..069da24
--- /dev/null
+++ b/openstack/compute/v2/flavors/urls_test.go
@@ -0,0 +1,26 @@
+package flavors
+
+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(), "foo")
+ expected := endpoint + "flavors/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "flavors/detail"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/images/requests.go b/openstack/compute/v2/images/requests.go
index a887cc6..603909c 100644
--- a/openstack/compute/v2/images/requests.go
+++ b/openstack/compute/v2/images/requests.go
@@ -1,25 +1,68 @@
package images
import (
- "github.com/racker/perigee"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
)
-// List enumerates the available images.
-func List(client *gophercloud.ServiceClient) pagination.Pager {
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToImageListQuery() (string, error)
+}
+
+// ListOpts contain options for limiting the number of Images returned from a call to ListDetail.
+type ListOpts struct {
+ // When the image last changed status (in date-time format).
+ ChangesSince string `q:"changes-since"`
+ // The number of Images to return.
+ Limit int `q:"limit"`
+ // UUID of the Image at which to set a marker.
+ Marker string `q:"marker"`
+ // The name of the Image.
+ Name string `q:"name:"`
+ // The name of the Server (in URL format).
+ Server string `q:"server"`
+ // The current status of the Image.
+ Status string `q:"status"`
+ // The value of the type of image (e.g. BASE, SERVER, ALL)
+ Type string `q:"type"`
+}
+
+// ToImageListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToImageListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// ListDetail enumerates the available images.
+func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listDetailURL(client)
+ if opts != nil {
+ query, err := opts.ToImageListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
return ImagePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
- return pagination.NewPager(client, listURL(client), createPage)
+ return pagination.NewPager(client, url, createPage)
}
// Get acquires additional detail about a specific image by ID.
// Use ExtractImage() to intepret the result as an openstack Image.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var result GetResult
- _, result.Err = perigee.Request("GET", imageURL(client, id), perigee.Options{
+ _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
Results: &result.Resp,
OkCodes: []int{200},
diff --git a/openstack/compute/v2/images/requests_test.go b/openstack/compute/v2/images/requests_test.go
index 396c21f..9a05f97 100644
--- a/openstack/compute/v2/images/requests_test.go
+++ b/openstack/compute/v2/images/requests_test.go
@@ -1,32 +1,24 @@
package images
import (
+ "encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
- "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
- "github.com/rackspace/gophercloud/testhelper"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
)
-const tokenID = "aaaaaa"
-
-func serviceClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{
- Provider: &gophercloud.ProviderClient{TokenID: tokenID},
- Endpoint: testhelper.Endpoint(),
- }
-}
-
func TestListImages(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
r.ParseForm()
@@ -70,9 +62,9 @@
}
})
- client := serviceClient()
pages := 0
- err := List(client).EachPage(func(page pagination.Page) (bool, error) {
+ options := &ListOpts{Limit: 2}
+ err := ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) {
pages++
actual, err := ExtractImages(page)
@@ -119,12 +111,12 @@
}
func TestGetImage(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `
@@ -145,8 +137,7 @@
`)
})
- client := serviceClient()
- actual, err := Get(client, "12345678").Extract()
+ actual, err := Get(fake.ServiceClient(), "12345678").Extract()
if err != nil {
t.Fatalf("Unexpected error from Get: %v", err)
}
@@ -166,3 +157,19 @@
t.Errorf("Expected %#v, but got %#v", expected, actual)
}
}
+
+func TestNextPageURL(t *testing.T) {
+ var page ImagePage
+ var body map[string]interface{}
+ bodyString := []byte(`{"images":{"links":[{"href":"http://192.154.23.87/12345/images/image3","rel":"bookmark"}]}, "images_links":[{"href":"http://192.154.23.87/12345/images/image4","rel":"next"}]}`)
+ err := json.Unmarshal(bodyString, &body)
+ if err != nil {
+ t.Fatalf("Error unmarshaling data into page body: %v", err)
+ }
+ page.Body = body
+
+ expected := "http://192.154.23.87/12345/images/image4"
+ actual, err := page.NextPageURL()
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/images/results.go b/openstack/compute/v2/images/results.go
index f93c90c..3c22eeb 100644
--- a/openstack/compute/v2/images/results.go
+++ b/openstack/compute/v2/images/results.go
@@ -65,12 +65,8 @@
// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
func (page ImagePage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"images_links"`
+ Links []gophercloud.Link `mapstructure:"images_links"`
}
var r resp
@@ -79,17 +75,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// ExtractImages converts a page of List results into a slice of usable Image structs.
diff --git a/openstack/compute/v2/images/urls.go b/openstack/compute/v2/images/urls.go
index 4ae2269..9b3c86d 100644
--- a/openstack/compute/v2/images/urls.go
+++ b/openstack/compute/v2/images/urls.go
@@ -2,10 +2,10 @@
import "github.com/rackspace/gophercloud"
-func listURL(client *gophercloud.ServiceClient) string {
+func listDetailURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("images", "detail")
}
-func imageURL(client *gophercloud.ServiceClient, id string) string {
+func getURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("images", id)
}
diff --git a/openstack/compute/v2/images/urls_test.go b/openstack/compute/v2/images/urls_test.go
new file mode 100644
index 0000000..b1ab3d6
--- /dev/null
+++ b/openstack/compute/v2/images/urls_test.go
@@ -0,0 +1,26 @@
+package images
+
+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(), "foo")
+ expected := endpoint + "images/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+ actual := listDetailURL(endpointClient())
+ expected := endpoint + "images/detail"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index d22cb46..386e0f6 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -15,7 +15,7 @@
return ServerPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
- return pagination.NewPager(client, detailURL(client), createPage)
+ return pagination.NewPager(client, listDetailURL(client), createPage)
}
// CreateOptsBuilder describes struct types that can be accepted by the Create call.
@@ -140,7 +140,7 @@
// Delete requests that a server previously provisioned be removed from your account.
func Delete(client *gophercloud.ServiceClient, id string) error {
- _, err := perigee.Request("DELETE", serverURL(client, id), perigee.Options{
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
@@ -150,15 +150,15 @@
// Get requests details on a single server, by ID.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var result GetResult
- _, result.Err = perigee.Request("GET", serverURL(client, id), perigee.Options{
+ _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
Results: &result.Resp,
MoreHeaders: client.Provider.AuthenticatedHeaders(),
})
return result
}
-// UpdateOptsLike allows extentions to add additional attributes to the Update request.
-type UpdateOptsLike interface {
+// UpdateOptsBuilder allows extentions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
ToServerUpdateMap() map[string]interface{}
}
@@ -192,9 +192,9 @@
}
// Update requests that various attributes of the indicated server be changed.
-func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsLike) UpdateResult {
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var result UpdateResult
- _, result.Err = perigee.Request("PUT", serverURL(client, id), perigee.Options{
+ _, result.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
Results: &result.Resp,
ReqBody: opts.ToServerUpdateMap(),
MoreHeaders: client.Provider.AuthenticatedHeaders(),
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index dd2e651..d284ed8 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -116,12 +116,8 @@
// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
func (page ServerPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"servers_links"`
+ Links []gophercloud.Link `mapstructure:"servers_links"`
}
var r resp
@@ -130,17 +126,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index 52be73e..57587ab 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -2,18 +2,30 @@
import "github.com/rackspace/gophercloud"
-func listURL(client *gophercloud.ServiceClient) string {
+func createURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("servers")
}
-func detailURL(client *gophercloud.ServiceClient) string {
+func listURL(client *gophercloud.ServiceClient) string {
+ return createURL(client)
+}
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("servers", "detail")
}
-func serverURL(client *gophercloud.ServiceClient, id string) string {
+func deleteURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id)
}
+func getURL(client *gophercloud.ServiceClient, id string) string {
+ return deleteURL(client, id)
+}
+
+func updateURL(client *gophercloud.ServiceClient, id string) string {
+ return deleteURL(client, id)
+}
+
func actionURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "action")
}
diff --git a/openstack/compute/v2/servers/urls_test.go b/openstack/compute/v2/servers/urls_test.go
new file mode 100644
index 0000000..cc895c9
--- /dev/null
+++ b/openstack/compute/v2/servers/urls_test.go
@@ -0,0 +1,56 @@
+package servers
+
+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 + "servers"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "servers"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+ actual := listDetailURL(endpointClient())
+ expected := endpoint + "servers/detail"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestActionURL(t *testing.T) {
+ actual := actionURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo/action"
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go
new file mode 100644
index 0000000..5a311e4
--- /dev/null
+++ b/openstack/endpoint_location.go
@@ -0,0 +1,124 @@
+package openstack
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+ endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
+ services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired
+// during the v2 identity service. The specified EndpointOpts are used to identify a unique,
+// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
+// criteria and when none do. The minimum that can be specified is a Type, but you will also often
+// need to specify a Name and/or a Region depending on what's available on your OpenStack
+// deployment.
+func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
+ // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
+ var endpoints = make([]tokens2.Endpoint, 0, 1)
+ for _, entry := range catalog.Entries {
+ if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
+ for _, endpoint := range entry.Endpoints {
+ if opts.Region == "" || endpoint.Region == opts.Region {
+ endpoints = append(endpoints, endpoint)
+ }
+ }
+ }
+ }
+
+ // Report an error if the options were ambiguous.
+ if len(endpoints) > 1 {
+ return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
+ }
+
+ // Extract the appropriate URL from the matching Endpoint.
+ for _, endpoint := range endpoints {
+ switch opts.Availability {
+ case gophercloud.AvailabilityPublic:
+ return gophercloud.NormalizeURL(endpoint.PublicURL), nil
+ case gophercloud.AvailabilityInternal:
+ return gophercloud.NormalizeURL(endpoint.InternalURL), nil
+ case gophercloud.AvailabilityAdmin:
+ return gophercloud.NormalizeURL(endpoint.AdminURL), nil
+ default:
+ return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
+ }
+ }
+
+ // Report an error if there were no matching endpoints.
+ return "", gophercloud.ErrEndpointNotFound
+}
+
+// V3EndpointURL discovers the endpoint URL for a specific service using multiple calls against
+// an identity v3 service endpoint. The specified EndpointOpts are used to identify a unique,
+// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
+// criteria and when none do. The minimum that can be specified is a Type, but you will also often
+// need to specify a Name and/or a Region depending on what's available on your OpenStack
+// deployment.
+func V3EndpointURL(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) {
+ // Discover the service we're interested in.
+ var services = make([]services3.Service, 0, 1)
+ servicePager := services3.List(v3Client, services3.ListOpts{ServiceType: opts.Type})
+ err := servicePager.EachPage(func(page pagination.Page) (bool, error) {
+ part, err := services3.ExtractServices(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, service := range part {
+ if service.Name == opts.Name {
+ services = append(services, service)
+ }
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ return "", err
+ }
+
+ if len(services) == 0 {
+ return "", gophercloud.ErrServiceNotFound
+ }
+ if len(services) > 1 {
+ return "", fmt.Errorf("Discovered %d matching services: %#v", len(services), services)
+ }
+ service := services[0]
+
+ // Enumerate the endpoints available for this service.
+ var endpoints []endpoints3.Endpoint
+ endpointPager := endpoints3.List(v3Client, endpoints3.ListOpts{
+ Availability: opts.Availability,
+ ServiceID: service.ID,
+ })
+ err = endpointPager.EachPage(func(page pagination.Page) (bool, error) {
+ part, err := endpoints3.ExtractEndpoints(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, endpoint := range part {
+ if opts.Region == "" || endpoint.Region == opts.Region {
+ endpoints = append(endpoints, endpoint)
+ }
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ return "", err
+ }
+
+ if len(endpoints) == 0 {
+ return "", gophercloud.ErrEndpointNotFound
+ }
+ if len(endpoints) > 1 {
+ return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
+ }
+ endpoint := endpoints[0]
+
+ return gophercloud.NormalizeURL(endpoint.URL), nil
+}
diff --git a/openstack/endpoint_location_test.go b/openstack/endpoint_location_test.go
new file mode 100644
index 0000000..4e0569a
--- /dev/null
+++ b/openstack/endpoint_location_test.go
@@ -0,0 +1,225 @@
+package openstack
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// Service catalog fixtures take too much vertical space!
+var catalog2 = tokens2.ServiceCatalog{
+ Entries: []tokens2.CatalogEntry{
+ tokens2.CatalogEntry{
+ Type: "same",
+ Name: "same",
+ Endpoints: []tokens2.Endpoint{
+ tokens2.Endpoint{
+ Region: "same",
+ PublicURL: "https://public.correct.com/",
+ InternalURL: "https://internal.correct.com/",
+ AdminURL: "https://admin.correct.com/",
+ },
+ tokens2.Endpoint{
+ Region: "different",
+ PublicURL: "https://badregion.com/",
+ },
+ },
+ },
+ tokens2.CatalogEntry{
+ Type: "same",
+ Name: "different",
+ Endpoints: []tokens2.Endpoint{
+ tokens2.Endpoint{
+ Region: "same",
+ PublicURL: "https://badname.com/",
+ },
+ tokens2.Endpoint{
+ Region: "different",
+ PublicURL: "https://badname.com/+badregion",
+ },
+ },
+ },
+ tokens2.CatalogEntry{
+ Type: "different",
+ Name: "different",
+ Endpoints: []tokens2.Endpoint{
+ tokens2.Endpoint{
+ Region: "same",
+ PublicURL: "https://badtype.com/+badname",
+ },
+ tokens2.Endpoint{
+ Region: "different",
+ PublicURL: "https://badtype.com/+badregion+badname",
+ },
+ },
+ },
+ },
+}
+
+func TestV2EndpointExact(t *testing.T) {
+ expectedURLs := map[gophercloud.Availability]string{
+ gophercloud.AvailabilityPublic: "https://public.correct.com/",
+ gophercloud.AvailabilityAdmin: "https://admin.correct.com/",
+ gophercloud.AvailabilityInternal: "https://internal.correct.com/",
+ }
+
+ for availability, expected := range expectedURLs {
+ actual, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+ Type: "same",
+ Name: "same",
+ Region: "same",
+ Availability: availability,
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, expected, actual)
+ }
+}
+
+func TestV2EndpointNone(t *testing.T) {
+ _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+ Type: "nope",
+ Availability: gophercloud.AvailabilityPublic,
+ })
+ th.CheckEquals(t, gophercloud.ErrEndpointNotFound, err)
+}
+
+func TestV2EndpointMultiple(t *testing.T) {
+ _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+ Type: "same",
+ Region: "same",
+ Availability: gophercloud.AvailabilityPublic,
+ })
+ if !strings.HasPrefix(err.Error(), "Discovered 2 matching endpoints:") {
+ t.Errorf("Received unexpected error: %v", err)
+ }
+}
+
+func TestV2EndpointBadAvailability(t *testing.T) {
+ _, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+ Type: "same",
+ Name: "same",
+ Region: "same",
+ Availability: "wat",
+ })
+ th.CheckEquals(t, err.Error(), "Unexpected availability in endpoint query: wat")
+}
+
+func setupV3Responses(t *testing.T) {
+ // Mock the service query.
+ th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "links": {
+ "next": null,
+ "previous": null
+ },
+ "services": [
+ {
+ "description": "Correct",
+ "id": "1234",
+ "name": "same",
+ "type": "same"
+ },
+ {
+ "description": "Bad Name",
+ "id": "9876",
+ "name": "different",
+ "type": "same"
+ }
+ ]
+ }
+ `)
+ })
+
+ // Mock the endpoint query.
+ th.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestFormValues(t, r, map[string]string{
+ "service_id": "1234",
+ "interface": "public",
+ })
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "endpoints": [
+ {
+ "id": "12",
+ "interface": "public",
+ "name": "the-right-one",
+ "region": "same",
+ "service_id": "1234",
+ "url": "https://correct:9000/"
+ },
+ {
+ "id": "14",
+ "interface": "public",
+ "name": "bad-region",
+ "region": "different",
+ "service_id": "1234",
+ "url": "https://bad-region:9001/"
+ }
+ ],
+ "links": {
+ "next": null,
+ "previous": null
+ }
+ }
+ `)
+ })
+}
+
+func TestV3EndpointExact(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ setupV3Responses(t)
+
+ actual, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{
+ Type: "same",
+ Name: "same",
+ Region: "same",
+ Availability: gophercloud.AvailabilityPublic,
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, actual, "https://correct:9000/")
+}
+
+func TestV3EndpointNoService(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "links": {
+ "next": null,
+ "previous": null
+ },
+ "services": []
+ }
+ `)
+ })
+
+ _, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{
+ Type: "nope",
+ Name: "same",
+ Region: "same",
+ Availability: gophercloud.AvailabilityPublic,
+ })
+ th.CheckEquals(t, gophercloud.ErrServiceNotFound, err)
+}
diff --git a/openstack/identity/v2/extensions/delegate.go b/openstack/identity/v2/extensions/delegate.go
index 1992a2c..cee275f 100644
--- a/openstack/identity/v2/extensions/delegate.go
+++ b/openstack/identity/v2/extensions/delegate.go
@@ -7,16 +7,6 @@
"github.com/rackspace/gophercloud/pagination"
)
-// Extension is a single OpenStack extension.
-type Extension struct {
- common.Extension
-}
-
-// GetResult wraps a GetResult from common.
-type GetResult struct {
- common.GetResult
-}
-
// ExtensionPage is a single page of Extension results.
type ExtensionPage struct {
common.ExtensionPage
@@ -33,53 +23,30 @@
// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
// elements into a slice of Extension structs.
-func ExtractExtensions(page pagination.Page) ([]Extension, error) {
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
// Identity v2 adds an intermediate "values" object.
- type extension struct {
- Updated string `mapstructure:"updated"`
- Name string `mapstructure:"name"`
- Namespace string `mapstructure:"namespace"`
- Alias string `mapstructure:"alias"`
- Description string `mapstructure:"description"`
- Links []interface{} `mapstructure:"links"`
- }
-
var resp struct {
Extensions struct {
- Values []extension `mapstructure:"values"`
+ Values []common.Extension `mapstructure:"values"`
} `mapstructure:"extensions"`
}
err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
- if err != nil {
- return nil, err
- }
-
- exts := make([]Extension, len(resp.Extensions.Values))
- for i, original := range resp.Extensions.Values {
- exts[i] = Extension{common.Extension{
- Updated: original.Updated,
- Name: original.Name,
- Namespace: original.Namespace,
- Alias: original.Alias,
- Description: original.Description,
- Links: original.Links,
- }}
- }
-
- return exts, err
+ return resp.Extensions.Values, err
}
// Get retrieves information for a specific extension using its alias.
-func Get(c *gophercloud.ServiceClient, alias string) GetResult {
- return GetResult{common.Get(c, alias)}
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+ return common.Get(c, alias)
}
// List returns a Pager which allows you to iterate over the full collection of extensions.
// It does not accept query parameters.
func List(c *gophercloud.ServiceClient) pagination.Pager {
- return pagination.NewPager(c, common.ListExtensionURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
- return ExtensionPage{common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)}}
+ return common.List(c).WithPageCreator(func(r pagination.LastHTTPResponse) pagination.Page {
+ return ExtensionPage{
+ ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)},
+ }
})
}
diff --git a/openstack/identity/v2/extensions/delegate_test.go b/openstack/identity/v2/extensions/delegate_test.go
index 90eb21b..504118a 100644
--- a/openstack/identity/v2/extensions/delegate_test.go
+++ b/openstack/identity/v2/extensions/delegate_test.go
@@ -1,76 +1,25 @@
package extensions
import (
- "fmt"
- "net/http"
"testing"
- "github.com/rackspace/gophercloud"
common "github.com/rackspace/gophercloud/openstack/common/extensions"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
)
-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("/extensions", 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")
-
- fmt.Fprintf(w, `
-{
- "extensions": {
- "values": [
- {
- "updated": "2013-01-20T00:00:00-00:00",
- "name": "Neutron Service Type Management",
- "links": [],
- "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
- "alias": "service-type",
- "description": "API for retrieving service providers for Neutron advanced services"
- }
- ]
- }
-}
- `)
- })
+ HandleListExtensionsSuccessfully(t)
count := 0
-
- err := List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
count++
actual, err := ExtractExtensions(page)
th.AssertNoErr(t, err)
-
- expected := []Extension{
- Extension{
- common.Extension{
- Updated: "2013-01-20T00:00:00-00:00",
- Name: "Neutron Service Type Management",
- Links: []interface{}{},
- Namespace: "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
- Alias: "service-type",
- Description: "API for retrieving service providers for Neutron advanced services",
- },
- },
- }
-
- th.AssertDeepEquals(t, expected, actual)
+ th.CheckDeepEquals(t, common.ExpectedExtensions, actual)
return true, nil
})
@@ -81,34 +30,9 @@
func TestGet(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
+ common.HandleGetExtensionSuccessfully(t)
- th.Mux.HandleFunc("/extensions/agent", 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, `
-{
- "extension": {
- "updated": "2013-02-03T10:00:00-00:00",
- "name": "agent",
- "links": [],
- "namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
- "alias": "agent",
- "description": "The agent management extension."
- }
-}
- `)
-
- ext, err := Get(ServiceClient(), "agent").Extract()
- th.AssertNoErr(t, err)
-
- th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
- th.AssertEquals(t, ext.Name, "agent")
- th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
- th.AssertEquals(t, ext.Alias, "agent")
- th.AssertEquals(t, ext.Description, "The agent management extension.")
- })
+ actual, err := Get(client.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, common.SingleExtension, actual)
}
diff --git a/openstack/identity/v2/extensions/fixtures.go b/openstack/identity/v2/extensions/fixtures.go
new file mode 100644
index 0000000..96cb7d2
--- /dev/null
+++ b/openstack/identity/v2/extensions/fixtures.go
@@ -0,0 +1,60 @@
+// +build fixtures
+
+package extensions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single Extension result. It differs from the delegated implementation
+// by the introduction of an intermediate "values" member.
+const ListOutput = `
+{
+ "extensions": {
+ "values": [
+ {
+ "updated": "2013-01-20T00:00:00-00:00",
+ "name": "Neutron Service Type Management",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+ "alias": "service-type",
+ "description": "API for retrieving service providers for Neutron advanced services"
+ }
+ ]
+ }
+}
+`
+
+// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List
+// call.
+func HandleListExtensionsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+
+ fmt.Fprintf(w, `
+{
+ "extensions": {
+ "values": [
+ {
+ "updated": "2013-01-20T00:00:00-00:00",
+ "name": "Neutron Service Type Management",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+ "alias": "service-type",
+ "description": "API for retrieving service providers for Neutron advanced services"
+ }
+ ]
+ }
+}
+ `)
+ })
+
+}
diff --git a/openstack/identity/v2/tenants/fixtures.go b/openstack/identity/v2/tenants/fixtures.go
new file mode 100644
index 0000000..7f044ac
--- /dev/null
+++ b/openstack/identity/v2/tenants/fixtures.go
@@ -0,0 +1,65 @@
+// +build fixtures
+
+package tenants
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Tenant results.
+const ListOutput = `
+{
+ "tenants": [
+ {
+ "id": "1234",
+ "name": "Red Team",
+ "description": "The team that is red",
+ "enabled": true
+ },
+ {
+ "id": "9876",
+ "name": "Blue Team",
+ "description": "The team that is blue",
+ "enabled": false
+ }
+ ]
+}
+`
+
+// RedTeam is a Tenant fixture.
+var RedTeam = Tenant{
+ ID: "1234",
+ Name: "Red Team",
+ Description: "The team that is red",
+ Enabled: true,
+}
+
+// BlueTeam is a Tenant fixture.
+var BlueTeam = Tenant{
+ ID: "9876",
+ Name: "Blue Team",
+ Description: "The team that is blue",
+ Enabled: false,
+}
+
+// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput.
+var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam}
+
+// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that
+// responds with a list of two tenants.
+func HandleListTenantsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, ListOutput)
+ })
+}
diff --git a/openstack/identity/v2/tenants/requests_test.go b/openstack/identity/v2/tenants/requests_test.go
index 685bf96..e8f172d 100644
--- a/openstack/identity/v2/tenants/requests_test.go
+++ b/openstack/identity/v2/tenants/requests_test.go
@@ -1,76 +1,26 @@
package tenants
import (
- "fmt"
- "net/http"
"testing"
- "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
)
-const tokenID = "1234123412341234"
-
func TestListTenants(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
-
- th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "Accept", "application/json")
- th.TestHeader(t, r, "X-Auth-Token", tokenID)
-
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, `
-{
- "tenants": [
- {
- "id": "1234",
- "name": "Red Team",
- "description": "The team that is red",
- "enabled": true
- },
- {
- "id": "9876",
- "name": "Blue Team",
- "description": "The team that is blue",
- "enabled": false
- }
- ]
-}
- `)
- })
-
- client := &gophercloud.ServiceClient{
- Provider: &gophercloud.ProviderClient{TokenID: tokenID},
- Endpoint: th.Endpoint(),
- }
+ HandleListTenantsSuccessfully(t)
count := 0
- err := List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
count++
actual, err := ExtractTenants(page)
th.AssertNoErr(t, err)
- expected := []Tenant{
- Tenant{
- ID: "1234",
- Name: "Red Team",
- Description: "The team that is red",
- Enabled: true,
- },
- Tenant{
- ID: "9876",
- Name: "Blue Team",
- Description: "The team that is blue",
- Enabled: false,
- },
- }
-
- th.CheckDeepEquals(t, expected, actual)
+ th.CheckDeepEquals(t, ExpectedTenantSlice, actual)
return true, nil
})
diff --git a/openstack/identity/v2/tenants/results.go b/openstack/identity/v2/tenants/results.go
index e4e3f47..c1220c3 100644
--- a/openstack/identity/v2/tenants/results.go
+++ b/openstack/identity/v2/tenants/results.go
@@ -2,6 +2,7 @@
import (
"github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
@@ -36,12 +37,8 @@
// NextPageURL extracts the "next" link from the tenants_links section of the result.
func (page TenantPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"tenants_links"`
+ Links []gophercloud.Link `mapstructure:"tenants_links"`
}
var r resp
@@ -50,17 +47,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// ExtractTenants returns a slice of Tenants contained in a single page of results.
diff --git a/openstack/identity/v2/tokens/errors.go b/openstack/identity/v2/tokens/errors.go
index 244db1b..3a9172e 100644
--- a/openstack/identity/v2/tokens/errors.go
+++ b/openstack/identity/v2/tokens/errors.go
@@ -9,6 +9,9 @@
// ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
ErrUserIDProvided = unacceptedAttributeErr("UserID")
+ // ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey.
+ ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
// ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
@@ -18,8 +21,8 @@
// ErrUsernameRequired is returned if you attempt ot authenticate without a Username.
ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.")
- // ErrPasswordOrAPIKey is returned if you provide both a password and an API key.
- ErrPasswordOrAPIKey = errors.New("Please supply exactly one of Password or APIKey in your AuthOptions.")
+ // ErrPasswordRequired is returned if you don't provide a password.
+ ErrPasswordRequired = errors.New("Please supply a Password in your AuthOptions.")
)
func unacceptedAttributeErr(attribute string) error {
diff --git a/openstack/identity/v2/tokens/fixtures.go b/openstack/identity/v2/tokens/fixtures.go
new file mode 100644
index 0000000..1cb0d05
--- /dev/null
+++ b/openstack/identity/v2/tokens/fixtures.go
@@ -0,0 +1,128 @@
+// +build fixtures
+
+package tokens
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// ExpectedToken is the token that should be parsed from TokenCreationResponse.
+var ExpectedToken = &Token{
+ ID: "aaaabbbbccccdddd",
+ ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
+ Tenant: tenants.Tenant{
+ ID: "fc394f2ab2df4114bde39905f800dc57",
+ Name: "test",
+ Description: "There are many tenants. This one is yours.",
+ Enabled: true,
+ },
+}
+
+// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse.
+var ExpectedServiceCatalog = &ServiceCatalog{
+ Entries: []CatalogEntry{
+ CatalogEntry{
+ Name: "inscrutablewalrus",
+ Type: "something",
+ Endpoints: []Endpoint{
+ Endpoint{
+ PublicURL: "http://something0:1234/v2/",
+ Region: "region0",
+ },
+ Endpoint{
+ PublicURL: "http://something1:1234/v2/",
+ Region: "region1",
+ },
+ },
+ },
+ CatalogEntry{
+ Name: "arbitrarypenguin",
+ Type: "else",
+ Endpoints: []Endpoint{
+ Endpoint{
+ PublicURL: "http://else0:4321/v3/",
+ Region: "region0",
+ },
+ },
+ },
+ },
+}
+
+// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog.
+const TokenCreationResponse = `
+{
+ "access": {
+ "token": {
+ "issued_at": "2014-01-30T15:30:58.000000Z",
+ "expires": "2014-01-31T15:30:58Z",
+ "id": "aaaabbbbccccdddd",
+ "tenant": {
+ "description": "There are many tenants. This one is yours.",
+ "enabled": true,
+ "id": "fc394f2ab2df4114bde39905f800dc57",
+ "name": "test"
+ }
+ },
+ "serviceCatalog": [
+ {
+ "endpoints": [
+ {
+ "publicURL": "http://something0:1234/v2/",
+ "region": "region0"
+ },
+ {
+ "publicURL": "http://something1:1234/v2/",
+ "region": "region1"
+ }
+ ],
+ "type": "something",
+ "name": "inscrutablewalrus"
+ },
+ {
+ "endpoints": [
+ {
+ "publicURL": "http://else0:4321/v3/",
+ "region": "region0"
+ }
+ ],
+ "type": "else",
+ "name": "arbitrarypenguin"
+ }
+ ]
+ }
+}
+`
+
+// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been
+// constructed properly given certain auth options, and returns the result.
+func HandleTokenPost(t *testing.T, requestJSON string) {
+ th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ if requestJSON != "" {
+ th.TestJSONRequest(t, r, requestJSON)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, TokenCreationResponse)
+ })
+}
+
+// IsSuccessful ensures that a CreateResult was successful and contains the correct token and
+// service catalog.
+func IsSuccessful(t *testing.T, result CreateResult) {
+ token, err := result.ExtractToken()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedToken, token)
+
+ serviceCatalog, err := result.ExtractServiceCatalog()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog)
+}
diff --git a/openstack/identity/v2/tokens/requests.go b/openstack/identity/v2/tokens/requests.go
index 5bd5a70..c25a72b 100644
--- a/openstack/identity/v2/tokens/requests.go
+++ b/openstack/identity/v2/tokens/requests.go
@@ -5,73 +5,80 @@
"github.com/rackspace/gophercloud"
)
+// AuthOptionsBuilder describes any argument that may be passed to the Create call.
+type AuthOptionsBuilder interface {
+
+ // ToTokenCreateMap assembles the Create request body, returning an error if parameters are
+ // missing or inconsistent.
+ ToTokenCreateMap() (map[string]interface{}, error)
+}
+
+// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
+// interface.
+type AuthOptions struct {
+ gophercloud.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+ return AuthOptions{AuthOptions: original}
+}
+
+// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON
+// request.
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+ // Error out if an unsupported auth option is present.
+ if auth.UserID != "" {
+ return nil, ErrUserIDProvided
+ }
+ if auth.APIKey != "" {
+ return nil, ErrAPIKeyProvided
+ }
+ if auth.DomainID != "" {
+ return nil, ErrDomainIDProvided
+ }
+ if auth.DomainName != "" {
+ return nil, ErrDomainNameProvided
+ }
+
+ // Username and Password are always required.
+ if auth.Username == "" {
+ return nil, ErrUsernameRequired
+ }
+ if auth.Password == "" {
+ return nil, ErrPasswordRequired
+ }
+
+ // Populate the request map.
+ authMap := make(map[string]interface{})
+
+ authMap["passwordCredentials"] = map[string]interface{}{
+ "username": auth.Username,
+ "password": auth.Password,
+ }
+
+ if auth.TenantID != "" {
+ authMap["tenantId"] = auth.TenantID
+ }
+ if auth.TenantName != "" {
+ authMap["tenantName"] = auth.TenantName
+ }
+
+ return map[string]interface{}{"auth": authMap}, nil
+}
+
// Create authenticates to the identity service and attempts to acquire a Token.
// If successful, the CreateResult
// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(),
// which abstracts all of the gory details about navigating service catalogs and such.
-func Create(client *gophercloud.ServiceClient, auth gophercloud.AuthOptions) CreateResult {
- type passwordCredentials struct {
- Username string `json:"username"`
- Password string `json:"password"`
+func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateResult {
+ request, err := auth.ToTokenCreateMap()
+ if err != nil {
+ return CreateResult{gophercloud.CommonResult{Err: err}}
}
- type apiKeyCredentials struct {
- Username string `json:"username"`
- APIKey string `json:"apiKey"`
- }
-
- var request struct {
- Auth struct {
- PasswordCredentials *passwordCredentials `json:"passwordCredentials,omitempty"`
- APIKeyCredentials *apiKeyCredentials `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
- TenantID string `json:"tenantId,omitempty"`
- TenantName string `json:"tenantName,omitempty"`
- } `json:"auth"`
- }
-
- // Error out if an unsupported auth option is present.
- if auth.UserID != "" {
- return createErr(ErrUserIDProvided)
- }
- if auth.DomainID != "" {
- return createErr(ErrDomainIDProvided)
- }
- if auth.DomainName != "" {
- return createErr(ErrDomainNameProvided)
- }
-
- // Username is always required.
- if auth.Username == "" {
- return createErr(ErrUsernameRequired)
- }
-
- // Populate either PasswordCredentials or APIKeyCredentials
- if auth.Password != "" {
- if auth.APIKey != "" {
- return createErr(ErrPasswordOrAPIKey)
- }
-
- // Username + Password
- request.Auth.PasswordCredentials = &passwordCredentials{
- Username: auth.Username,
- Password: auth.Password,
- }
- } else if auth.APIKey != "" {
- // API key authentication.
- request.Auth.APIKeyCredentials = &apiKeyCredentials{
- Username: auth.Username,
- APIKey: auth.APIKey,
- }
- } else {
- return createErr(ErrPasswordOrAPIKey)
- }
-
- // Populate the TenantName or TenantID, if provided.
- request.Auth.TenantID = auth.TenantID
- request.Auth.TenantName = auth.TenantName
-
var result CreateResult
- _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ _, result.Err = perigee.Request("POST", CreateURL(client), perigee.Options{
ReqBody: &request,
Results: &result.Resp,
OkCodes: []int{200, 203},
diff --git a/openstack/identity/v2/tokens/requests_test.go b/openstack/identity/v2/tokens/requests_test.go
index 0e6269f..2f02825 100644
--- a/openstack/identity/v2/tokens/requests_test.go
+++ b/openstack/identity/v2/tokens/requests_test.go
@@ -1,153 +1,37 @@
package tokens
import (
- "fmt"
- "net/http"
"testing"
- "time"
"github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
)
-var expectedToken = &Token{
- ID: "aaaabbbbccccdddd",
- ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
- Tenant: tenants.Tenant{
- ID: "fc394f2ab2df4114bde39905f800dc57",
- Name: "test",
- Description: "There are many tenants. This one is yours.",
- Enabled: true,
- },
-}
-
-var expectedServiceCatalog = &ServiceCatalog{
- Entries: []CatalogEntry{
- CatalogEntry{
- Name: "inscrutablewalrus",
- Type: "something",
- Endpoints: []Endpoint{
- Endpoint{
- PublicURL: "http://something0:1234/v2/",
- Region: "region0",
- },
- Endpoint{
- PublicURL: "http://something1:1234/v2/",
- Region: "region1",
- },
- },
- },
- CatalogEntry{
- Name: "arbitrarypenguin",
- Type: "else",
- Endpoints: []Endpoint{
- Endpoint{
- PublicURL: "http://else0:4321/v3/",
- Region: "region0",
- },
- },
- },
- },
-}
-
func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) CreateResult {
th.SetupHTTP()
defer th.TeardownHTTP()
+ HandleTokenPost(t, requestJSON)
- client := gophercloud.ServiceClient{Endpoint: th.Endpoint()}
-
- th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "POST")
- th.TestHeader(t, r, "Content-Type", "application/json")
- th.TestHeader(t, r, "Accept", "application/json")
- th.TestJSONRequest(t, r, requestJSON)
-
- w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, `
-{
- "access": {
- "token": {
- "issued_at": "2014-01-30T15:30:58.000000Z",
- "expires": "2014-01-31T15:30:58Z",
- "id": "aaaabbbbccccdddd",
- "tenant": {
- "description": "There are many tenants. This one is yours.",
- "enabled": true,
- "id": "fc394f2ab2df4114bde39905f800dc57",
- "name": "test"
- }
- },
- "serviceCatalog": [
- {
- "endpoints": [
- {
- "publicURL": "http://something0:1234/v2/",
- "region": "region0"
- },
- {
- "publicURL": "http://something1:1234/v2/",
- "region": "region1"
- }
- ],
- "type": "something",
- "name": "inscrutablewalrus"
- },
- {
- "endpoints": [
- {
- "publicURL": "http://else0:4321/v3/",
- "region": "region0"
- }
- ],
- "type": "else",
- "name": "arbitrarypenguin"
- }
- ]
- }
-}
- `)
- })
-
- return Create(&client, options)
+ return Create(client.ServiceClient(), AuthOptions{options})
}
func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) {
th.SetupHTTP()
defer th.TeardownHTTP()
+ HandleTokenPost(t, "")
- client := gophercloud.ServiceClient{Endpoint: th.Endpoint()}
-
- th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "POST")
- th.TestHeader(t, r, "Content-Type", "application/json")
- th.TestHeader(t, r, "Accept", "application/json")
-
- w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, `{}`)
- })
-
- actualErr := Create(&client, options).Err
+ actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err
th.CheckEquals(t, expectedErr, actualErr)
}
-func isSuccessful(t *testing.T, result CreateResult) {
- token, err := result.ExtractToken()
- th.AssertNoErr(t, err)
- th.CheckDeepEquals(t, expectedToken, token)
-
- serviceCatalog, err := result.ExtractServiceCatalog()
- th.AssertNoErr(t, err)
- th.CheckDeepEquals(t, expectedServiceCatalog, serviceCatalog)
-}
-
func TestCreateWithPassword(t *testing.T) {
options := gophercloud.AuthOptions{
Username: "me",
Password: "swordfish",
}
- isSuccessful(t, tokenPost(t, options, `
+ IsSuccessful(t, tokenPost(t, options, `
{
"auth": {
"passwordCredentials": {
@@ -159,24 +43,6 @@
`))
}
-func TestCreateTokenWithAPIKey(t *testing.T) {
- options := gophercloud.AuthOptions{
- Username: "me",
- APIKey: "1234567890abcdef",
- }
-
- isSuccessful(t, tokenPost(t, options, `
- {
- "auth": {
- "RAX-KSKEY:apiKeyCredentials": {
- "username": "me",
- "apiKey": "1234567890abcdef"
- }
- }
- }
- `))
-}
-
func TestCreateTokenWithTenantID(t *testing.T) {
options := gophercloud.AuthOptions{
Username: "me",
@@ -184,7 +50,7 @@
TenantID: "fc394f2ab2df4114bde39905f800dc57",
}
- isSuccessful(t, tokenPost(t, options, `
+ IsSuccessful(t, tokenPost(t, options, `
{
"auth": {
"tenantId": "fc394f2ab2df4114bde39905f800dc57",
@@ -204,7 +70,7 @@
TenantName: "demo",
}
- isSuccessful(t, tokenPost(t, options, `
+ IsSuccessful(t, tokenPost(t, options, `
{
"auth": {
"tenantName": "demo",
@@ -223,15 +89,27 @@
UserID: "1234",
Password: "thing",
}
+
tokenPostErr(t, options, ErrUserIDProvided)
}
+func TestProhibitAPIKey(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ APIKey: "123412341234",
+ }
+
+ tokenPostErr(t, options, ErrAPIKeyProvided)
+}
+
func TestProhibitDomainID(t *testing.T) {
options := gophercloud.AuthOptions{
Username: "me",
Password: "thing",
DomainID: "1234",
}
+
tokenPostErr(t, options, ErrDomainIDProvided)
}
@@ -241,6 +119,7 @@
Password: "thing",
DomainName: "wat",
}
+
tokenPostErr(t, options, ErrDomainNameProvided)
}
@@ -248,21 +127,14 @@
options := gophercloud.AuthOptions{
Password: "thing",
}
+
tokenPostErr(t, options, ErrUsernameRequired)
}
-func TestProhibitBothPasswordAndAPIKey(t *testing.T) {
+func TestRequirePassword(t *testing.T) {
options := gophercloud.AuthOptions{
Username: "me",
- Password: "thing",
- APIKey: "123412341234",
}
- tokenPostErr(t, options, ErrPasswordOrAPIKey)
-}
-func TestRequirePasswordOrAPIKey(t *testing.T) {
- options := gophercloud.AuthOptions{
- Username: "me",
- }
- tokenPostErr(t, options, ErrPasswordOrAPIKey)
+ tokenPostErr(t, options, ErrPasswordRequired)
}
diff --git a/openstack/identity/v2/tokens/urls.go b/openstack/identity/v2/tokens/urls.go
index 86d19f2..cd4c696 100644
--- a/openstack/identity/v2/tokens/urls.go
+++ b/openstack/identity/v2/tokens/urls.go
@@ -2,6 +2,7 @@
import "github.com/rackspace/gophercloud"
-func listURL(client *gophercloud.ServiceClient) string {
+// CreateURL generates the URL used to create new Tokens.
+func CreateURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("tokens")
}
diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go
index 2dd2357..d1c2472 100644
--- a/openstack/identity/v3/endpoints/results.go
+++ b/openstack/identity/v3/endpoints/results.go
@@ -1,8 +1,6 @@
package endpoints
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -24,11 +22,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Endpoint: %v", err)
- }
- return &res.Endpoint, nil
+ return &res.Endpoint, err
}
// CreateResult is the deferred result of a Create call.
@@ -77,8 +72,6 @@
}
err := mapstructure.Decode(page.(EndpointPage).Body, &response)
- if err != nil {
- return nil, err
- }
- return response.Endpoints, nil
+
+ return response.Endpoints, err
}
diff --git a/openstack/identity/v3/services/results.go b/openstack/identity/v3/services/results.go
index b4e7bd2..e4e068b 100644
--- a/openstack/identity/v3/services/results.go
+++ b/openstack/identity/v3/services/results.go
@@ -1,8 +1,6 @@
package services
import (
- "fmt"
-
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -25,11 +23,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Service: %v", err)
- }
- return &res.Service, nil
+ return &res.Service, err
}
// CreateResult is the deferred result of a Create call.
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index e96da51..1be98cb 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -40,11 +40,8 @@
// Attempt to parse the timestamp.
token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
- if err != nil {
- return nil, err
- }
- return &token, nil
+ return &token, err
}
// CreateResult is the deferred response from a Create call.
diff --git a/openstack/networking/v2/apiversions/requests_test.go b/openstack/networking/v2/apiversions/requests_test.go
index c49b509..d35af9f 100644
--- a/openstack/networking/v2/apiversions/requests_test.go
+++ b/openstack/networking/v2/apiversions/requests_test.go
@@ -65,6 +65,22 @@
}
}
+func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ if _, err := ExtractAPIVersions(page); err == nil {
+ t.Fatalf("Expected error, got nil")
+ }
+ return true, nil
+ })
+}
+
func TestAPIInfo(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
@@ -148,3 +164,19 @@
t.Errorf("Expected 1 page, got %d", count)
}
}
+
+func TestNonJSONCannotBeExtractedIntoAPIVersionResources(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
+ if _, err := ExtractVersionResources(page); err == nil {
+ t.Fatalf("Expected error, got nil")
+ }
+ return true, nil
+ })
+}
diff --git a/openstack/networking/v2/apiversions/results.go b/openstack/networking/v2/apiversions/results.go
index e13998d..9715934 100644
--- a/openstack/networking/v2/apiversions/results.go
+++ b/openstack/networking/v2/apiversions/results.go
@@ -35,11 +35,8 @@
}
err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Versions, nil
+ return resp.Versions, err
}
// APIVersionResource represents a generic API resource. It contains the name
@@ -75,9 +72,6 @@
}
err := mapstructure.Decode(page.(APIVersionResourcePage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.APIVersionResources, nil
+ return resp.APIVersionResources, err
}
diff --git a/openstack/networking/v2/apiversions/urls.go b/openstack/networking/v2/apiversions/urls.go
index c43f991..58aa2b6 100644
--- a/openstack/networking/v2/apiversions/urls.go
+++ b/openstack/networking/v2/apiversions/urls.go
@@ -7,9 +7,9 @@
)
func apiVersionsURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL("")
+ return c.Endpoint
}
func apiInfoURL(c *gophercloud.ServiceClient, version string) string {
- return c.ServiceURL(strings.TrimRight(version, "/") + "/")
+ return c.Endpoint + strings.TrimRight(version, "/") + "/"
}
diff --git a/openstack/networking/v2/common/common_tests.go b/openstack/networking/v2/common/common_tests.go
new file mode 100644
index 0000000..4160351
--- /dev/null
+++ b/openstack/networking/v2/common/common_tests.go
@@ -0,0 +1,14 @@
+package common
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const TokenID = client.TokenID
+
+func ServiceClient() *gophercloud.ServiceClient {
+ sc := client.ServiceClient()
+ sc.ResourceBase = sc.Endpoint + "v2.0/"
+ return sc
+}
diff --git a/openstack/networking/v2/extensions/delegate_test.go b/openstack/networking/v2/extensions/delegate_test.go
index 8de8906..3d2ac78 100755
--- a/openstack/networking/v2/extensions/delegate_test.go
+++ b/openstack/networking/v2/extensions/delegate_test.go
@@ -6,16 +6,16 @@
"testing"
common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/extensions", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -73,7 +73,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -92,14 +92,14 @@
}
}
`)
-
- ext, err := Get(fake.ServiceClient(), "agent").Extract()
- th.AssertNoErr(t, err)
-
- th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
- th.AssertEquals(t, ext.Name, "agent")
- th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
- th.AssertEquals(t, ext.Alias, "agent")
- th.AssertEquals(t, ext.Description, "The agent management extension.")
})
+
+ ext, err := Get(fake.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
+ th.AssertEquals(t, ext.Name, "agent")
+ th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
+ th.AssertEquals(t, ext.Alias, "agent")
+ th.AssertEquals(t, ext.Description, "The agent management extension.")
}
diff --git a/openstack/networking/v2/extensions/external/requests.go b/openstack/networking/v2/extensions/external/requests.go
index afdd428..2f04593 100644
--- a/openstack/networking/v2/extensions/external/requests.go
+++ b/openstack/networking/v2/extensions/external/requests.go
@@ -24,12 +24,15 @@
}
// ToNetworkCreateMap casts a CreateOpts struct to a map.
-func (o CreateOpts) ToNetworkCreateMap() map[string]map[string]interface{} {
- outer := o.Parent.ToNetworkCreateMap()
+func (o CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+ outer, err := o.Parent.ToNetworkCreateMap()
+ if err != nil {
+ return nil, err
+ }
- outer["network"]["router:external"] = o.External
+ outer["network"].(map[string]interface{})["router:external"] = o.External
- return outer
+ return outer, nil
}
// UpdateOpts is the structure used when updating existing external network
@@ -41,10 +44,13 @@
}
// ToNetworkUpdateMap casts an UpdateOpts struct to a map.
-func (o UpdateOpts) ToNetworkUpdateMap() map[string]map[string]interface{} {
- outer := o.Parent.ToNetworkUpdateMap()
+func (o UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+ outer, err := o.Parent.ToNetworkUpdateMap()
+ if err != nil {
+ return nil, err
+ }
- outer["network"]["router:external"] = o.External
+ outer["network"].(map[string]interface{})["router:external"] = o.External
- return outer
+ return outer, nil
}
diff --git a/openstack/networking/v2/extensions/external/results.go b/openstack/networking/v2/extensions/external/results.go
index 4cd2133..47f7258 100644
--- a/openstack/networking/v2/extensions/external/results.go
+++ b/openstack/networking/v2/extensions/external/results.go
@@ -1,8 +1,6 @@
package external
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
"github.com/rackspace/gophercloud/pagination"
@@ -37,52 +35,36 @@
External bool `mapstructure:"router:external" json:"router:external"`
}
-// ExtractGet decorates a GetResult struct returned from a networks.Get()
-// function with extended attributes.
-func ExtractGet(r networks.GetResult) (*NetworkExternal, error) {
- if r.Err != nil {
- return nil, r.Err
+func commonExtract(e error, response map[string]interface{}) (*NetworkExternal, error) {
+ if e != nil {
+ return nil, e
}
+
var res struct {
Network *NetworkExternal `json:"network"`
}
- err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
- }
- return res.Network, nil
+
+ err := mapstructure.Decode(response, &res)
+
+ return res.Network, err
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExternal, error) {
+ return commonExtract(r.Err, r.Resp)
}
// ExtractCreate decorates a CreateResult struct returned from a networks.Create()
// function with extended attributes.
func ExtractCreate(r networks.CreateResult) (*NetworkExternal, error) {
- if r.Err != nil {
- return nil, r.Err
- }
- var res struct {
- Network *NetworkExternal `json:"network"`
- }
- err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
- }
- return res.Network, nil
+ return commonExtract(r.Err, r.Resp)
}
// ExtractUpdate decorates a UpdateResult struct returned from a
// networks.Update() function with extended attributes.
func ExtractUpdate(r networks.UpdateResult) (*NetworkExternal, error) {
- if r.Err != nil {
- return nil, r.Err
- }
- var res struct {
- Network *NetworkExternal `json:"network"`
- }
- err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
- }
- return res.Network, nil
+ return commonExtract(r.Err, r.Resp)
}
// ExtractList accepts a Page struct, specifically a NetworkPage struct, and
@@ -94,9 +76,6 @@
}
err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Networks, nil
+ return resp.Networks, err
}
diff --git a/openstack/networking/v2/extensions/external/results_test.go b/openstack/networking/v2/extensions/external/results_test.go
index 6bed126..916cd2c 100644
--- a/openstack/networking/v2/extensions/external/results_test.go
+++ b/openstack/networking/v2/extensions/external/results_test.go
@@ -1,6 +1,7 @@
package external
import (
+ "errors"
"fmt"
"net/http"
"testing"
@@ -228,3 +229,26 @@
th.AssertNoErr(t, err)
th.AssertEquals(t, true, n.External)
}
+
+func TestExtractFnsReturnsErrWhenResultContainsErr(t *testing.T) {
+ gr := networks.GetResult{}
+ gr.Err = errors.New("")
+
+ if _, err := ExtractGet(gr); err == nil {
+ t.Fatalf("Expected error, got one")
+ }
+
+ ur := networks.UpdateResult{}
+ ur.Err = errors.New("")
+
+ if _, err := ExtractUpdate(ur); err == nil {
+ t.Fatalf("Expected error, got one")
+ }
+
+ cr := networks.CreateResult{}
+ cr.Err = errors.New("")
+
+ if _, err := ExtractCreate(cr); err == nil {
+ t.Fatalf("Expected error, got one")
+ }
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go b/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
index b9153fc..19614be 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
@@ -5,9 +5,9 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestList(t *testing.T) {
@@ -90,6 +90,34 @@
}
}
+func TestInvalidNextPageURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `{"floatingips": [{}], "floatingips_links": {}}`)
+ })
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ ExtractFloatingIPs(page)
+ return true, nil
+ })
+}
+
+func TestRequiredFieldsForCreate(t *testing.T) {
+ res1 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: ""})
+ if res1.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ res2 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: "foo", PortID: ""})
+ if res2.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
func TestCreate(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/results.go b/openstack/networking/v2/extensions/layer3/floatingips/results.go
index 4857c92..43892f0 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/results.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/results.go
@@ -89,12 +89,8 @@
// the end of a page and the pager seeks to traverse over a new one. In order
// to do this, it needs to construct the next page's URL.
func (p FloatingIPPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"floatingips_links"`
+ Links []gophercloud.Link `mapstructure:"floatingips_links"`
}
var r resp
@@ -103,17 +99,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a NetworkPage struct is empty.
@@ -134,9 +120,6 @@
}
err := mapstructure.Decode(page.(FloatingIPPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.FloatingIPs, nil
+ return resp.FloatingIPs, err
}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/openstack/networking/v2/extensions/layer3/floatingips/urls.go
index dbe3f9f..355f20d 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/urls.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/urls.go
@@ -2,15 +2,12 @@
import "github.com/rackspace/gophercloud"
-const (
- version = "v2.0"
- resourcePath = "floatingips"
-)
+const resourcePath = "floatingips"
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, resourcePath)
+ return c.ServiceURL(resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, resourcePath, id)
+ return c.ServiceURL(resourcePath, id)
}
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests_test.go b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
index 56a0d74..c34264d 100755
--- a/openstack/networking/v2/extensions/layer3/routers/requests_test.go
+++ b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
@@ -5,9 +5,9 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestURLs(t *testing.T) {
@@ -288,6 +288,17 @@
th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID)
}
+func TestAddInterfaceRequiredOpts(t *testing.T) {
+ _, err := AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{}).Extract()
+ if err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ _, err = AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{SubnetID: "bar", PortID: "baz"}).Extract()
+ if err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
func TestRemoveInterface(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/networking/v2/extensions/layer3/routers/results.go b/openstack/networking/v2/extensions/layer3/routers/results.go
index d14578c..eae647f 100755
--- a/openstack/networking/v2/extensions/layer3/routers/results.go
+++ b/openstack/networking/v2/extensions/layer3/routers/results.go
@@ -1,8 +1,6 @@
package routers
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -53,12 +51,8 @@
// the end of a page and the pager seeks to traverse over a new one. In order
// to do this, it needs to construct the next page's URL.
func (p RouterPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"routers_links"`
+ Links []gophercloud.Link `mapstructure:"routers_links"`
}
var r resp
@@ -67,17 +61,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a RouterPage struct is empty.
@@ -98,11 +82,8 @@
}
err := mapstructure.Decode(page.(RouterPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Routers, nil
+ return resp.Routers, err
}
type commonResult struct {
@@ -120,11 +101,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron router: %v", err)
- }
- return res.Router, nil
+ return res.Router, err
}
// CreateResult represents the result of a create operation.
@@ -176,9 +154,6 @@
var res *InterfaceInfo
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron router interface: %v", err)
- }
- return res, nil
+ return res, err
}
diff --git a/openstack/networking/v2/extensions/layer3/routers/urls.go b/openstack/networking/v2/extensions/layer3/routers/urls.go
index 512c8a4..bc22c2a 100644
--- a/openstack/networking/v2/extensions/layer3/routers/urls.go
+++ b/openstack/networking/v2/extensions/layer3/routers/urls.go
@@ -2,23 +2,20 @@
import "github.com/rackspace/gophercloud"
-const (
- version = "v2.0"
- resourcePath = "routers"
-)
+const resourcePath = "routers"
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, resourcePath)
+ return c.ServiceURL(resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, resourcePath, id)
+ return c.ServiceURL(resourcePath, id)
}
func addInterfaceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, resourcePath, id, "add_router_interface")
+ return c.ServiceURL(resourcePath, id, "add_router_interface")
}
func removeInterfaceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, resourcePath, id, "remove_router_interface")
+ return c.ServiceURL(resourcePath, id, "remove_router_interface")
}
diff --git a/openstack/networking/v2/extensions/lbaas/members/requests_test.go b/openstack/networking/v2/extensions/lbaas/members/requests_test.go
index cae3524..dc1ece3 100644
--- a/openstack/networking/v2/extensions/lbaas/members/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas/members/requests_test.go
@@ -5,9 +5,9 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestURLs(t *testing.T) {
diff --git a/openstack/networking/v2/extensions/lbaas/members/results.go b/openstack/networking/v2/extensions/lbaas/members/results.go
index 9466fee..b006551 100644
--- a/openstack/networking/v2/extensions/lbaas/members/results.go
+++ b/openstack/networking/v2/extensions/lbaas/members/results.go
@@ -1,8 +1,6 @@
package members
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -46,12 +44,8 @@
// the end of a page and the pager seeks to traverse over a new one. In order
// to do this, it needs to construct the next page's URL.
func (p MemberPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"members_links"`
+ Links []gophercloud.Link `mapstructure:"members_links"`
}
var r resp
@@ -60,17 +54,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a MemberPage struct is empty.
@@ -113,11 +97,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron member: %v", err)
- }
- return res.Member, nil
+ return res.Member, err
}
// CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/lbaas/members/urls.go b/openstack/networking/v2/extensions/lbaas/members/urls.go
index 9d5ecec..94b57e4 100644
--- a/openstack/networking/v2/extensions/lbaas/members/urls.go
+++ b/openstack/networking/v2/extensions/lbaas/members/urls.go
@@ -3,15 +3,14 @@
import "github.com/rackspace/gophercloud"
const (
- version = "v2.0"
rootPath = "lb"
resourcePath = "members"
)
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, rootPath, resourcePath)
+ return c.ServiceURL(rootPath, resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, rootPath, resourcePath, id)
+ return c.ServiceURL(rootPath, resourcePath, id)
}
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go b/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
index 68a3288..5c5a1d2 100644
--- a/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
@@ -5,9 +5,9 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestURLs(t *testing.T) {
@@ -158,6 +158,17 @@
th.AssertNoErr(t, err)
}
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Type: TypeHTTP})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
func TestGet(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/results.go b/openstack/networking/v2/extensions/lbaas/monitors/results.go
index a41f20e..656ab1d 100644
--- a/openstack/networking/v2/extensions/lbaas/monitors/results.go
+++ b/openstack/networking/v2/extensions/lbaas/monitors/results.go
@@ -1,8 +1,6 @@
package monitors
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -74,12 +72,8 @@
// the end of a page and the pager seeks to traverse over a new one. In order
// to do this, it needs to construct the next page's URL.
func (p MonitorPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"health_monitors_links"`
+ Links []gophercloud.Link `mapstructure:"health_monitors_links"`
}
var r resp
@@ -88,17 +82,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a PoolPage struct is empty.
@@ -119,11 +103,8 @@
}
err := mapstructure.Decode(page.(MonitorPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Monitors, nil
+ return resp.Monitors, err
}
type commonResult struct {
@@ -141,11 +122,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron monitor: %v", err)
- }
- return res.Monitor, nil
+ return res.Monitor, err
}
// CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/urls.go b/openstack/networking/v2/extensions/lbaas/monitors/urls.go
index e4b2afc..46e84bb 100644
--- a/openstack/networking/v2/extensions/lbaas/monitors/urls.go
+++ b/openstack/networking/v2/extensions/lbaas/monitors/urls.go
@@ -3,15 +3,14 @@
import "github.com/rackspace/gophercloud"
const (
- version = "v2.0"
rootPath = "lb"
resourcePath = "health_monitors"
)
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, rootPath, resourcePath)
+ return c.ServiceURL(rootPath, resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, rootPath, resourcePath, id)
+ return c.ServiceURL(rootPath, resourcePath, id)
}
diff --git a/openstack/networking/v2/extensions/lbaas/pools/requests_test.go b/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
index 6af47a1..6da29a6 100644
--- a/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
@@ -5,9 +5,9 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestURLs(t *testing.T) {
diff --git a/openstack/networking/v2/extensions/lbaas/pools/results.go b/openstack/networking/v2/extensions/lbaas/pools/results.go
index 5b2adde..4233176 100644
--- a/openstack/networking/v2/extensions/lbaas/pools/results.go
+++ b/openstack/networking/v2/extensions/lbaas/pools/results.go
@@ -1,8 +1,6 @@
package pools
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -68,12 +66,8 @@
// the end of a page and the pager seeks to traverse over a new one. In order
// to do this, it needs to construct the next page's URL.
func (p PoolPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"pools_links"`
+ Links []gophercloud.Link `mapstructure:"pools_links"`
}
var r resp
@@ -82,17 +76,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a PoolPage struct is empty.
@@ -113,11 +97,8 @@
}
err := mapstructure.Decode(page.(PoolPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Pools, nil
+ return resp.Pools, err
}
type commonResult struct {
@@ -135,11 +116,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron pool: %v", err)
- }
- return res.Pool, nil
+ return res.Pool, err
}
// CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/lbaas/pools/urls.go b/openstack/networking/v2/extensions/lbaas/pools/urls.go
index 124ef5d..6cd15b0 100644
--- a/openstack/networking/v2/extensions/lbaas/pools/urls.go
+++ b/openstack/networking/v2/extensions/lbaas/pools/urls.go
@@ -3,24 +3,23 @@
import "github.com/rackspace/gophercloud"
const (
- version = "v2.0"
rootPath = "lb"
resourcePath = "pools"
monitorPath = "health_monitors"
)
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, rootPath, resourcePath)
+ return c.ServiceURL(rootPath, resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, rootPath, resourcePath, id)
+ return c.ServiceURL(rootPath, resourcePath, id)
}
func associateURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, rootPath, resourcePath, id, monitorPath)
+ return c.ServiceURL(rootPath, resourcePath, id, monitorPath)
}
func disassociateURL(c *gophercloud.ServiceClient, poolID, monitorID string) string {
- return c.ServiceURL(version, rootPath, resourcePath, poolID, monitorPath, monitorID)
+ return c.ServiceURL(rootPath, resourcePath, poolID, monitorPath, monitorID)
}
diff --git a/openstack/networking/v2/extensions/lbaas/vips/requests_test.go b/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
index f4e90c7..430f1a1 100644
--- a/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
@@ -5,9 +5,9 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestURLs(t *testing.T) {
@@ -139,7 +139,8 @@
"admin_state_up": true,
"subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
"pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
- "protocol_port": 80
+ "protocol_port": 80,
+ "session_persistence": {"type": "SOURCE_IP"}
}
}
`)
@@ -175,6 +176,7 @@
SubnetID: "8032909d-47a1-4715-90af-5153ffe39861",
PoolID: "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
ProtocolPort: 80,
+ Persistence: &SessionPersistence{Type: "SOURCE_IP"},
}
r, err := Create(fake.ServiceClient(), opts).Extract()
@@ -195,6 +197,29 @@
th.AssertEquals(t, "NewVip", r.Name)
}
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar", ProtocolPort: 80})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
func TestGet(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
@@ -254,7 +279,8 @@
th.TestJSONRequest(t, r, `
{
"vip": {
- "connection_limit": 1000
+ "connection_limit": 1000,
+ "session_persistence": {"type": "SOURCE_IP"}
}
}
`)
@@ -284,7 +310,10 @@
})
i1000 := 1000
- options := UpdateOpts{ConnLimit: &i1000}
+ options := UpdateOpts{
+ ConnLimit: &i1000,
+ Persistence: &SessionPersistence{Type: "SOURCE_IP"},
+ }
vip, err := Update(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304", options).Extract()
th.AssertNoErr(t, err)
diff --git a/openstack/networking/v2/extensions/lbaas/vips/results.go b/openstack/networking/v2/extensions/lbaas/vips/results.go
index 62efe09..5925adc 100644
--- a/openstack/networking/v2/extensions/lbaas/vips/results.go
+++ b/openstack/networking/v2/extensions/lbaas/vips/results.go
@@ -1,8 +1,6 @@
package vips
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -28,7 +26,7 @@
Type string `mapstructure:"type" json:"type"`
// Name of cookie if persistence mode is set appropriately
- CookieName string `mapstructure:"cookie_name" json:"cookie_name"`
+ CookieName string `mapstructure:"cookie_name" json:"cookie_name,omitempty"`
}
// VirtualIP is the primary load balancing configuration object that specifies
@@ -93,12 +91,8 @@
// the end of a page and the pager seeks to traverse over a new one. In order
// to do this, it needs to construct the next page's URL.
func (p VIPPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"vips_links"`
+ Links []gophercloud.Link `mapstructure:"vips_links"`
}
var r resp
@@ -107,17 +101,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a RouterPage struct is empty.
@@ -138,11 +122,8 @@
}
err := mapstructure.Decode(page.(VIPPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.VIPs, nil
+ return resp.VIPs, err
}
type commonResult struct {
@@ -160,11 +141,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron Virtual IP: %v", err)
- }
- return res.VirtualIP, nil
+ return res.VirtualIP, err
}
// CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/lbaas/vips/urls.go b/openstack/networking/v2/extensions/lbaas/vips/urls.go
index 570db6d..2b6f67e 100644
--- a/openstack/networking/v2/extensions/lbaas/vips/urls.go
+++ b/openstack/networking/v2/extensions/lbaas/vips/urls.go
@@ -3,15 +3,14 @@
import "github.com/rackspace/gophercloud"
const (
- version = "v2.0"
rootPath = "lb"
resourcePath = "vips"
)
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, rootPath, resourcePath)
+ return c.ServiceURL(rootPath, resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, rootPath, resourcePath, id)
+ return c.ServiceURL(rootPath, resourcePath, id)
}
diff --git a/openstack/networking/v2/extensions/provider/results.go b/openstack/networking/v2/extensions/provider/results.go
index a20b259..8fc21b4 100755
--- a/openstack/networking/v2/extensions/provider/results.go
+++ b/openstack/networking/v2/extensions/provider/results.go
@@ -1,8 +1,6 @@
package provider
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
"github.com/rackspace/gophercloud/pagination"
@@ -70,14 +68,14 @@
if r.Err != nil {
return nil, r.Err
}
+
var res struct {
Network *NetworkExtAttrs `json:"network"`
}
+
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
- }
- return res.Network, nil
+
+ return res.Network, err
}
// ExtractCreate decorates a CreateResult struct returned from a networks.Create()
@@ -86,14 +84,14 @@
if r.Err != nil {
return nil, r.Err
}
+
var res struct {
Network *NetworkExtAttrs `json:"network"`
}
+
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
- }
- return res.Network, nil
+
+ return res.Network, err
}
// ExtractUpdate decorates a UpdateResult struct returned from a
@@ -102,14 +100,14 @@
if r.Err != nil {
return nil, r.Err
}
+
var res struct {
Network *NetworkExtAttrs `json:"network"`
}
+
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
- }
- return res.Network, nil
+
+ return res.Network, err
}
// ExtractList accepts a Page struct, specifically a NetworkPage struct, and
@@ -121,9 +119,6 @@
}
err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Networks, nil
+ return resp.Networks, err
}
diff --git a/openstack/networking/v2/extensions/provider/results_test.go b/openstack/networking/v2/extensions/provider/results_test.go
index 74951d9..9801b2e 100644
--- a/openstack/networking/v2/extensions/provider/results_test.go
+++ b/openstack/networking/v2/extensions/provider/results_test.go
@@ -5,17 +5,17 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -109,7 +109,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -150,7 +150,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -202,7 +202,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
diff --git a/openstack/networking/v2/extensions/security/groups/requests_test.go b/openstack/networking/v2/extensions/security/groups/requests_test.go
index 857bdc6..5f074c7 100644
--- a/openstack/networking/v2/extensions/security/groups/requests_test.go
+++ b/openstack/networking/v2/extensions/security/groups/requests_test.go
@@ -5,10 +5,10 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestURLs(t *testing.T) {
diff --git a/openstack/networking/v2/extensions/security/groups/results.go b/openstack/networking/v2/extensions/security/groups/results.go
index 6db613e..617d690 100644
--- a/openstack/networking/v2/extensions/security/groups/results.go
+++ b/openstack/networking/v2/extensions/security/groups/results.go
@@ -1,8 +1,6 @@
package groups
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
@@ -40,12 +38,8 @@
// reached the end of a page and the pager seeks to traverse over a new one. In
// order to do this, it needs to construct the next page's URL.
func (p SecGroupPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"security_groups_links"`
+ Links []gophercloud.Link `mapstructure:"security_groups_links"`
}
var r resp
@@ -54,17 +48,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a SecGroupPage struct is empty.
@@ -85,11 +69,8 @@
}
err := mapstructure.Decode(page.(SecGroupPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.SecGroups, nil
+ return resp.SecGroups, err
}
type commonResult struct {
@@ -107,11 +88,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron secgroup: %v", err)
- }
- return res.SecGroup, nil
+ return res.SecGroup, err
}
// CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/security/groups/urls.go b/openstack/networking/v2/extensions/security/groups/urls.go
index 2f2bbdd..84f7324 100644
--- a/openstack/networking/v2/extensions/security/groups/urls.go
+++ b/openstack/networking/v2/extensions/security/groups/urls.go
@@ -2,15 +2,12 @@
import "github.com/rackspace/gophercloud"
-const (
- version = "v2.0"
- rootPath = "security-groups"
-)
+const rootPath = "security-groups"
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, rootPath)
+ return c.ServiceURL(rootPath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, rootPath, id)
+ return c.ServiceURL(rootPath, id)
}
diff --git a/openstack/networking/v2/extensions/security/rules/requests_test.go b/openstack/networking/v2/extensions/security/rules/requests_test.go
index a2c7fbb..b5afef3 100644
--- a/openstack/networking/v2/extensions/security/rules/requests_test.go
+++ b/openstack/networking/v2/extensions/security/rules/requests_test.go
@@ -5,9 +5,9 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestURLs(t *testing.T) {
@@ -165,6 +165,25 @@
th.AssertNoErr(t, err)
}
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{Direction: "something"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: "something"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4, SecGroupID: "something", Protocol: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
func TestGet(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go
index 13d5c7d..ca8435e 100644
--- a/openstack/networking/v2/extensions/security/rules/results.go
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -1,8 +1,6 @@
package rules
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -65,12 +63,8 @@
// reached the end of a page and the pager seeks to traverse over a new one. In
// order to do this, it needs to construct the next page's URL.
func (p SecGroupRulePage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"security_group_rules_links"`
+ Links []gophercloud.Link `mapstructure:"security_group_rules_links"`
}
var r resp
@@ -79,17 +73,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a SecGroupRulePage struct is empty.
@@ -110,11 +94,8 @@
}
err := mapstructure.Decode(page.(SecGroupRulePage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.SecGroupRules, nil
+ return resp.SecGroupRules, err
}
type commonResult struct {
@@ -132,11 +113,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron SecGroupRule: %v", err)
- }
- return res.SecGroupRule, nil
+ return res.SecGroupRule, err
}
// CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/security/rules/urls.go b/openstack/networking/v2/extensions/security/rules/urls.go
index 2ffbf37..8e2b2bb 100644
--- a/openstack/networking/v2/extensions/security/rules/urls.go
+++ b/openstack/networking/v2/extensions/security/rules/urls.go
@@ -2,15 +2,12 @@
import "github.com/rackspace/gophercloud"
-const (
- version = "v2.0"
- rootPath = "security-group-rules"
-)
+const rootPath = "security-group-rules"
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, rootPath)
+ return c.ServiceURL(rootPath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, rootPath, id)
+ return c.ServiceURL(rootPath, id)
}
diff --git a/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go
index 81b1545..8cfe4e0 100644
--- a/openstack/networking/v2/networks/requests.go
+++ b/openstack/networking/v2/networks/requests.go
@@ -1,9 +1,23 @@
package networks
import (
- "github.com/racker/perigee"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
)
type networkOpts struct {
@@ -13,6 +27,12 @@
TenantID string
}
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToNetworkListQuery() (string, error)
+}
+
// ListOpts allows the filtering and sorting of paginated collections through
// the API. Filtering is achieved by passing in struct field values that map to
// the network attributes you want to see returned. SortKey allows you to sort
@@ -31,17 +51,29 @@
SortDir string `q:"sort_dir"`
}
+// ToNetworkListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToNetworkListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
// List returns a Pager which allows you to iterate over a collection of
// networks. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
- // Build query parameters
- q, err := gophercloud.BuildQueryString(&opts)
- if err != nil {
- return pagination.Pager{Err: err}
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToNetworkListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
}
- u := listURL(c) + q.String()
- return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+
+ return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
return NetworkPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
})
}
@@ -62,7 +94,7 @@
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type CreateOptsBuilder interface {
- ToNetworkCreateMap() map[string]map[string]interface{}
+ ToNetworkCreateMap() (map[string]interface{}, error)
}
// CreateOpts is the common options struct used in this package's Create
@@ -70,26 +102,23 @@
type CreateOpts networkOpts
// ToNetworkCreateMap casts a CreateOpts struct to a map.
-func (o CreateOpts) ToNetworkCreateMap() map[string]map[string]interface{} {
- inner := make(map[string]interface{})
+func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+ n := make(map[string]interface{})
- if o.AdminStateUp != nil {
- inner["admin_state_up"] = &o.AdminStateUp
+ if opts.AdminStateUp != nil {
+ n["admin_state_up"] = &opts.AdminStateUp
}
- if o.Name != "" {
- inner["name"] = o.Name
+ if opts.Name != "" {
+ n["name"] = opts.Name
}
- if o.Shared != nil {
- inner["shared"] = &o.Shared
+ if opts.Shared != nil {
+ n["shared"] = &opts.Shared
}
- if o.TenantID != "" {
- inner["tenant_id"] = o.TenantID
+ if opts.TenantID != "" {
+ n["tenant_id"] = opts.TenantID
}
- outer := make(map[string]map[string]interface{})
- outer["network"] = inner
-
- return outer
+ return map[string]interface{}{"network": n}, nil
}
// Create accepts a CreateOpts struct and creates a new network using the values
@@ -102,7 +131,11 @@
func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
- reqBody := opts.ToNetworkCreateMap()
+ reqBody, err := opts.ToNetworkCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
// Send request to API
_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
@@ -119,7 +152,7 @@
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type UpdateOptsBuilder interface {
- ToNetworkUpdateMap() map[string]map[string]interface{}
+ ToNetworkUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts is the common options struct used in this package's Update
@@ -127,23 +160,20 @@
type UpdateOpts networkOpts
// ToNetworkUpdateMap casts a UpdateOpts struct to a map.
-func (o UpdateOpts) ToNetworkUpdateMap() map[string]map[string]interface{} {
- inner := make(map[string]interface{})
+func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+ n := make(map[string]interface{})
- if o.AdminStateUp != nil {
- inner["admin_state_up"] = &o.AdminStateUp
+ if opts.AdminStateUp != nil {
+ n["admin_state_up"] = &opts.AdminStateUp
}
- if o.Name != "" {
- inner["name"] = o.Name
+ if opts.Name != "" {
+ n["name"] = opts.Name
}
- if o.Shared != nil {
- inner["shared"] = &o.Shared
+ if opts.Shared != nil {
+ n["shared"] = &opts.Shared
}
- outer := make(map[string]map[string]interface{})
- outer["network"] = inner
-
- return outer
+ return map[string]interface{}{"network": n}, nil
}
// Update accepts a UpdateOpts struct and updates an existing network using the
@@ -151,7 +181,11 @@
func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
- reqBody := opts.ToNetworkUpdateMap()
+ reqBody, err := opts.ToNetworkUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
// Send request to API
_, res.Err = perigee.Request("PUT", getURL(c, networkID), perigee.Options{
diff --git a/openstack/networking/v2/networks/requests_test.go b/openstack/networking/v2/networks/requests_test.go
index 6b22acd..a263b7b 100644
--- a/openstack/networking/v2/networks/requests_test.go
+++ b/openstack/networking/v2/networks/requests_test.go
@@ -5,16 +5,16 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -97,7 +97,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -137,7 +137,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -187,7 +187,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -216,7 +216,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -264,7 +264,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
diff --git a/openstack/networking/v2/networks/results.go b/openstack/networking/v2/networks/results.go
index 2dbd55f..e605fcf 100644
--- a/openstack/networking/v2/networks/results.go
+++ b/openstack/networking/v2/networks/results.go
@@ -1,8 +1,6 @@
package networks
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -23,11 +21,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
- }
- return res.Network, nil
+ return res.Network, err
}
// CreateResult represents the result of a create operation.
@@ -83,12 +78,8 @@
// the end of a page and the pager seeks to traverse over a new one. In order
// to do this, it needs to construct the next page's URL.
func (p NetworkPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"networks_links"`
+ Links []gophercloud.Link `mapstructure:"networks_links"`
}
var r resp
@@ -97,17 +88,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a NetworkPage struct is empty.
@@ -128,9 +109,6 @@
}
err := mapstructure.Decode(page.(NetworkPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Networks, nil
+ return resp.Networks, err
}
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 6928d8f..c846de9 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -1,11 +1,31 @@
package ports
import (
- "github.com/racker/perigee"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
)
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToPortListQuery() (string, error)
+}
+
// ListOpts allows the filtering and sorting of paginated collections through
// the API. Filtering is achieved by passing in struct field values that map to
// the port attributes you want to see returned. SortKey allows you to sort
@@ -27,6 +47,15 @@
SortDir string `q:"sort_dir"`
}
+// ToPortListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPortListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
// List returns a Pager which allows you to iterate over a collection of
// ports. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
@@ -34,15 +63,17 @@
// Default policy settings return only those ports that are owned by the tenant
// who submits the request, unless the request is submitted by an user with
// administrative rights.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
- // Build query parameters
- q, err := gophercloud.BuildQueryString(&opts)
- if err != nil {
- return pagination.Pager{Err: err}
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToPortListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
}
- u := listURL(c) + q.String()
- return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
return PortPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
})
}
@@ -58,6 +89,14 @@
return res
}
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToPortCreateMap() (map[string]interface{}, error)
+}
+
// CreateOpts represents the attributes used when creating a new port.
type CreateOpts struct {
NetworkID string
@@ -71,51 +110,54 @@
SecurityGroups []string
}
+// ToPortCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
+ p := make(map[string]interface{})
+
+ if opts.NetworkID == "" {
+ return nil, errNetworkIDRequired
+ }
+ p["network_id"] = opts.NetworkID
+
+ if opts.DeviceID != "" {
+ p["device_id"] = opts.DeviceID
+ }
+ if opts.DeviceOwner != "" {
+ p["device_owner"] = opts.DeviceOwner
+ }
+ if opts.FixedIPs != nil {
+ p["fixed_ips"] = opts.FixedIPs
+ }
+ if opts.SecurityGroups != nil {
+ p["security_groups"] = opts.SecurityGroups
+ }
+ if opts.TenantID != "" {
+ p["tenant_id"] = opts.TenantID
+ }
+ if opts.AdminStateUp != nil {
+ p["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ p["name"] = opts.Name
+ }
+ if opts.MACAddress != "" {
+ p["mac_address"] = opts.MACAddress
+ }
+
+ return map[string]interface{}{"port": p}, nil
+}
+
// Create accepts a CreateOpts struct and creates a new network using the values
// provided. You must remember to provide a NetworkID value.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
- type port struct {
- NetworkID string `json:"network_id"`
- Name *string `json:"name,omitempty"`
- AdminStateUp *bool `json:"admin_state_up,omitempty"`
- MACAddress *string `json:"mac_address,omitempty"`
- FixedIPs interface{} `json:"fixed_ips,omitempty"`
- DeviceID *string `json:"device_id,omitempty"`
- DeviceOwner *string `json:"device_owner,omitempty"`
- TenantID *string `json:"tenant_id,omitempty"`
- SecurityGroups []string `json:"security_groups,omitempty"`
- }
- type request struct {
- Port port `json:"port"`
- }
-
- // Validate
- if opts.NetworkID == "" {
- res.Err = errNetworkIDRequired
+ reqBody, err := opts.ToPortCreateMap()
+ if err != nil {
+ res.Err = err
return res
}
- // Populate request body
- reqBody := request{Port: port{
- NetworkID: opts.NetworkID,
- Name: gophercloud.MaybeString(opts.Name),
- AdminStateUp: opts.AdminStateUp,
- TenantID: gophercloud.MaybeString(opts.TenantID),
- MACAddress: gophercloud.MaybeString(opts.MACAddress),
- DeviceID: gophercloud.MaybeString(opts.DeviceID),
- DeviceOwner: gophercloud.MaybeString(opts.DeviceOwner),
- }}
-
- if opts.FixedIPs != nil {
- reqBody.Port.FixedIPs = opts.FixedIPs
- }
-
- if opts.SecurityGroups != nil {
- reqBody.Port.SecurityGroups = opts.SecurityGroups
- }
-
// Response
_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
@@ -128,6 +170,14 @@
return res
}
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+ ToPortUpdateMap() (map[string]interface{}, error)
+}
+
// UpdateOpts represents the attributes used when updating an existing port.
type UpdateOpts struct {
Name string
@@ -138,39 +188,43 @@
SecurityGroups []string
}
+// ToPortUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
+ p := make(map[string]interface{})
+
+ if opts.DeviceID != "" {
+ p["device_id"] = opts.DeviceID
+ }
+ if opts.DeviceOwner != "" {
+ p["device_owner"] = opts.DeviceOwner
+ }
+ if opts.FixedIPs != nil {
+ p["fixed_ips"] = opts.FixedIPs
+ }
+ if opts.SecurityGroups != nil {
+ p["security_groups"] = opts.SecurityGroups
+ }
+ if opts.AdminStateUp != nil {
+ p["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ p["name"] = opts.Name
+ }
+
+ return map[string]interface{}{"port": p}, nil
+}
+
// Update accepts a UpdateOpts struct and updates an existing port using the
// values provided.
-func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
- type port struct {
- Name *string `json:"name,omitempty"`
- AdminStateUp *bool `json:"admin_state_up,omitempty"`
- FixedIPs interface{} `json:"fixed_ips,omitempty"`
- DeviceID *string `json:"device_id,omitempty"`
- DeviceOwner *string `json:"device_owner,omitempty"`
- SecurityGroups []string `json:"security_groups,omitempty"`
- }
- type request struct {
- Port port `json:"port"`
- }
-
- // Populate request body
- reqBody := request{Port: port{
- Name: gophercloud.MaybeString(opts.Name),
- AdminStateUp: opts.AdminStateUp,
- DeviceID: gophercloud.MaybeString(opts.DeviceID),
- DeviceOwner: gophercloud.MaybeString(opts.DeviceOwner),
- }}
-
- if opts.FixedIPs != nil {
- reqBody.Port.FixedIPs = opts.FixedIPs
- }
-
- if opts.SecurityGroups != nil {
- reqBody.Port.SecurityGroups = opts.SecurityGroups
- }
-
- // Response
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
+
+ reqBody, err := opts.ToPortUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
_, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
diff --git a/openstack/networking/v2/ports/requests_test.go b/openstack/networking/v2/ports/requests_test.go
index 7576341..9e323ef 100644
--- a/openstack/networking/v2/ports/requests_test.go
+++ b/openstack/networking/v2/ports/requests_test.go
@@ -5,16 +5,16 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -93,7 +93,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -147,7 +147,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -157,7 +157,14 @@
"port": {
"network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
"name": "private-port",
- "admin_state_up": true
+ "admin_state_up": true,
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "security_groups": ["foo"]
}
}
`)
@@ -193,7 +200,15 @@
})
asu := true
- options := CreateOpts{Name: "private-port", AdminStateUp: &asu, NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7"}
+ options := CreateOpts{
+ Name: "private-port",
+ AdminStateUp: &asu,
+ NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ FixedIPs: []IP{
+ IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ },
+ SecurityGroups: []string{"foo"},
+ }
n, err := Create(fake.ServiceClient(), options).Extract()
th.AssertNoErr(t, err)
@@ -211,11 +226,18 @@
th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
}
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
func TestUpdate(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -288,7 +310,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
diff --git a/openstack/networking/v2/ports/results.go b/openstack/networking/v2/ports/results.go
index 91118a4..cedd658 100644
--- a/openstack/networking/v2/ports/results.go
+++ b/openstack/networking/v2/ports/results.go
@@ -1,8 +1,6 @@
package ports
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -23,11 +21,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron port: %v", err)
- }
- return res.Port, nil
+ return res.Port, err
}
// CreateResult represents the result of a create operation.
@@ -94,10 +89,7 @@
// to do this, it needs to construct the next page's URL.
func (p PortPage) NextPageURL() (string, error) {
type resp struct {
- Links []struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- } `mapstructure:"ports_links"`
+ Links []gophercloud.Link `mapstructure:"ports_links"`
}
var r resp
@@ -106,17 +98,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a PortPage struct is empty.
@@ -137,9 +119,6 @@
}
err := mapstructure.Decode(page.(PortPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Ports, nil
+ return resp.Ports, err
}
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
index 092c914..8eed269 100644
--- a/openstack/networking/v2/subnets/requests.go
+++ b/openstack/networking/v2/subnets/requests.go
@@ -1,11 +1,31 @@
package subnets
import (
- "github.com/racker/perigee"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
)
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToSubnetListQuery() (string, error)
+}
+
// ListOpts allows the filtering and sorting of paginated collections through
// the API. Filtering is achieved by passing in struct field values that map to
// the subnet attributes you want to see returned. SortKey allows you to sort
@@ -26,6 +46,15 @@
SortDir string `q:"sort_dir"`
}
+// ToSubnetListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToSubnetListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
// List returns a Pager which allows you to iterate over a collection of
// subnets. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
@@ -33,13 +62,15 @@
// Default policy settings return only those subnets that are owned by the tenant
// who submits the request, unless the request is submitted by an user with
// administrative rights.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
- // Build query parameters
- query, err := gophercloud.BuildQueryString(&opts)
- if err != nil {
- return pagination.Pager{Err: err}
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToSubnetListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
}
- url := listURL(c) + query.String()
return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
return SubnetPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
@@ -63,6 +94,14 @@
IPv6 = 6
)
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToSubnetCreateMap() (map[string]interface{}, error)
+}
+
// CreateOpts represents the attributes used when creating a new subnet.
type CreateOpts struct {
// Required
@@ -76,65 +115,64 @@
IPVersion int
EnableDHCP *bool
DNSNameservers []string
- HostRoutes []interface{}
+ HostRoutes []HostRoute
+}
+
+// ToSubnetCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.NetworkID == "" {
+ return nil, errNetworkIDRequired
+ }
+ if opts.CIDR == "" {
+ return nil, errCIDRRequired
+ }
+ if opts.IPVersion != 0 && opts.IPVersion != IPv4 && opts.IPVersion != IPv6 {
+ return nil, errInvalidIPType
+ }
+
+ s["network_id"] = opts.NetworkID
+ s["cidr"] = opts.CIDR
+
+ if opts.EnableDHCP != nil {
+ s["enable_dhcp"] = &opts.EnableDHCP
+ }
+ if opts.Name != "" {
+ s["name"] = opts.Name
+ }
+ if opts.GatewayIP != "" {
+ s["gateway_ip"] = opts.GatewayIP
+ }
+ if opts.TenantID != "" {
+ s["tenant_id"] = opts.TenantID
+ }
+ if opts.IPVersion != 0 {
+ s["ip_version"] = opts.IPVersion
+ }
+ if len(opts.AllocationPools) != 0 {
+ s["allocation_pools"] = opts.AllocationPools
+ }
+ if len(opts.DNSNameservers) != 0 {
+ s["dns_nameservers"] = opts.DNSNameservers
+ }
+ if len(opts.HostRoutes) != 0 {
+ s["host_routes"] = opts.HostRoutes
+ }
+
+ return map[string]interface{}{"subnet": s}, nil
}
// Create accepts a CreateOpts struct and creates a new subnet using the values
// provided. You must remember to provide a valid NetworkID, CIDR and IP version.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
- // Validate required options
- if opts.NetworkID == "" {
- res.Err = errNetworkIDRequired
+ reqBody, err := opts.ToSubnetCreateMap()
+ if err != nil {
+ res.Err = err
return res
}
- if opts.CIDR == "" {
- res.Err = errCIDRRequired
- return res
- }
- if opts.IPVersion != 0 && opts.IPVersion != IPv4 && opts.IPVersion != IPv6 {
- res.Err = errInvalidIPType
- return res
- }
-
- type subnet struct {
- NetworkID string `json:"network_id"`
- CIDR string `json:"cidr"`
- Name *string `json:"name,omitempty"`
- TenantID *string `json:"tenant_id,omitempty"`
- AllocationPools []AllocationPool `json:"allocation_pools,omitempty"`
- GatewayIP *string `json:"gateway_ip,omitempty"`
- IPVersion int `json:"ip_version,omitempty"`
- EnableDHCP *bool `json:"enable_dhcp,omitempty"`
- DNSNameservers []string `json:"dns_nameservers,omitempty"`
- HostRoutes []interface{} `json:"host_routes,omitempty"`
- }
- type request struct {
- Subnet subnet `json:"subnet"`
- }
-
- reqBody := request{Subnet: subnet{
- NetworkID: opts.NetworkID,
- CIDR: opts.CIDR,
- Name: gophercloud.MaybeString(opts.Name),
- TenantID: gophercloud.MaybeString(opts.TenantID),
- GatewayIP: gophercloud.MaybeString(opts.GatewayIP),
- EnableDHCP: opts.EnableDHCP,
- }}
-
- if opts.IPVersion != 0 {
- reqBody.Subnet.IPVersion = opts.IPVersion
- }
- if len(opts.AllocationPools) != 0 {
- reqBody.Subnet.AllocationPools = opts.AllocationPools
- }
- if len(opts.DNSNameservers) != 0 {
- reqBody.Subnet.DNSNameservers = opts.DNSNameservers
- }
- if len(opts.HostRoutes) != 0 {
- reqBody.Subnet.HostRoutes = opts.HostRoutes
- }
_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
@@ -146,44 +184,55 @@
return res
}
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToSubnetUpdateMap() (map[string]interface{}, error)
+}
+
// UpdateOpts represents the attributes used when updating an existing subnet.
type UpdateOpts struct {
Name string
GatewayIP string
DNSNameservers []string
- HostRoutes []interface{}
+ HostRoutes []HostRoute
EnableDHCP *bool
}
+// ToSubnetUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.EnableDHCP != nil {
+ s["enable_dhcp"] = &opts.EnableDHCP
+ }
+ if opts.Name != "" {
+ s["name"] = opts.Name
+ }
+ if opts.GatewayIP != "" {
+ s["gateway_ip"] = opts.GatewayIP
+ }
+ if len(opts.DNSNameservers) != 0 {
+ s["dns_nameservers"] = opts.DNSNameservers
+ }
+ if len(opts.HostRoutes) != 0 {
+ s["host_routes"] = opts.HostRoutes
+ }
+
+ return map[string]interface{}{"subnet": s}, nil
+}
+
// Update accepts a UpdateOpts struct and updates an existing subnet using the
// values provided.
-func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
- type subnet struct {
- Name *string `json:"name,omitempty"`
- GatewayIP *string `json:"gateway_ip,omitempty"`
- DNSNameservers []string `json:"dns_nameservers,omitempty"`
- HostRoutes []interface{} `json:"host_routes,omitempty"`
- EnableDHCP *bool `json:"enable_dhcp,omitempty"`
- }
- type request struct {
- Subnet subnet `json:"subnet"`
- }
-
- reqBody := request{Subnet: subnet{
- Name: gophercloud.MaybeString(opts.Name),
- GatewayIP: gophercloud.MaybeString(opts.GatewayIP),
- EnableDHCP: opts.EnableDHCP,
- }}
-
- if len(opts.DNSNameservers) != 0 {
- reqBody.Subnet.DNSNameservers = opts.DNSNameservers
- }
-
- if len(opts.HostRoutes) != 0 {
- reqBody.Subnet.HostRoutes = opts.HostRoutes
- }
-
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
+
+ reqBody, err := opts.ToSubnetUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
_, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
diff --git a/openstack/networking/v2/subnets/requests_test.go b/openstack/networking/v2/subnets/requests_test.go
index 9f3e8df..987064a 100644
--- a/openstack/networking/v2/subnets/requests_test.go
+++ b/openstack/networking/v2/subnets/requests_test.go
@@ -5,16 +5,16 @@
"net/http"
"testing"
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -128,7 +128,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -184,7 +184,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -194,7 +194,15 @@
"subnet": {
"network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
"ip_version": 4,
- "cidr": "192.168.199.0/24"
+ "cidr": "192.168.199.0/24",
+ "dns_nameservers": ["foo"],
+ "allocation_pools": [
+ {
+ "start": "192.168.199.2",
+ "end": "192.168.199.254"
+ }
+ ],
+ "host_routes": [{"destination":"","nexthop": "bar"}]
}
}
`)
@@ -226,7 +234,21 @@
`)
})
- opts := CreateOpts{NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", IPVersion: 4, CIDR: "192.168.199.0/24"}
+ opts := CreateOpts{
+ NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ IPVersion: 4,
+ CIDR: "192.168.199.0/24",
+ AllocationPools: []AllocationPool{
+ AllocationPool{
+ Start: "192.168.199.2",
+ End: "192.168.199.254",
+ },
+ },
+ DNSNameservers: []string{"foo"},
+ HostRoutes: []HostRoute{
+ HostRoute{NextHop: "bar"},
+ },
+ }
s, err := Create(fake.ServiceClient(), opts).Extract()
th.AssertNoErr(t, err)
@@ -248,11 +270,28 @@
th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126")
}
+func TestRequiredCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
func TestUpdate(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -260,7 +299,9 @@
th.TestJSONRequest(t, r, `
{
"subnet": {
- "name": "my_new_subnet"
+ "name": "my_new_subnet",
+ "dns_nameservers": ["foo"],
+ "host_routes": [{"destination":"","nexthop": "bar"}]
}
}
`)
@@ -292,7 +333,13 @@
`)
})
- opts := UpdateOpts{Name: "my_new_subnet"}
+ opts := UpdateOpts{
+ Name: "my_new_subnet",
+ DNSNameservers: []string{"foo"},
+ HostRoutes: []HostRoute{
+ HostRoute{NextHop: "bar"},
+ },
+ }
s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
th.AssertNoErr(t, err)
@@ -304,7 +351,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
diff --git a/openstack/networking/v2/subnets/results.go b/openstack/networking/v2/subnets/results.go
index 5c5744c..3c9ef71 100644
--- a/openstack/networking/v2/subnets/results.go
+++ b/openstack/networking/v2/subnets/results.go
@@ -1,8 +1,6 @@
package subnets
import (
- "fmt"
-
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
@@ -23,11 +21,8 @@
}
err := mapstructure.Decode(r.Resp, &res)
- if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron subnet: %v", err)
- }
- return res.Subnet, nil
+ return res.Subnet, err
}
// CreateResult represents the result of a create operation.
@@ -99,12 +94,8 @@
// the end of a page and the pager seeks to traverse over a new one. In order
// to do this, it needs to construct the next page's URL.
func (p SubnetPage) NextPageURL() (string, error) {
- type link struct {
- Href string `mapstructure:"href"`
- Rel string `mapstructure:"rel"`
- }
type resp struct {
- Links []link `mapstructure:"subnets_links"`
+ Links []gophercloud.Link `mapstructure:"subnets_links"`
}
var r resp
@@ -113,17 +104,7 @@
return "", err
}
- var url string
- for _, l := range r.Links {
- if l.Rel == "next" {
- url = l.Href
- }
- }
- if url == "" {
- return "", nil
- }
-
- return url, nil
+ return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a SubnetPage struct is empty.
@@ -144,9 +125,6 @@
}
err := mapstructure.Decode(page.(SubnetPage).Body, &resp)
- if err != nil {
- return nil, err
- }
- return resp.Subnets, nil
+ return resp.Subnets, err
}
diff --git a/openstack/objectstorage/v1/accounts/accounts.go b/openstack/objectstorage/v1/accounts/accounts.go
deleted file mode 100644
index c460e45..0000000
--- a/openstack/objectstorage/v1/accounts/accounts.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package accounts
-
-import (
- "strings"
-)
-
-// UpdateOpts is a structure that contains parameters for updating, creating, or deleting an
-// account's metadata.
-type UpdateOpts struct {
- Metadata map[string]string
- Headers map[string]string
-}
-
-// GetOpts is a structure that contains parameters for getting an account's metadata.
-type GetOpts struct {
- Headers map[string]string
-}
-
-// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
-// and returns the custom metatdata associated with the account.
-func ExtractMetadata(gr GetResult) map[string]string {
- metadata := make(map[string]string)
- for k, v := range gr.Header {
- if strings.HasPrefix(k, "X-Account-Meta-") {
- key := strings.TrimPrefix(k, "X-Account-Meta-")
- metadata[key] = v[0]
- }
- }
- return metadata
-}
diff --git a/openstack/objectstorage/v1/accounts/accounts_test.go b/openstack/objectstorage/v1/accounts/accounts_test.go
deleted file mode 100644
index 2c2a84a..0000000
--- a/openstack/objectstorage/v1/accounts/accounts_test.go
+++ /dev/null
@@ -1,19 +0,0 @@
-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/requests.go b/openstack/objectstorage/v1/accounts/requests.go
index c738573..55fcb05 100644
--- a/openstack/objectstorage/v1/accounts/requests.go
+++ b/openstack/objectstorage/v1/accounts/requests.go
@@ -1,46 +1,107 @@
package accounts
import (
- "net/http"
-
"github.com/racker/perigee"
"github.com/rackspace/gophercloud"
)
-// GetResult is a *http.Response that is returned from a call to the Get function.
-type GetResult *http.Response
-
-// Update is a function that creates, updates, or deletes an account'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-Account-Meta-"+k] = v
- }
-
- _, err := perigee.Request("POST", accountURL(c), perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{204},
- })
- return err
+// GetOptsBuilder allows extensions to add additional headers to the Get
+// request.
+type GetOptsBuilder interface {
+ ToAccountGetMap() (map[string]string, error)
}
-// Get is a function that retrieves an account's metadata. To extract just the custom
-// metadata, pass the GetResult response to the ExtractMetadata function.
-func Get(c *gophercloud.ServiceClient, opts GetOpts) (GetResult, error) {
+// GetOpts is a structure that contains parameters for getting an account's
+// metadata.
+type GetOpts struct {
+ Newest bool `h:"X-Newest"`
+}
+
+// ToAccountGetMap formats a GetOpts into a map[string]string of headers.
+func (opts GetOpts) ToAccountGetMap() (map[string]string, error) {
+ return gophercloud.BuildHeaders(opts)
+}
+
+// Get is a function that retrieves an account's metadata. To extract just the
+// custom metadata, call the ExtractMetadata method on the GetResult. To extract
+// all the headers that are returned (including the metadata), call the
+// ExtractHeaders method on the GetResult.
+func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult {
+ var res GetResult
h := c.Provider.AuthenticatedHeaders()
- for k, v := range opts.Headers {
- h[k] = v
+ if opts != nil {
+ headers, err := opts.ToAccountGetMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
}
- resp, err := perigee.Request("HEAD", accountURL(c), perigee.Options{
+ resp, err := perigee.Request("HEAD", getURL(c), perigee.Options{
MoreHeaders: h,
OkCodes: []int{204},
})
- return &resp.HttpResponse, err
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional headers to the Update
+// request.
+type UpdateOptsBuilder interface {
+ ToAccountUpdateMap() (map[string]string, error)
+}
+
+// UpdateOpts is a structure that contains parameters for updating, creating, or
+// deleting an account's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ ContentType string `h:"Content-Type"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+ TempURLKey string `h:"X-Account-Meta-Temp-URL-Key"`
+ TempURLKey2 string `h:"X-Account-Meta-Temp-URL-Key-2"`
+}
+
+// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers.
+func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ headers["X-Account-Meta-"+k] = v
+ }
+ return headers, err
+}
+
+// Update is a function that creates, updates, or deletes an account's metadata.
+// To extract the headers returned, call the ExtractHeaders method on the
+// UpdateResult.
+func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+ h := c.Provider.AuthenticatedHeaders()
+
+ if opts != nil {
+ headers, err := opts.ToAccountUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ for k, v := range headers {
+ h[k] = v
+ }
+ }
+
+ resp, err := perigee.Request("POST", updateURL(c), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ res.Resp = &resp.HttpResponse
+ res.Err = err
+ return res
}
diff --git a/openstack/objectstorage/v1/accounts/requests_test.go b/openstack/objectstorage/v1/accounts/requests_test.go
index 348f93e..0090eea 100644
--- a/openstack/objectstorage/v1/accounts/requests_test.go
+++ b/openstack/objectstorage/v1/accounts/requests_test.go
@@ -4,41 +4,50 @@
"net/http"
"testing"
- "github.com/rackspace/gophercloud/testhelper"
+ th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
var metadata = map[string]string{"gophercloud-test": "accounts"}
func TestUpdateAccount(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts")
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts")
+
+ w.Header().Set("X-Account-Container-Count", "2")
+ w.Header().Set("X-Account-Bytes-Used", "14")
+ w.Header().Set("X-Account-Meta-Subject", "books")
+
w.WriteHeader(http.StatusNoContent)
})
- err := Update(fake.ServiceClient(), UpdateOpts{Metadata: metadata})
+ options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}}
+ _, err := Update(fake.ServiceClient(), options).ExtractHeaders()
if err != nil {
t.Fatalf("Unable to update account: %v", err)
}
}
func TestGetAccount(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "HEAD")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "HEAD")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.Header().Set("X-Account-Meta-Foo", "bar")
w.WriteHeader(http.StatusNoContent)
})
- _, err := Get(fake.ServiceClient(), GetOpts{})
+ expected := map[string]string{"Foo": "bar"}
+ actual, err := Get(fake.ServiceClient(), &GetOpts{}).ExtractMetadata()
if err != nil {
t.Fatalf("Unable to get account metadata: %v", err)
}
+ th.CheckDeepEquals(t, expected, actual)
}
diff --git a/openstack/objectstorage/v1/accounts/results.go b/openstack/objectstorage/v1/accounts/results.go
new file mode 100644
index 0000000..8ff8183
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/results.go
@@ -0,0 +1,34 @@
+package accounts
+
+import (
+ "strings"
+
+ objectstorage "github.com/rackspace/gophercloud/openstack/objectstorage/v1"
+)
+
+// GetResult is returned from a call to the Get function. See v1.CommonResult.
+type GetResult struct {
+ objectstorage.CommonResult
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metatdata associated with the account.
+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-Account-Meta-") {
+ key := strings.TrimPrefix(k, "X-Account-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata, nil
+}
+
+// UpdateResult is returned from a call to the Update function. See v1.CommonResult.
+type UpdateResult struct {
+ objectstorage.CommonResult
+}
diff --git a/openstack/objectstorage/v1/accounts/urls.go b/openstack/objectstorage/v1/accounts/urls.go
index 53b1343..9952fe4 100644
--- a/openstack/objectstorage/v1/accounts/urls.go
+++ b/openstack/objectstorage/v1/accounts/urls.go
@@ -2,7 +2,10 @@
import "github.com/rackspace/gophercloud"
-// accountURL returns the URI for making Account requests.
-func accountURL(c *gophercloud.ServiceClient) string {
+func getURL(c *gophercloud.ServiceClient) string {
return c.Endpoint
}
+
+func updateURL(c *gophercloud.ServiceClient) string {
+ return getURL(c)
+}
diff --git a/openstack/objectstorage/v1/accounts/urls_test.go b/openstack/objectstorage/v1/accounts/urls_test.go
index f127a5e..074d52d 100644
--- a/openstack/objectstorage/v1/accounts/urls_test.go
+++ b/openstack/objectstorage/v1/accounts/urls_test.go
@@ -4,14 +4,23 @@
"testing"
"github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
)
-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)
- }
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient())
+ expected := endpoint
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient())
+ expected := endpoint
+ th.CheckEquals(t, expected, actual)
}
diff --git a/openstack/objectstorage/v1/common.go b/openstack/objectstorage/v1/common.go
new file mode 100644
index 0000000..1a6c44a
--- /dev/null
+++ b/openstack/objectstorage/v1/common.go
@@ -0,0 +1,25 @@
+package v1
+
+import (
+ "net/http"
+)
+
+// CommonResult is a structure that contains the response and error of a call to an
+// object storage endpoint.
+type CommonResult struct {
+ Resp *http.Response
+ Err error
+}
+
+// ExtractHeaders will extract and return the headers from a *http.Response.
+func (cr CommonResult) ExtractHeaders() (http.Header, error) {
+ if cr.Err != nil {
+ return nil, cr.Err
+ }
+
+ var headers http.Header
+ if cr.Err != nil {
+ return headers, cr.Err
+ }
+ return cr.Resp.Header, nil
+}
diff --git a/openstack/objectstorage/v1/containers/requests.go b/openstack/objectstorage/v1/containers/requests.go
index d772a43..ce3f540 100644
--- a/openstack/objectstorage/v1/containers/requests.go
+++ b/openstack/objectstorage/v1/containers/requests.go
@@ -6,35 +6,50 @@
"github.com/rackspace/gophercloud/pagination"
)
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+ ToContainerListParams() (bool, string, error)
+}
+
// 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"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ EndMarker string `q:"end_marker"`
+ Format string `q:"format"`
+ Prefix string `q:"prefix"`
+ Delimiter string `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
+// ToContainerListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each container.
+func (opts ListOpts) ToContainerListParams() (bool, string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return false, "", err
+ }
+ return opts.Full, q.String(), nil
+}
- url := accountURL(c)
+// 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 ListOptsBuilder) pagination.Pager {
+ headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+
+ url := listURL(c)
if opts != nil {
- query, err := gophercloud.BuildQueryString(opts)
+ full, query, err := opts.ToContainerListParams()
if err != nil {
return pagination.Pager{Err: err}
}
- url += query.String()
+ url += query
- if !opts.Full {
- headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+ if full {
+ headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"}
}
- } else {
- headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
}
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
@@ -48,6 +63,12 @@
return pager
}
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToContainerCreateMap() (map[string]string, error)
+}
+
// CreateOpts is a structure that holds parameters for creating a container.
type CreateOpts struct {
Metadata map[string]string
@@ -61,13 +82,25 @@
VersionsLocation string `h:"X-Versions-Location"`
}
+// ToContainerCreateMap formats a CreateOpts into a map of headers.
+func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) {
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+ return h, nil
+}
+
// Create is a function that creates a new container.
-func Create(c *gophercloud.ServiceClient, containerName string, opts *CreateOpts) CreateResult {
+func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsBuilder) CreateResult {
var res CreateResult
h := c.Provider.AuthenticatedHeaders()
if opts != nil {
- headers, err := gophercloud.BuildHeaders(opts)
+ headers, err := opts.ToContainerCreateMap()
if err != nil {
res.Err = err
return res
@@ -76,13 +109,9 @@
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{
+ resp, err := perigee.Request("PUT", createURL(c, containerName), perigee.Options{
MoreHeaders: h,
OkCodes: []int{201, 204},
})
@@ -94,7 +123,7 @@
// 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{
+ resp, err := perigee.Request("DELETE", deleteURL(c, containerName), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
@@ -103,8 +132,14 @@
return res
}
-// UpdateOpts is a structure that holds parameters for updating, creating, or deleting a
-// container's metadata.
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToContainerUpdateMap() (map[string]string, error)
+}
+
+// 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"`
@@ -117,13 +152,26 @@
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 {
+// ToContainerUpdateMap formats a CreateOpts into a map of headers.
+func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) {
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+ return h, nil
+}
+
+// Update is a function that creates, updates, or deletes a container's
+// metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
h := c.Provider.AuthenticatedHeaders()
if opts != nil {
- headers, err := gophercloud.BuildHeaders(opts)
+ headers, err := opts.ToContainerUpdateMap()
if err != nil {
res.Err = err
return res
@@ -132,13 +180,9 @@
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{
+ resp, err := perigee.Request("POST", updateURL(c, containerName), perigee.Options{
MoreHeaders: h,
OkCodes: []int{204},
})
@@ -147,11 +191,12 @@
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.
+// 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{
+ resp, err := perigee.Request("HEAD", getURL(c, containerName), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
diff --git a/openstack/objectstorage/v1/containers/requests_test.go b/openstack/objectstorage/v1/containers/requests_test.go
index 09930d0..9562676 100644
--- a/openstack/objectstorage/v1/containers/requests_test.go
+++ b/openstack/objectstorage/v1/containers/requests_test.go
@@ -6,20 +6,20 @@
"testing"
"github.com/rackspace/gophercloud/pagination"
- "github.com/rackspace/gophercloud/testhelper"
+ th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
var metadata = map[string]string{"gophercloud-test": "containers"}
func TestListContainerInfo(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestHeader(t, r, "Accept", "application/json")
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
w.Header().Set("Content-Type", "application/json")
r.ParseForm()
@@ -68,7 +68,7 @@
},
}
- testhelper.CheckDeepEquals(t, expected, actual)
+ th.CheckDeepEquals(t, expected, actual)
return true, nil
})
@@ -79,13 +79,13 @@
}
func TestListContainerNames(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestHeader(t, r, "Accept", "text/plain")
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "text/plain")
w.Header().Set("Content-Type", "text/plain")
r.ParseForm()
@@ -112,7 +112,7 @@
expected := []string{"janeausten", "marktwain"}
- testhelper.CheckDeepEquals(t, expected, actual)
+ th.CheckDeepEquals(t, expected, actual)
return true, nil
})
@@ -123,30 +123,34 @@
}
func TestCreateContainer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "PUT")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestHeader(t, r, "Accept", "application/json")
+ th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Add("X-Container-Meta-Foo", "bar")
w.WriteHeader(http.StatusNoContent)
})
- _, err := Create(fake.ServiceClient(), "testContainer", nil).ExtractHeaders()
+ options := CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}}
+ headers, err := Create(fake.ServiceClient(), "testContainer", options).ExtractHeaders()
if err != nil {
t.Fatalf("Unexpected error creating container: %v", err)
}
+ th.CheckEquals(t, "bar", headers["X-Container-Meta-Foo"][0])
}
func TestDeleteContainer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "DELETE")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestHeader(t, r, "Accept", "application/json")
+ th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
w.WriteHeader(http.StatusNoContent)
})
@@ -157,30 +161,31 @@
}
func TestUpateContainer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestHeader(t, r, "Accept", "application/json")
+ th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
w.WriteHeader(http.StatusNoContent)
})
- _, err := Update(fake.ServiceClient(), "testContainer", nil).ExtractHeaders()
+ options := &UpdateOpts{Metadata: map[string]string{"foo": "bar"}}
+ _, err := Update(fake.ServiceClient(), "testContainer", options).ExtractHeaders()
if err != nil {
t.Fatalf("Unexpected error updating container metadata: %v", err)
}
}
func TestGetContainer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "HEAD")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestHeader(t, r, "Accept", "application/json")
+ th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "HEAD")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
w.WriteHeader(http.StatusNoContent)
})
diff --git a/openstack/objectstorage/v1/containers/results.go b/openstack/objectstorage/v1/containers/results.go
index be96fca..227a9dc 100644
--- a/openstack/objectstorage/v1/containers/results.go
+++ b/openstack/objectstorage/v1/containers/results.go
@@ -2,11 +2,12 @@
import (
"fmt"
- "net/http"
"strings"
- "github.com/mitchellh/mapstructure"
+ objectstorage "github.com/rackspace/gophercloud/openstack/objectstorage/v1"
"github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
)
// Container represents a container resource.
@@ -97,8 +98,7 @@
// GetResult represents the result of a get operation.
type GetResult struct {
- Resp *http.Response
- Err error
+ objectstorage.CommonResult
}
// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
@@ -117,37 +117,23 @@
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
-}
-
// CreateResult represents the result of a create operation. To extract the
// the headers from the HTTP response, you can invoke the 'ExtractHeaders'
// method on the result struct.
type CreateResult struct {
- commonResult
+ objectstorage.CommonResult
}
// UpdateResult represents the result of an update operation. To extract the
// the headers from the HTTP response, you can invoke the 'ExtractHeaders'
// method on the result struct.
type UpdateResult struct {
- commonResult
+ objectstorage.CommonResult
}
// DeleteResult represents the result of a delete operation. To extract the
// the headers from the HTTP response, you can invoke the 'ExtractHeaders'
// method on the result struct.
type DeleteResult struct {
- commonResult
+ objectstorage.CommonResult
}
diff --git a/openstack/objectstorage/v1/containers/urls.go b/openstack/objectstorage/v1/containers/urls.go
index 2a06f95..f864f84 100644
--- a/openstack/objectstorage/v1/containers/urls.go
+++ b/openstack/objectstorage/v1/containers/urls.go
@@ -2,12 +2,22 @@
import "github.com/rackspace/gophercloud"
-// accountURL returns the URI used to list Containers.
-func accountURL(c *gophercloud.ServiceClient) string {
+func listURL(c *gophercloud.ServiceClient) string {
return c.Endpoint
}
-// containerURL returns the URI for making Container requests.
-func containerURL(c *gophercloud.ServiceClient, container string) string {
+func createURL(c *gophercloud.ServiceClient, container string) string {
return c.ServiceURL(container)
}
+
+func getURL(c *gophercloud.ServiceClient, container string) string {
+ return createURL(c, container)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, container string) string {
+ return createURL(c, container)
+}
+
+func updateURL(c *gophercloud.ServiceClient, container string) string {
+ return createURL(c, container)
+}
diff --git a/openstack/objectstorage/v1/containers/urls_test.go b/openstack/objectstorage/v1/containers/urls_test.go
index da37bf6..d043a2a 100644
--- a/openstack/objectstorage/v1/containers/urls_test.go
+++ b/openstack/objectstorage/v1/containers/urls_test.go
@@ -1,29 +1,43 @@
package containers
import (
- "testing"
"github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "testing"
)
-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)
- }
+const endpoint = "http://localhost:57909/"
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
}
-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 TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
}
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
index bc21496..3274e04 100644
--- a/openstack/objectstorage/v1/objects/requests.go
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -10,38 +10,52 @@
"github.com/rackspace/gophercloud/pagination"
)
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+ ToObjectListParams() (bool, string, error)
+}
+
// 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"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ EndMarker string `q:"end_marker"`
+ Format string `q:"format"`
+ Prefix string `q:"prefix"`
+ Delimiter string `q:"delimiter"`
+ Path string `q:"path"`
+}
+
+// ToObjectListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each object.
+func (opts ListOpts) ToObjectListParams() (bool, string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return false, "", err
+ }
+ return opts.Full, q.String(), nil
}
// 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
+func List(c *gophercloud.ServiceClient, containerName string, opts ListOptsBuilder) pagination.Pager {
+ headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
- url := containerURL(c, containerName)
+ url := listURL(c, containerName)
if opts != nil {
- query, err := gophercloud.BuildQueryString(opts)
+ full, query, err := opts.ToObjectListParams()
if err != nil {
fmt.Printf("Error building query string: %v", err)
return pagination.Pager{Err: err}
}
- url += query.String()
+ url += query
- if !opts.Full {
- headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+ if full {
+ headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"}
}
- } else {
- headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
}
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
@@ -55,6 +69,12 @@
return pager
}
+// DownloadOptsBuilder allows extensions to add additional parameters to the
+// Download request.
+type DownloadOptsBuilder interface {
+ ToObjectDownloadParams() (map[string]string, string, error)
+}
+
// DownloadOpts is a structure that holds parameters for downloading an object.
type DownloadOpts struct {
IfMatch string `h:"If-Match"`
@@ -67,17 +87,31 @@
Signature string `q:"signature"`
}
+// ToObjectDownloadParams formats a DownloadOpts into a query string and map of
+// headers.
+func (opts ListOpts) ToObjectDownloadParams() (map[string]string, string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return nil, "", err
+ }
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, q.String(), err
+ }
+ return h, q.String(), nil
+}
+
// 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 {
+// To extract just the content, pass the DownloadResult response to the
+// ExtractContent function.
+func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) DownloadResult {
var res DownloadResult
- url := objectURL(c, containerName, objectName)
+ url := downloadURL(c, containerName, objectName)
h := c.Provider.AuthenticatedHeaders()
if opts != nil {
- headers, err := gophercloud.BuildHeaders(opts)
+ headers, query, err := opts.ToObjectDownloadParams()
if err != nil {
res.Err = err
return res
@@ -87,12 +121,7 @@
h[k] = v
}
- query, err := gophercloud.BuildQueryString(opts)
- if err != nil {
- res.Err = err
- return res
- }
- url += query.String()
+ url += query
}
resp, err := perigee.Request("GET", url, perigee.Options{
@@ -104,6 +133,12 @@
return res
}
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToObjectCreateParams() (map[string]string, string, error)
+}
+
// CreateOpts is a structure that holds parameters for creating an object.
type CreateOpts struct {
Metadata map[string]string
@@ -124,16 +159,35 @@
Signature string `q:"signature"`
}
+// ToObjectCreateParams formats a CreateOpts into a query string and map of
+// headers.
+func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return nil, "", err
+ }
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, q.String(), err
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+
+ return h, q.String(), nil
+}
+
// 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 {
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult {
var res CreateResult
var reqBody []byte
- url := objectURL(c, containerName, objectName)
+ url := createURL(c, containerName, objectName)
h := c.Provider.AuthenticatedHeaders()
if opts != nil {
- headers, err := gophercloud.BuildHeaders(opts)
+ headers, query, err := opts.ToObjectCreateParams()
if err != nil {
res.Err = err
return res
@@ -143,17 +197,7 @@
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()
+ url += query
}
if content != nil {
@@ -175,7 +219,14 @@
return res
}
-// CopyOpts is a structure that holds parameters for copying one object to another.
+// CopyOptsBuilder allows extensions to add additional parameters to the
+// Copy request.
+type CopyOptsBuilder interface {
+ ToObjectCopyMap() (map[string]string, error)
+}
+
+// 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"`
@@ -184,29 +235,37 @@
Destination string `h:"Destination,required"`
}
+// ToObjectCopyMap formats a CopyOpts into a map of headers.
+func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) {
+ if opts.Destination == "" {
+ return nil, fmt.Errorf("Required CopyOpts field 'Destination' not set.")
+ }
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+ return h, nil
+}
+
// Copy is a function that copies one object to another.
-func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts *CopyOpts) CopyResult {
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) 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)
+ headers, err := opts.ToObjectCopyMap()
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)
+ url := copyURL(c, containerName, objectName)
resp, err := perigee.Request("COPY", url, perigee.Options{
MoreHeaders: h,
OkCodes: []int{201},
@@ -215,23 +274,38 @@
return res
}
+// DeleteOptsBuilder allows extensions to add additional parameters to the
+// Delete request.
+type DeleteOptsBuilder interface {
+ ToObjectDeleteQuery() (string, error)
+}
+
// DeleteOpts is a structure that holds parameters for deleting an object.
type DeleteOpts struct {
MultipartManifest string `q:"multipart-manifest"`
}
+// ToObjectDeleteQuery formats a DeleteOpts into a query string.
+func (opts DeleteOpts) ToObjectDeleteQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
// Delete is a function that deletes an object.
-func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts *DeleteOpts) DeleteResult {
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) DeleteResult {
var res DeleteResult
- url := objectURL(c, containerName, objectName)
+ url := deleteURL(c, containerName, objectName)
if opts != nil {
- query, err := gophercloud.BuildQueryString(opts)
+ query, err := opts.ToObjectDeleteQuery()
if err != nil {
res.Err = err
return res
}
- url += query.String()
+ url += query
}
resp, err := perigee.Request("DELETE", url, perigee.Options{
@@ -243,25 +317,40 @@
return res
}
+// GetOptsBuilder allows extensions to add additional parameters to the
+// Get request.
+type GetOptsBuilder interface {
+ ToObjectGetQuery() (string, error)
+}
+
// GetOpts is a structure that holds parameters for getting an object's metadata.
type GetOpts struct {
Expires string `q:"expires"`
Signature string `q:"signature"`
}
+// ToObjectGetQuery formats a GetOpts into a query string.
+func (opts GetOpts) ToObjectGetQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
// 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 {
+func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) GetResult {
var res GetResult
- url := objectURL(c, containerName, objectName)
+ url := getURL(c, containerName, objectName)
if opts != nil {
- query, err := gophercloud.BuildQueryString(opts)
+ query, err := opts.ToObjectGetQuery()
if err != nil {
res.Err = err
return res
}
- url += query.String()
+ url += query
}
resp, err := perigee.Request("HEAD", url, perigee.Options{
@@ -273,6 +362,12 @@
return res
}
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToObjectUpdateMap() (map[string]string, error)
+}
+
// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
// object's metadata.
type UpdateOpts struct {
@@ -285,13 +380,25 @@
DetectContentType bool `h:"X-Detect-Content-Type"`
}
+// ToObjectUpdateMap formats a UpdateOpts into a map of headers.
+func (opts UpdateOpts) ToObjectUpdateMap() (map[string]string, error) {
+ h, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+ return h, nil
+}
+
// Update is a function that creates, updates, or deletes an object's metadata.
-func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts *UpdateOpts) UpdateResult {
+func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
h := c.Provider.AuthenticatedHeaders()
if opts != nil {
- headers, err := gophercloud.BuildHeaders(opts)
+ headers, err := opts.ToObjectUpdateMap()
if err != nil {
res.Err = err
return res
@@ -300,13 +407,9 @@
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)
+ url := updateURL(c, containerName, objectName)
resp, err := perigee.Request("POST", url, perigee.Options{
MoreHeaders: h,
OkCodes: []int{202},
diff --git a/openstack/objectstorage/v1/objects/requests_test.go b/openstack/objectstorage/v1/objects/requests_test.go
index 089081f..11d7c44 100644
--- a/openstack/objectstorage/v1/objects/requests_test.go
+++ b/openstack/objectstorage/v1/objects/requests_test.go
@@ -166,7 +166,8 @@
})
content := bytes.NewBufferString("Did gyre and gimble in the wabe")
- _, err := Create(fake.ServiceClient(), "testContainer", "testObject", content, nil).ExtractHeaders()
+ options := &CreateOpts{ContentType: "application/json"}
+ _, err := Create(fake.ServiceClient(), "testContainer", "testObject", content, options).ExtractHeaders()
if err != nil {
t.Fatalf("Unexpected error creating object: %v", err)
}
@@ -184,7 +185,8 @@
w.WriteHeader(http.StatusCreated)
})
- _, err := Copy(fake.ServiceClient(), "testContainer", "testObject", &CopyOpts{Destination: "/newTestContainer/newTestObject"}).ExtractHeaders()
+ options := &CopyOpts{Destination: "/newTestContainer/newTestObject"}
+ _, err := Copy(fake.ServiceClient(), "testContainer", "testObject", options).ExtractHeaders()
if err != nil {
t.Fatalf("Unexpected error copying object: %v", err)
}
diff --git a/openstack/objectstorage/v1/objects/results.go b/openstack/objectstorage/v1/objects/results.go
index f7db3ed..1dda7a3 100644
--- a/openstack/objectstorage/v1/objects/results.go
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -3,11 +3,12 @@
import (
"fmt"
"io/ioutil"
- "net/http"
"strings"
- "github.com/mitchellh/mapstructure"
+ objectstorage "github.com/rackspace/gophercloud/openstack/objectstorage/v1"
"github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
)
// Object is a structure that holds information related to a storage object.
@@ -97,7 +98,7 @@
// DownloadResult is a *http.Response that is returned from a call to the Download function.
type DownloadResult struct {
- commonResult
+ objectstorage.CommonResult
}
// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
@@ -117,7 +118,7 @@
// GetResult is a *http.Response that is returned from a call to the Get function.
type GetResult struct {
- commonResult
+ objectstorage.CommonResult
}
// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
@@ -136,36 +137,22 @@
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
-}
-
// CreateResult represents the result of a create operation.
type CreateResult struct {
- commonResult
+ objectstorage.CommonResult
}
// UpdateResult represents the result of an update operation.
type UpdateResult struct {
- commonResult
+ objectstorage.CommonResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
- commonResult
+ objectstorage.CommonResult
}
// CopyResult represents the result of a copy operation.
type CopyResult struct {
- commonResult
+ objectstorage.CommonResult
}
diff --git a/openstack/objectstorage/v1/objects/urls.go b/openstack/objectstorage/v1/objects/urls.go
index a377960..d2ec62c 100644
--- a/openstack/objectstorage/v1/objects/urls.go
+++ b/openstack/objectstorage/v1/objects/urls.go
@@ -1,13 +1,33 @@
package objects
-import "github.com/rackspace/gophercloud"
+import (
+ "github.com/rackspace/gophercloud"
+)
-// objectURL returns the URI for making Object requests.
-func objectURL(c *gophercloud.ServiceClient, container, object string) string {
+func listURL(c *gophercloud.ServiceClient, container string) string {
+ return c.ServiceURL(container)
+}
+
+func copyURL(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)
+func createURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
+}
+
+func getURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
+}
+
+func downloadURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
+}
+
+func updateURL(c *gophercloud.ServiceClient, container, object string) string {
+ return copyURL(c, container, object)
}
diff --git a/openstack/objectstorage/v1/objects/urls_test.go b/openstack/objectstorage/v1/objects/urls_test.go
index 89d1cb1..1dcfe35 100644
--- a/openstack/objectstorage/v1/objects/urls_test.go
+++ b/openstack/objectstorage/v1/objects/urls_test.go
@@ -2,27 +2,55 @@
import (
"testing"
+
"github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
)
-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)
- }
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
}
-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)
- }
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient(), "foo")
+ expected := endpoint + "foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestCopyURL(t *testing.T) {
+ actual := copyURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestDownloadURL(t *testing.T) {
+ actual := downloadURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "foo/bar"
+ th.CheckEquals(t, expected, actual)
}
diff --git a/pagination/pager.go b/pagination/pager.go
index 22d6d84..75fe408 100644
--- a/pagination/pager.go
+++ b/pagination/pager.go
@@ -29,10 +29,10 @@
// Pager knows how to advance through a specific resource collection, one page at a time.
type Pager struct {
- initialURL string
-
client *gophercloud.ServiceClient
+ initialURL string
+
createPage func(r LastHTTPResponse) Page
Err error
@@ -45,8 +45,18 @@
// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page.
func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r LastHTTPResponse) Page) Pager {
return Pager{
- initialURL: initialURL,
client: client,
+ initialURL: initialURL,
+ createPage: createPage,
+ }
+}
+
+// WithPageCreator returns a new Pager that substitutes a different page creation function. This is
+// useful for overriding List functions in delegation.
+func (p Pager) WithPageCreator(createPage func(r LastHTTPResponse) Page) Pager {
+ return Pager{
+ client: p.client,
+ initialURL: p.initialURL,
createPage: createPage,
}
}
diff --git a/params_test.go b/params_test.go
index 03eaefc..9f1d3bd 100644
--- a/params_test.go
+++ b/params_test.go
@@ -2,58 +2,141 @@
import (
"net/url"
+ "reflect"
"testing"
+ "time"
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) {
+func TestMaybeString(t *testing.T) {
+ testString := ""
var expected *string
- actual := MaybeString("")
- th.CheckDeepEquals(t, actual, expected)
+ actual := MaybeString(testString)
+ th.CheckDeepEquals(t, expected, actual)
+
+ testString = "carol"
+ expected = &testString
+ actual = MaybeString(testString)
+ th.CheckDeepEquals(t, expected, actual)
}
-func TestBuildQueryStringWithPointerToStruct(t *testing.T) {
- expected := &url.URL{
- RawQuery: "j=2&r=red",
- }
+func TestMaybeInt(t *testing.T) {
+ testInt := 0
+ var expected *int
+ actual := MaybeInt(testInt)
+ th.CheckDeepEquals(t, expected, actual)
- type Opts struct {
+ testInt = 4
+ expected = &testInt
+ actual = MaybeInt(testInt)
+ th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestBuildQueryString(t *testing.T) {
+ opts := struct {
J int `q:"j"`
- R string `q:"r"`
- C bool
+ R string `q:"r,required"`
+ C bool `q:"c"`
+ }{
+ J: 2,
+ R: "red",
+ C: true,
}
-
- opts := Opts{J: 2, R: "red"}
-
+ expected := &url.URL{RawQuery: "j=2&r=red&c=true"}
actual, err := BuildQueryString(&opts)
if err != nil {
t.Errorf("Error building query string: %v", err)
}
+ th.CheckDeepEquals(t, expected, actual)
- th.CheckDeepEquals(t, actual, expected)
-}
-
-func TestBuildQueryStringWithoutRequiredFieldSet(t *testing.T) {
- type Opts struct {
+ opts = struct {
J int `q:"j"`
R string `q:"r,required"`
- C bool
+ C bool `q:"c"`
+ }{
+ J: 2,
+ C: true,
}
-
- opts := Opts{J: 2, C: true}
-
- _, err := BuildQueryString(&opts)
+ _, err = BuildQueryString(&opts)
if err == nil {
- t.Error("Unexpected result: There should be an error thrown when a required field isn't set.")
+ t.Errorf("Expected error: 'Required field not set'")
+ }
+ th.CheckDeepEquals(t, expected, actual)
+
+ _, err = BuildQueryString(map[string]interface{}{"Number": 4})
+ if err == nil {
+ t.Errorf("Expected error: 'Options type is not a struct'")
+ }
+}
+
+func TestBuildHeaders(t *testing.T) {
+ testStruct := struct {
+ Accept string `h:"Accept"`
+ Num int `h:"Number,required"`
+ Style bool `h:"Style"`
+ }{
+ Accept: "application/json",
+ Num: 4,
+ Style: true,
+ }
+ expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true"}
+ actual, err := BuildHeaders(&testStruct)
+ th.CheckNoErr(t, err)
+ th.CheckDeepEquals(t, expected, actual)
+
+ testStruct.Num = 0
+ _, err = BuildHeaders(&testStruct)
+ if err == nil {
+ t.Errorf("Expected error: 'Required header not set'")
}
- t.Log(err)
+ _, err = BuildHeaders(map[string]interface{}{"Number": 4})
+ if err == nil {
+ t.Errorf("Expected error: 'Options type is not a struct'")
+ }
+}
+
+func TestIsZero(t *testing.T) {
+ var testMap map[string]interface{}
+ testMapValue := reflect.ValueOf(testMap)
+ expected := true
+ actual := isZero(testMapValue)
+ th.CheckEquals(t, expected, actual)
+ testMap = map[string]interface{}{"empty": false}
+ testMapValue = reflect.ValueOf(testMap)
+ expected = false
+ actual = isZero(testMapValue)
+ th.CheckEquals(t, expected, actual)
+
+ var testArray [2]string
+ testArrayValue := reflect.ValueOf(testArray)
+ expected = true
+ actual = isZero(testArrayValue)
+ th.CheckEquals(t, expected, actual)
+ testArray = [2]string{"one", "two"}
+ testArrayValue = reflect.ValueOf(testArray)
+ expected = false
+ actual = isZero(testArrayValue)
+ th.CheckEquals(t, expected, actual)
+
+ var testStruct struct {
+ A string
+ B time.Time
+ }
+ testStructValue := reflect.ValueOf(testStruct)
+ expected = true
+ actual = isZero(testStructValue)
+ th.CheckEquals(t, expected, actual)
+ testStruct = struct {
+ A string
+ B time.Time
+ }{
+ B: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
+ }
+ testStructValue = reflect.ValueOf(testStruct)
+ expected = false
+ actual = isZero(testStructValue)
+ th.CheckEquals(t, expected, actual)
+
}
diff --git a/provider_client_test.go b/provider_client_test.go
new file mode 100644
index 0000000..b260246
--- /dev/null
+++ b/provider_client_test.go
@@ -0,0 +1,16 @@
+package gophercloud
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedHeaders(t *testing.T) {
+ p := &ProviderClient{
+ TokenID: "1234",
+ }
+ expected := map[string]string{"X-Auth-Token": "1234"}
+ actual := p.AuthenticatedHeaders()
+ th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/client.go b/rackspace/client.go
new file mode 100644
index 0000000..d8c4a19
--- /dev/null
+++ b/rackspace/client.go
@@ -0,0 +1,115 @@
+package rackspace
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ tokens2 "github.com/rackspace/gophercloud/rackspace/identity/v2/tokens"
+)
+
+const (
+ // RackspaceUSIdentity is an identity endpoint located in the United States.
+ RackspaceUSIdentity = "https://identity.api.rackspacecloud.com/v2.0/"
+
+ // RackspaceUKIdentity is an identity endpoint located in the UK.
+ RackspaceUKIdentity = "https://lon.identity.api.rackspacecloud.com/v2.0/"
+)
+
+const (
+ v20 = "v2.0"
+)
+
+// NewClient creates a client that's prepared to communicate with the Rackspace API, but is not
+// yet authenticated. Most users will probably prefer using the AuthenticatedClient function
+// instead.
+//
+// Provide the base URL of the identity endpoint you wish to authenticate against as "endpoint".
+// Often, this will be either RackspaceUSIdentity or RackspaceUKIdentity.
+func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
+ if endpoint == "" {
+ return os.NewClient(RackspaceUSIdentity)
+ }
+ return os.NewClient(endpoint)
+}
+
+// AuthenticatedClient logs in to Rackspace with the provided credentials and constructs a
+// ProviderClient that's ready to operate.
+//
+// If the provided AuthOptions does not specify an explicit IdentityEndpoint, it will default to
+// the canonical, production Rackspace US identity endpoint.
+func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
+ client, err := NewClient(options.IdentityEndpoint)
+ if err != nil {
+ return nil, err
+ }
+
+ err = Authenticate(client, options)
+ if err != nil {
+ return nil, err
+ }
+ return client, nil
+}
+
+// Authenticate or re-authenticate against the most recent identity service supported at the
+// provided endpoint.
+func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+ versions := []*utils.Version{
+ &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
+ }
+
+ chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions)
+ if err != nil {
+ return err
+ }
+
+ switch chosen.ID {
+ case v20:
+ return v2auth(client, endpoint, options)
+ default:
+ // The switch statement must be out of date from the versions list.
+ return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
+ }
+}
+
+// AuthenticateV2 explicitly authenticates with v2 of the identity service.
+func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+ return v2auth(client, "", options)
+}
+
+func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
+ v2Client := NewIdentityV2(client)
+ if endpoint != "" {
+ v2Client.Endpoint = endpoint
+ }
+
+ result := tokens2.Create(v2Client, tokens2.WrapOptions(options))
+
+ token, err := result.ExtractToken()
+ if err != nil {
+ return err
+ }
+
+ catalog, err := result.ExtractServiceCatalog()
+ if err != nil {
+ return err
+ }
+
+ client.TokenID = token.ID
+ client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+ return os.V2EndpointURL(catalog, opts)
+ }
+
+ return nil
+}
+
+// NewIdentityV2 creates a ServiceClient that may be used to access the v2 identity service.
+func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
+ v2Endpoint := client.IdentityBase + "v2.0/"
+
+ return &gophercloud.ServiceClient{
+ Provider: client,
+ Endpoint: v2Endpoint,
+ }
+}
diff --git a/rackspace/client_test.go b/rackspace/client_test.go
new file mode 100644
index 0000000..73b1c88
--- /dev/null
+++ b/rackspace/client_test.go
@@ -0,0 +1,38 @@
+package rackspace
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedClientV2(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, `
+ {
+ "access": {
+ "token": {
+ "id": "01234567890",
+ "expires": "2014-10-01T10:00:00.000000Z"
+ },
+ "serviceCatalog": []
+ }
+ }
+ `)
+ })
+
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ APIKey: "09876543210",
+ IdentityEndpoint: th.Endpoint() + "v2.0/",
+ }
+ client, err := AuthenticatedClient(options)
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, "01234567890", client.TokenID)
+}
diff --git a/rackspace/identity/v2/extensions/delegate.go b/rackspace/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..fc547cd
--- /dev/null
+++ b/rackspace/identity/v2/extensions/delegate.go
@@ -0,0 +1,24 @@
+package extensions
+
+import (
+ "github.com/rackspace/gophercloud"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
+// elements into a slice of os.Extension structs.
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
+ return common.ExtractExtensions(page)
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+ return common.Get(c, alias)
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return common.List(c)
+}
diff --git a/rackspace/identity/v2/extensions/delegate_test.go b/rackspace/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..e30f794
--- /dev/null
+++ b/rackspace/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,39 @@
+package extensions
+
+import (
+ "testing"
+
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ common.HandleListExtensionsSuccessfully(t)
+
+ count := 0
+
+ err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, common.ExpectedExtensions, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ common.HandleGetExtensionSuccessfully(t)
+
+ actual, err := Get(fake.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, common.SingleExtension, actual)
+}
diff --git a/rackspace/identity/v2/tenants/delegate.go b/rackspace/identity/v2/tenants/delegate.go
new file mode 100644
index 0000000..6cdd0cf
--- /dev/null
+++ b/rackspace/identity/v2/tenants/delegate.go
@@ -0,0 +1,17 @@
+package tenants
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractTenants interprets a page of List results as a more usable slice of Tenant structs.
+func ExtractTenants(page pagination.Page) ([]os.Tenant, error) {
+ return os.ExtractTenants(page)
+}
+
+// List enumerates the tenants to which the current token grants access.
+func List(client *gophercloud.ServiceClient, opts *os.ListOpts) pagination.Pager {
+ return os.List(client, opts)
+}
diff --git a/rackspace/identity/v2/tenants/delegate_test.go b/rackspace/identity/v2/tenants/delegate_test.go
new file mode 100644
index 0000000..eccbfe2
--- /dev/null
+++ b/rackspace/identity/v2/tenants/delegate_test.go
@@ -0,0 +1,28 @@
+package tenants
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListTenants(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleListTenantsSuccessfully(t)
+
+ count := 0
+ err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ actual, err := ExtractTenants(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, os.ExpectedTenantSlice, actual)
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
diff --git a/rackspace/identity/v2/tokens/delegate.go b/rackspace/identity/v2/tokens/delegate.go
new file mode 100644
index 0000000..4f9885a
--- /dev/null
+++ b/rackspace/identity/v2/tokens/delegate.go
@@ -0,0 +1,60 @@
+package tokens
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+)
+
+var (
+ // ErrPasswordProvided is returned if both a password and an API key are provided to Create.
+ ErrPasswordProvided = errors.New("Please provide either a password or an API key.")
+)
+
+// AuthOptions wraps the OpenStack AuthOptions struct to be able to customize the request body
+// when API key authentication is used.
+type AuthOptions struct {
+ os.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+ return AuthOptions{AuthOptions: os.WrapOptions(original)}
+}
+
+// ToTokenCreateMap serializes an AuthOptions into a request body. If an API key is provided, it
+// will be used, otherwise
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+ if auth.APIKey == "" {
+ return auth.AuthOptions.ToTokenCreateMap()
+ }
+
+ // Verify that other required attributes are present.
+ if auth.Username == "" {
+ return nil, os.ErrUsernameRequired
+ }
+
+ authMap := make(map[string]interface{})
+
+ authMap["RAX-KSKEY:apiKeyCredentials"] = map[string]interface{}{
+ "username": auth.Username,
+ "apiKey": auth.APIKey,
+ }
+
+ if auth.TenantID != "" {
+ authMap["tenantId"] = auth.TenantID
+ }
+ if auth.TenantName != "" {
+ authMap["tenantName"] = auth.TenantName
+ }
+
+ return map[string]interface{}{"auth": authMap}, nil
+}
+
+// Create authenticates to Rackspace's identity service and attempts to acquire a Token. Rather
+// than interact with this service directly, users should generally call
+// rackspace.AuthenticatedClient().
+func Create(client *gophercloud.ServiceClient, auth AuthOptions) os.CreateResult {
+ return os.Create(client, auth)
+}
diff --git a/rackspace/identity/v2/tokens/delegate_test.go b/rackspace/identity/v2/tokens/delegate_test.go
new file mode 100644
index 0000000..6678ff4
--- /dev/null
+++ b/rackspace/identity/v2/tokens/delegate_test.go
@@ -0,0 +1,36 @@
+package tokens
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) os.CreateResult {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleTokenPost(t, requestJSON)
+
+ return Create(client.ServiceClient(), WrapOptions(options))
+}
+
+func TestCreateTokenWithAPIKey(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ APIKey: "1234567890abcdef",
+ }
+
+ os.IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "RAX-KSKEY:apiKeyCredentials": {
+ "username": "me",
+ "apiKey": "1234567890abcdef"
+ }
+ }
+ }
+ `))
+}
diff --git a/results.go b/results.go
index f7d526c..647ba46 100644
--- a/results.go
+++ b/results.go
@@ -10,3 +10,28 @@
// RFC3339Milli describes a time format used by API responses.
const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
+
+// Link represents a structure that enables paginated collections how to
+// traverse backward or forward. The "Rel" field is usually either "next".
+type Link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+}
+
+// ExtractNextURL attempts to extract the next URL from a JSON structure. It
+// follows the common structure of nesting back and next links.
+func ExtractNextURL(links []Link) (string, error) {
+ var url string
+
+ for _, l := range links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
diff --git a/script/acceptancetest b/script/acceptancetest
new file mode 100755
index 0000000..d8039ae
--- /dev/null
+++ b/script/acceptancetest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the acceptance tests.
+
+exec go test -tags 'acceptance fixtures' ./acceptance/... $@
diff --git a/scripts/create-environment.sh b/script/bootstrap
old mode 100644
new mode 100755
similarity index 100%
rename from scripts/create-environment.sh
rename to script/bootstrap
diff --git a/script/cibuild b/script/cibuild
new file mode 100755
index 0000000..1cb389e
--- /dev/null
+++ b/script/cibuild
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Test script to be invoked by Travis.
+
+exec script/unittest -v
diff --git a/script/test b/script/test
new file mode 100755
index 0000000..1e03dff
--- /dev/null
+++ b/script/test
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run all the tests.
+
+exec go test -tags 'acceptance fixtures' ./... $@
diff --git a/script/unittest b/script/unittest
new file mode 100755
index 0000000..d3440a9
--- /dev/null
+++ b/script/unittest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the unit tests.
+
+exec go test -tags fixtures ./... $@
diff --git a/scripts/test-all.sh b/scripts/test-all.sh
deleted file mode 100755
index 096736f..0000000
--- a/scripts/test-all.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash
-#
-# This script is responsible for executing all the acceptance tests found in
-# the acceptance/ directory.
-
-# Find where _this_ script is running from.
-SCRIPTS=$(dirname $0)
-SCRIPTS=$(cd $SCRIPTS; pwd)
-
-# Locate the acceptance test / examples directory.
-ACCEPTANCE=$(cd $SCRIPTS/../acceptance; pwd)
-
-# Go workspace path
-WS=$(cd $SCRIPTS/..; pwd)
-
-# In order to run Go code interactively, we need the GOPATH environment
-# to be set.
-if [ "x$GOPATH" == "x" ]; then
- export GOPATH=$WS
- echo "WARNING: You didn't have your GOPATH environment variable set."
- echo " I'm assuming $GOPATH as its value."
-fi
-
-# Run all acceptance tests sequentially.
-# If any test fails, we fail fast.
-LIBS=$(ls $ACCEPTANCE/lib*.go)
-for T in $(ls -1 $ACCEPTANCE/[0-9][0-9]*.go); do
- if ! [ -x $T ]; then
- CMD="go run $T $LIBS -quiet"
- echo "$CMD ..."
- if ! $CMD ; then
- echo "- FAILED. Try re-running w/out the -quiet option to see output."
- exit 1
- fi
- fi
-done
-
diff --git a/service_client_test.go b/service_client_test.go
new file mode 100644
index 0000000..84beb3f
--- /dev/null
+++ b/service_client_test.go
@@ -0,0 +1,14 @@
+package gophercloud
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestServiceURL(t *testing.T) {
+ c := &ServiceClient{Endpoint: "http://123.45.67.8/"}
+ expected := "http://123.45.67.8/more/parts/here"
+ actual := c.ServiceURL("more", "parts", "here")
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/util.go b/util.go
index c424759..1715458 100644
--- a/util.go
+++ b/util.go
@@ -2,6 +2,7 @@
import (
"fmt"
+ "strings"
"time"
)
@@ -20,3 +21,11 @@
}
return fmt.Errorf("Time out in WaitFor.")
}
+
+// NormalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
+func NormalizeURL(url string) string {
+ if !strings.HasSuffix(url, "/") {
+ return url + "/"
+ }
+ return url
+}
diff --git a/util_test.go b/util_test.go
new file mode 100644
index 0000000..6fbd920
--- /dev/null
+++ b/util_test.go
@@ -0,0 +1,21 @@
+package gophercloud
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitFor(t *testing.T) {
+ err := WaitFor(0, func() (bool, error) {
+ return true, nil
+ })
+ if err == nil {
+ t.Errorf("Expected error: 'Time out in WaitFor'")
+ }
+
+ err = WaitFor(5, func() (bool, error) {
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+}