Merge pull request #224 from jrperritt/v0.2.0-consistency-checks
V0.2.0 consistency checks
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/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/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 e98e5d6..4827072 100755
--- a/openstack/common/extensions/results.go
+++ b/openstack/common/extensions/results.go
@@ -29,12 +29,12 @@
// 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.
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/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/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/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/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/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
+}