OpenStack Client: Handle New Identity Endpoints

This commit modifies the openstack client to handle new identity
endpoints in the following ways:

1. Identity endpoints published with a valid URL path
(http://example.com/identity) are now parsed correctly.

If the endpoint has a version suffix (http://example.com/identity/v3),
the client will use /identity as the base and /identity/v3 as the
endpoint.

If the endpoint does not have a version suffix, both the base and the
endpoint will be set to /identity and further version discovery will
be done.

2. Version discovery can now handle version IDs other than v2.0 and v3.
If the Identity Service is publishing an ID of v3.8, Gophercloud will
recognize it as a valid result.

Related-PROD: PROD-24705 (PROD:24705)

Change-Id: I994e159d2bfd4f594eb16308a5a5ca76339206e5
diff --git a/openstack/client.go b/openstack/client.go
index b06a981..3db846e 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -4,6 +4,8 @@
 	"fmt"
 	"net/url"
 	"reflect"
+	"regexp"
+	"strings"
 
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
 	tokens2 "gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/identity/v2/tokens"
@@ -12,8 +14,13 @@
 )
 
 const (
-	v20 = "v2.0"
-	v30 = "v3.0"
+	// v2 represents Keystone v2.
+	// It should never increase beyond 2.0.
+	v2 = "v2.0"
+
+	// v3 represents Keystone v3.
+	// The version can be anything from v3 to v3.x.
+	v3 = "v3"
 )
 
 // NewClient prepares an unauthenticated ProviderClient instance.
@@ -25,24 +32,25 @@
 	if err != nil {
 		return nil, err
 	}
-	hadPath := u.Path != ""
-	u.Path, u.RawQuery, u.Fragment = "", "", ""
-	base := u.String()
+
+	u.RawQuery, u.Fragment = "", ""
+
+	var base string
+	versionRe := regexp.MustCompile("v[0-9.]+/?")
+	if version := versionRe.FindString(u.Path); version != "" {
+		base = strings.Replace(u.String(), version, "", -1)
+	} else {
+		base = u.String()
+	}
 
 	endpoint = gophercloud.NormalizeURL(endpoint)
 	base = gophercloud.NormalizeURL(base)
 
-	if hadPath {
-		return &gophercloud.ProviderClient{
-			IdentityBase:     base,
-			IdentityEndpoint: endpoint,
-		}, nil
-	}
-
 	return &gophercloud.ProviderClient{
 		IdentityBase:     base,
-		IdentityEndpoint: "",
+		IdentityEndpoint: endpoint,
 	}, nil
+
 }
 
 // AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and
@@ -65,8 +73,8 @@
 // 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{
-		{ID: v20, Priority: 20, Suffix: "/v2.0/"},
-		{ID: v30, Priority: 30, Suffix: "/v3/"},
+		{ID: v2, Priority: 20, Suffix: "/v2.0/"},
+		{ID: v3, Priority: 30, Suffix: "/v3/"},
 	}
 
 	chosen, endpoint, err := utils.ChooseVersion(client, versions)
@@ -75,9 +83,9 @@
 	}
 
 	switch chosen.ID {
-	case v20:
+	case v2:
 		return v2auth(client, endpoint, options, gophercloud.EndpointOpts{})
-	case v30:
+	case v3:
 		return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{})
 	default:
 		// The switch statement must be out of date from the versions list.