Moving calls to client helper while I'm at it
diff --git a/_site/openstack/utils/choose_version.go b/_site/openstack/utils/choose_version.go
new file mode 100644
index 0000000..753f8f8
--- /dev/null
+++ b/_site/openstack/utils/choose_version.go
@@ -0,0 +1,114 @@
+package utils
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/racker/perigee"
+)
+
+// Version is a supported API version, corresponding to a vN package within the appropriate service.
+type Version struct {
+ ID string
+ Suffix string
+ Priority int
+}
+
+var goodStatus = map[string]bool{
+ "current": true,
+ "supported": true,
+ "stable": true,
+}
+
+// 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(identityBase string, identityEndpoint string, recognized []*Version) (*Version, string, error) {
+ type linkResp struct {
+ Href string `json:"href"`
+ Rel string `json:"rel"`
+ }
+
+ type valueResp struct {
+ ID string `json:"id"`
+ Status string `json:"status"`
+ Links []linkResp `json:"links"`
+ }
+
+ type versionsResp struct {
+ Values []valueResp `json:"values"`
+ }
+
+ type response struct {
+ Versions versionsResp `json:"versions"`
+ }
+
+ normalize := func(endpoint string) string {
+ if !strings.HasSuffix(endpoint, "/") {
+ return endpoint + "/"
+ }
+ return endpoint
+ }
+ 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", identityBase, perigee.Options{
+ Results: &resp,
+ OkCodes: []int{200, 300},
+ })
+
+ if err != nil {
+ return nil, "", err
+ }
+
+ byID := make(map[string]*Version)
+ for _, version := range recognized {
+ byID[version.ID] = version
+ }
+
+ var highest *Version
+ var endpoint string
+
+ for _, value := range resp.Versions.Values {
+ href := ""
+ for _, link := range value.Links {
+ if link.Rel == "self" {
+ href = normalize(link.Href)
+ }
+ }
+
+ if matching, ok := byID[value.ID]; ok {
+ // Prefer a version that exactly matches the provided endpoint.
+ if href == identityEndpoint {
+ if href == "" {
+ return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, identityBase)
+ }
+ return matching, href, nil
+ }
+
+ // Otherwise, find the highest-priority version with a whitelisted status.
+ if goodStatus[strings.ToLower(value.Status)] {
+ if highest == nil || matching.Priority > highest.Priority {
+ highest = matching
+ endpoint = href
+ }
+ }
+ }
+ }
+
+ if highest == nil {
+ 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, identityBase)
+ }
+
+ return highest, endpoint, nil
+}
diff --git a/_site/openstack/utils/choose_version_test.go b/_site/openstack/utils/choose_version_test.go
new file mode 100644
index 0000000..9552696
--- /dev/null
+++ b/_site/openstack/utils/choose_version_test.go
@@ -0,0 +1,105 @@
+package utils
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+func setupVersionHandler() {
+ testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, `
+ {
+ "versions": {
+ "values": [
+ {
+ "status": "stable",
+ "id": "v3.0",
+ "links": [
+ { "href": "%s/v3.0", "rel": "self" }
+ ]
+ },
+ {
+ "status": "stable",
+ "id": "v2.0",
+ "links": [
+ { "href": "%s/v2.0", "rel": "self" }
+ ]
+ }
+ ]
+ }
+ }
+ `, testhelper.Server.URL, testhelper.Server.URL)
+ })
+}
+
+func TestChooseVersion(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ setupVersionHandler()
+
+ 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})
+
+ if err != nil {
+ t.Fatalf("Unexpected error from ChooseVersion: %v", err)
+ }
+
+ if v != v3 {
+ t.Errorf("Expected %#v to win, but %#v did instead", v3, v)
+ }
+
+ expected := testhelper.Endpoint() + "v3.0/"
+ if endpoint != expected {
+ t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint)
+ }
+}
+
+func TestChooseVersionOpinionatedLink(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ setupVersionHandler()
+
+ v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "nope"}
+ v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "northis"}
+
+ 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)
+ }
+
+ 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)
+ }
+}
diff --git a/_site/openstack/utils/utils.go b/_site/openstack/utils/utils.go
new file mode 100644
index 0000000..1d09d9e
--- /dev/null
+++ b/_site/openstack/utils/utils.go
@@ -0,0 +1,73 @@
+// Package utils contains utilities which eases working with Gophercloud's OpenStack APIs.
+package utils
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/rackspace/gophercloud"
+)
+
+var nilOptions = gophercloud.AuthOptions{}
+
+// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the required OS_AUTH_URL, OS_USERNAME, or OS_PASSWORD
+// environment variables, respectively, remain undefined. See the AuthOptions() function for more details.
+var (
+ ErrNoAuthURL = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.")
+ ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.")
+ ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD needs to be set.")
+)
+
+// AuthOptions fills out an identity.AuthOptions structure with the settings found on the various OpenStack
+// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
+// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must
+// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional.
+func AuthOptions() (gophercloud.AuthOptions, error) {
+ authURL := os.Getenv("OS_AUTH_URL")
+ username := os.Getenv("OS_USERNAME")
+ userID := os.Getenv("OS_USERID")
+ password := os.Getenv("OS_PASSWORD")
+ tenantID := os.Getenv("OS_TENANT_ID")
+ tenantName := os.Getenv("OS_TENANT_NAME")
+ domainID := os.Getenv("OS_DOMAIN_ID")
+ domainName := os.Getenv("OS_DOMAIN_NAME")
+
+ if authURL == "" {
+ return nilOptions, ErrNoAuthURL
+ }
+
+ if username == "" && userID == "" {
+ return nilOptions, ErrNoUsername
+ }
+
+ if password == "" {
+ return nilOptions, ErrNoPassword
+ }
+
+ ao := gophercloud.AuthOptions{
+ IdentityEndpoint: authURL,
+ UserID: userID,
+ Username: username,
+ Password: password,
+ TenantID: tenantID,
+ TenantName: tenantName,
+ DomainID: domainID,
+ DomainName: domainName,
+ }
+
+ return ao, nil
+}
+
+// BuildQuery constructs the query section of a URI from a map.
+func BuildQuery(params map[string]string) string {
+ if len(params) == 0 {
+ return ""
+ }
+
+ query := "?"
+ for k, v := range params {
+ query += k + "=" + v + "&"
+ }
+ query = query[:len(query)-1]
+ return query
+}