Merge pull request #408 from ggiamarchi/keystone-v3-auth
Keystone v3 service catalog + auth scope
diff --git a/openstack/client.go b/openstack/client.go
index aaf940b..1193b19 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -133,10 +133,36 @@
v3Client.Endpoint = endpoint
}
- token, err := tokens3.Create(v3Client, options, nil).Extract()
+ var scope *tokens3.Scope
+ if options.TenantID != "" {
+ scope = &tokens3.Scope{
+ ProjectID: options.TenantID,
+ }
+ options.TenantID = ""
+ options.TenantName = ""
+ } else {
+ if options.TenantName != "" {
+ scope = &tokens3.Scope{
+ ProjectName: options.TenantName,
+ DomainID: options.DomainID,
+ DomainName: options.DomainName,
+ }
+ options.TenantName = ""
+ }
+ }
+
+ result := tokens3.Create(v3Client, options, scope)
+
+ token, err := result.ExtractToken()
if err != nil {
return err
}
+
+ catalog, err := result.ExtractServiceCatalog()
+ if err != nil {
+ return err
+ }
+
client.TokenID = token.ID
if options.AllowReauth {
@@ -145,7 +171,7 @@
}
}
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
- return V3EndpointURL(v3Client, opts)
+ return V3EndpointURL(catalog, opts)
}
return nil
diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go
index 5a311e4..29d02c4 100644
--- a/openstack/endpoint_location.go
+++ b/openstack/endpoint_location.go
@@ -5,9 +5,7 @@
"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"
+ tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
)
// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired
@@ -52,73 +50,42 @@
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,
+// V3EndpointURL discovers the endpoint URL for a specific service from a Catalog acquired
+// during the v3 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 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)
+func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
+ // Extract Endpoints from the catalog entries that match the requested Type, Interface,
+ // Name if provided, and Region if provided.
+ var endpoints = make([]tokens3.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.Availability != gophercloud.AvailabilityAdmin &&
+ opts.Availability != gophercloud.AvailabilityPublic &&
+ opts.Availability != gophercloud.AvailabilityInternal {
+ return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
+ }
+ if (opts.Availability == gophercloud.Availability(endpoint.Interface)) &&
+ (opts.Region == "" || endpoint.Region == opts.Region) {
+ endpoints = append(endpoints, endpoint)
+ }
}
}
-
- 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
- }
+ // Report an error if the options were ambiguous.
if len(endpoints) > 1 {
return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
}
- endpoint := endpoints[0]
- return gophercloud.NormalizeURL(endpoint.URL), nil
+ // Extract the URL from the matching Endpoint.
+ for _, endpoint := range endpoints {
+ return gophercloud.NormalizeURL(endpoint.URL), nil
+ }
+
+ // Report an error if there were no matching endpoints.
+ return "", gophercloud.ErrEndpointNotFound
}
diff --git a/openstack/endpoint_location_test.go b/openstack/endpoint_location_test.go
index 4e0569a..8e65918 100644
--- a/openstack/endpoint_location_test.go
+++ b/openstack/endpoint_location_test.go
@@ -1,15 +1,13 @@
package openstack
import (
- "fmt"
- "net/http"
"strings"
"testing"
"github.com/rackspace/gophercloud"
tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+ tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
th "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
)
// Service catalog fixtures take too much vertical space!
@@ -107,119 +105,124 @@
Region: "same",
Availability: "wat",
})
- th.CheckEquals(t, err.Error(), "Unexpected availability in endpoint query: wat")
+ th.CheckEquals(t, "Unexpected availability in endpoint query: wat", err.Error())
}
-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
+var catalog3 = tokens3.ServiceCatalog{
+ Entries: []tokens3.CatalogEntry{
+ tokens3.CatalogEntry{
+ Type: "same",
+ Name: "same",
+ Endpoints: []tokens3.Endpoint{
+ tokens3.Endpoint{
+ ID: "1",
+ Region: "same",
+ Interface: "public",
+ URL: "https://public.correct.com/",
},
- "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
- }
- }
- `)
- })
+ tokens3.Endpoint{
+ ID: "2",
+ Region: "same",
+ Interface: "admin",
+ URL: "https://admin.correct.com/",
+ },
+ tokens3.Endpoint{
+ ID: "3",
+ Region: "same",
+ Interface: "internal",
+ URL: "https://internal.correct.com/",
+ },
+ tokens3.Endpoint{
+ ID: "4",
+ Region: "different",
+ Interface: "public",
+ URL: "https://badregion.com/",
+ },
+ },
+ },
+ tokens3.CatalogEntry{
+ Type: "same",
+ Name: "different",
+ Endpoints: []tokens3.Endpoint{
+ tokens3.Endpoint{
+ ID: "5",
+ Region: "same",
+ Interface: "public",
+ URL: "https://badname.com/",
+ },
+ tokens3.Endpoint{
+ ID: "6",
+ Region: "different",
+ Interface: "public",
+ URL: "https://badname.com/+badregion",
+ },
+ },
+ },
+ tokens3.CatalogEntry{
+ Type: "different",
+ Name: "different",
+ Endpoints: []tokens3.Endpoint{
+ tokens3.Endpoint{
+ ID: "7",
+ Region: "same",
+ Interface: "public",
+ URL: "https://badtype.com/+badname",
+ },
+ tokens3.Endpoint{
+ ID: "8",
+ Region: "different",
+ Interface: "public",
+ URL: "https://badtype.com/+badregion+badname",
+ },
+ },
+ },
+ },
}
func TestV3EndpointExact(t *testing.T) {
- th.SetupHTTP()
- defer th.TeardownHTTP()
- setupV3Responses(t)
+ expectedURLs := map[gophercloud.Availability]string{
+ gophercloud.AvailabilityPublic: "https://public.correct.com/",
+ gophercloud.AvailabilityAdmin: "https://admin.correct.com/",
+ gophercloud.AvailabilityInternal: "https://internal.correct.com/",
+ }
- actual, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{
+ for availability, expected := range expectedURLs {
+ actual, err := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{
+ Type: "same",
+ Name: "same",
+ Region: "same",
+ Availability: availability,
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, expected, actual)
+ }
+}
+
+func TestV3EndpointNone(t *testing.T) {
+ _, err := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{
+ Type: "nope",
+ Availability: gophercloud.AvailabilityPublic,
+ })
+ th.CheckEquals(t, gophercloud.ErrEndpointNotFound, err)
+}
+
+func TestV3EndpointMultiple(t *testing.T) {
+ _, err := V3EndpointURL(&catalog3, 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 TestV3EndpointBadAvailability(t *testing.T) {
+ _, err := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{
Type: "same",
Name: "same",
Region: "same",
- Availability: gophercloud.AvailabilityPublic,
+ Availability: "wat",
})
- 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)
+ th.CheckEquals(t, "Unexpected availability in endpoint query: wat", err.Error())
}
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index d1fff4c..d134f7d 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -7,13 +7,58 @@
"github.com/rackspace/gophercloud"
)
+// Endpoint represents a single API endpoint offered by a service.
+// It matches either a public, internal or admin URL.
+// If supported, it contains a region specifier, again if provided.
+// The significance of the Region field will depend upon your provider.
+type Endpoint struct {
+ ID string `mapstructure:"id"`
+ Region string `mapstructure:"region"`
+ Interface string `mapstructure:"interface"`
+ URL string `mapstructure:"url"`
+}
+
+// CatalogEntry provides a type-safe interface to an Identity API V3 service catalog listing.
+// Each class of service, such as cloud DNS or block storage services, could have multiple
+// CatalogEntry representing it (one by interface type, e.g public, admin or internal).
+//
+// Note: when looking for the desired service, try, whenever possible, to key off the type field.
+// Otherwise, you'll tie the representation of the service to a specific provider.
+type CatalogEntry struct {
+
+ // Service ID
+ ID string `mapstructure:"id"`
+
+ // Name will contain the provider-specified name for the service.
+ Name string `mapstructure:"name"`
+
+ // Type will contain a type string if OpenStack defines a type for the service.
+ // Otherwise, for provider-specific services, the provider may assign their own type strings.
+ Type string `mapstructure:"type"`
+
+ // Endpoints will let the caller iterate over all the different endpoints that may exist for
+ // the service.
+ Endpoints []Endpoint `mapstructure:"endpoints"`
+}
+
+// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
+type ServiceCatalog struct {
+ Entries []CatalogEntry
+}
+
// commonResult is the deferred result of a Create or a Get call.
type commonResult struct {
gophercloud.Result
}
-// Extract interprets a commonResult as a Token.
+// Extract is a shortcut for ExtractToken.
+// This function is deprecated and still present for backward compatibility.
func (r commonResult) Extract() (*Token, error) {
+ return r.ExtractToken()
+}
+
+// ExtractToken interprets a commonResult as a Token.
+func (r commonResult) ExtractToken() (*Token, error) {
if r.Err != nil {
return nil, r.Err
}
@@ -40,7 +85,28 @@
return &token, err
}
-// CreateResult is the deferred response from a Create call.
+// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
+func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
+ if result.Err != nil {
+ return nil, result.Err
+ }
+
+ var response struct {
+ Token struct {
+ Entries []CatalogEntry `mapstructure:"catalog"`
+ } `mapstructure:"token"`
+ }
+
+ err := mapstructure.Decode(result.Body, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ return &ServiceCatalog{Entries: response.Token.Entries}, nil
+}
+
+// CreateResult defers the interpretation of a created token.
+// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
type CreateResult struct {
commonResult
}