Ookay, last reshuffle.
* openstack.NewClient() normalizes the identity endpoint with a trailing slash, and sets base and endpoint.
* utils.ChooseVersion() checks suffixes first to short-circuit actual version calls.
* gophercloud.ProviderClient distinguishes between the root of all identity services (IdentityBase)
and the endpoint of the requested auth service (IdentityEndpoint).
diff --git a/openstack/client.go b/openstack/client.go
index d244053..7e9b628 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -2,6 +2,7 @@
import (
"fmt"
+ "net/url"
"strings"
"github.com/rackspace/gophercloud"
@@ -22,11 +23,32 @@
// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly,
// for example.
func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
+ u, err := url.Parse(endpoint)
+ if err != nil {
+ return nil, err
+ }
+ hadPath := u.Path != ""
+ u.Path, u.RawQuery, u.Fragment = "", "", ""
+ base := u.String()
+
if !strings.HasSuffix(endpoint, "/") {
endpoint = endpoint + "/"
}
+ if !strings.HasSuffix(base, "/") {
+ base = base + "/"
+ }
- return &gophercloud.ProviderClient{IdentityEndpoint: endpoint}, nil
+ if hadPath {
+ return &gophercloud.ProviderClient{
+ IdentityBase: base,
+ IdentityEndpoint: endpoint,
+ }, nil
+ }
+
+ return &gophercloud.ProviderClient{
+ IdentityBase: base,
+ IdentityEndpoint: "",
+ }, nil
}
// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and
@@ -49,61 +71,55 @@
// 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},
- &utils.Version{ID: v30, Priority: 30},
+ &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
+ &utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"},
}
- chosen, endpoint, err := utils.ChooseVersion(client.IdentityEndpoint, versions)
+ chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions)
if err != nil {
return err
}
switch chosen.ID {
case v20:
- v2Client := NewIdentityV2(client)
- v2Client.Endpoint = endpoint
-
- result, err := identity2.Authenticate(v2Client, options)
- if err != nil {
- return err
- }
-
- token, err := identity2.GetToken(result)
- if err != nil {
- return err
- }
-
- client.TokenID = token.ID
- client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
- return v2endpointLocator(result, opts)
- }
-
- return nil
+ return v2auth(client, endpoint, options)
case v30:
- // Override the generated service endpoint with the one returned by the version endpoint.
- v3Client := NewIdentityV3(client)
- v3Client.Endpoint = endpoint
-
- result, err := tokens3.Create(v3Client, options, nil)
- if err != nil {
- return err
- }
-
- client.TokenID, err = result.TokenID()
- if err != nil {
- return err
- }
- client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
- return v3endpointLocator(v3Client, opts)
- }
-
- return nil
+ return v3auth(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 against the identity v2 endpoint.
+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, err := identity2.Authenticate(v2Client, options)
+ if err != nil {
+ return err
+ }
+
+ token, err := identity2.GetToken(result)
+ if err != nil {
+ return err
+ }
+
+ client.TokenID = token.ID
+ client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+ return v2endpointLocator(result, opts)
+ }
+
+ return nil
+}
+
func v2endpointLocator(authResults identity2.AuthResults, opts gophercloud.EndpointOpts) (string, error) {
catalog, err := identity2.GetServiceCatalog(authResults)
if err != nil {
@@ -150,6 +166,34 @@
return "", gophercloud.ErrEndpointNotFound
}
+// AuthenticateV3 explicitly authenticates against the identity v3 service.
+func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+ return v3auth(client, "", options)
+}
+
+func v3auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
+ // Override the generated service endpoint with the one returned by the version endpoint.
+ v3Client := NewIdentityV3(client)
+ if endpoint != "" {
+ v3Client.Endpoint = endpoint
+ }
+
+ result, err := tokens3.Create(v3Client, options, nil)
+ if err != nil {
+ return err
+ }
+
+ client.TokenID, err = result.TokenID()
+ if err != nil {
+ return err
+ }
+ client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+ return v3endpointLocator(v3Client, opts)
+ }
+
+ return nil
+}
+
func v3endpointLocator(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) {
// Transform URLType into an Interface.
var endpointInterface = endpoints3.InterfacePublic
@@ -233,7 +277,7 @@
// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service.
func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
- v2Endpoint := client.IdentityEndpoint + "v2.0/"
+ v2Endpoint := client.IdentityBase + "v2.0/"
return &gophercloud.ServiceClient{
Provider: client,
@@ -243,7 +287,7 @@
// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service.
func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
- v3Endpoint := client.IdentityEndpoint + "v3/"
+ v3Endpoint := client.IdentityBase + "v3/"
return &gophercloud.ServiceClient{
Provider: client,
diff --git a/openstack/utils/choose_version.go b/openstack/utils/choose_version.go
index 402a870..753f8f8 100644
--- a/openstack/utils/choose_version.go
+++ b/openstack/utils/choose_version.go
@@ -2,7 +2,6 @@
import (
"fmt"
- "net/url"
"strings"
"github.com/racker/perigee"
@@ -11,6 +10,7 @@
// Version is a supported API version, corresponding to a vN package within the appropriate service.
type Version struct {
ID string
+ Suffix string
Priority int
}
@@ -23,7 +23,7 @@
// ChooseVersion queries the base endpoint of a API to choose the most recent non-experimental alternative from a service's
// published versions.
// It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint.
-func ChooseVersion(identityEndpoint string, recognized []*Version) (*Version, string, error) {
+func ChooseVersion(identityBase string, identityEndpoint string, recognized []*Version) (*Version, string, error) {
type linkResp struct {
Href string `json:"href"`
Rel string `json:"rel"`
@@ -43,16 +43,23 @@
Versions versionsResp `json:"versions"`
}
- // Normalize the identity endpoint that's provided by trimming any path, query or fragment from the URL.
- u, err := url.Parse(identityEndpoint)
- if err != nil {
- return nil, "", err
+ normalize := func(endpoint string) string {
+ if !strings.HasSuffix(endpoint, "/") {
+ return endpoint + "/"
+ }
+ return endpoint
}
- u.Path, u.RawQuery, u.Fragment = "", "", ""
- normalized := u.String()
+ identityEndpoint = normalize(identityEndpoint)
+
+ // If a full endpoint is specified, check version suffixes for a match first.
+ for _, v := range recognized {
+ if strings.HasSuffix(identityEndpoint, v.Suffix) {
+ return v, identityEndpoint, nil
+ }
+ }
var resp response
- _, err = perigee.Request("GET", normalized, perigee.Options{
+ _, err := perigee.Request("GET", identityBase, perigee.Options{
Results: &resp,
OkCodes: []int{200, 300},
})
@@ -69,14 +76,6 @@
var highest *Version
var endpoint string
- normalize := func(endpoint string) string {
- if !strings.HasSuffix(endpoint, "/") {
- return endpoint + "/"
- }
- return endpoint
- }
- normalizedGiven := normalize(identityEndpoint)
-
for _, value := range resp.Versions.Values {
href := ""
for _, link := range value.Links {
@@ -87,9 +86,9 @@
if matching, ok := byID[value.ID]; ok {
// Prefer a version that exactly matches the provided endpoint.
- if href == normalizedGiven {
+ if href == identityEndpoint {
if href == "" {
- return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, normalized)
+ return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, identityBase)
}
return matching, href, nil
}
@@ -105,10 +104,10 @@
}
if highest == nil {
- return nil, "", fmt.Errorf("No supported version available from endpoint %s", normalized)
+ return nil, "", fmt.Errorf("No supported version available from endpoint %s", identityBase)
}
if endpoint == "" {
- return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, normalized)
+ return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, identityBase)
}
return highest, endpoint, nil
diff --git a/openstack/utils/choose_version_test.go b/openstack/utils/choose_version_test.go
index b724ee8..9552696 100644
--- a/openstack/utils/choose_version_test.go
+++ b/openstack/utils/choose_version_test.go
@@ -40,10 +40,10 @@
defer testhelper.TeardownHTTP()
setupVersionHandler()
- v2 := &Version{ID: "v2.0", Priority: 2}
- v3 := &Version{ID: "v3.0", Priority: 3}
+ v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "blarg"}
+ v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "hargl"}
- v, endpoint, err := ChooseVersion(testhelper.Endpoint(), []*Version{v2, v3})
+ v, endpoint, err := ChooseVersion(testhelper.Endpoint(), "", []*Version{v2, v3})
if err != nil {
t.Fatalf("Unexpected error from ChooseVersion: %v", err)
@@ -64,10 +64,32 @@
defer testhelper.TeardownHTTP()
setupVersionHandler()
- v2 := &Version{ID: "v2.0", Priority: 2}
- v3 := &Version{ID: "v3.0", Priority: 3}
+ v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "nope"}
+ v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "northis"}
- v, endpoint, err := ChooseVersion(testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
+ v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
+ if err != nil {
+ t.Fatalf("Unexpected error from ChooseVersion: %v", err)
+ }
+
+ if v != v2 {
+ t.Errorf("Expected %#v to win, but %#v did instead", v2, v)
+ }
+
+ expected := testhelper.Endpoint() + "v2.0/"
+ if endpoint != expected {
+ t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint)
+ }
+}
+
+func TestChooseVersionFromSuffix(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "/v2.0/"}
+ v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "/v3.0/"}
+
+ v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
if err != nil {
t.Fatalf("Unexpected error from ChooseVersion: %v", err)
}
diff --git a/openstack/utils/client.go b/openstack/utils/client.go
index 7e3aa12..f8a6c57 100644
--- a/openstack/utils/client.go
+++ b/openstack/utils/client.go
@@ -47,7 +47,7 @@
Options: ao,
}
- c := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint}
+ c := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint + "/"}
ar, err := identity.Authenticate(c, ao)
if err != nil {
return client, err
diff --git a/provider_client.go b/provider_client.go
index 971276e..2be665e 100644
--- a/provider_client.go
+++ b/provider_client.go
@@ -6,9 +6,14 @@
// providing whatever authentication credentials are required.
type ProviderClient struct {
- // IdentityEndpoint is the front door to an openstack provider.
+ // IdentityBase is the front door to an openstack provider.
// Generally this will be populated when you authenticate.
// It should be the *root* resource of the identity service, not of a specific identity version.
+ IdentityBase string
+
+ // IdentityEndpoint is the originally requested identity endpoint.
+ // This may be a specific version of the identity service, in which case that endpoint is used rather than querying the
+ // version-negotiation endpoint.
IdentityEndpoint string
// TokenID is the most recently valid token issued.