Merge pull request #115 from rackspace/gh-112-openstack-env-vars
Provide API for building AuthOptions from env vars.
diff --git a/acceptance/18-osutil-authentication.go b/acceptance/18-osutil-authentication.go
new file mode 100644
index 0000000..936f376
--- /dev/null
+++ b/acceptance/18-osutil-authentication.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/osutil"
+)
+
+func main() {
+ provider, authOptions, err := osutil.AuthOptions()
+ if err != nil {
+ panic(err)
+ }
+ _, err = gophercloud.Authenticate(provider, authOptions)
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/osutil/auth.go b/osutil/auth.go
new file mode 100644
index 0000000..60a5f0b
--- /dev/null
+++ b/osutil/auth.go
@@ -0,0 +1,59 @@
+package osutil
+
+import (
+ "fmt"
+ "github.com/rackspace/gophercloud"
+ "os"
+)
+
+var (
+ nilOptions = gophercloud.AuthOptions{}
+
+ // ErrNoAuthUrl errors occur when the value of the OS_AUTH_URL environment variable cannot be determined.
+ ErrNoAuthUrl = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.")
+
+ // ErrNoUsername errors occur when the value of the OS_USERNAME environment variable cannot be determined.
+ ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.")
+
+ // ErrNoPassword errors occur when the value of the OS_PASSWORD environment variable cannot be determined.
+ ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD or OS_API_KEY needs to be set.")
+)
+
+// AuthOptions fills out a gophercloud.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.
+//
+// The value of OS_AUTH_URL will be returned directly to the caller, for subsequent use in
+// gophercloud.Authenticate()'s Provider parameter. This function will not interpret the value of OS_AUTH_URL,
+// so as a convenient extention, you may set OS_AUTH_URL to, e.g., "rackspace-uk", or any other Gophercloud-recognized
+// provider shortcuts. For broad compatibility, especially with local installations, you should probably
+// avoid the temptation to do this.
+func AuthOptions() (string, gophercloud.AuthOptions, error) {
+ provider := os.Getenv("OS_AUTH_URL")
+ username := os.Getenv("OS_USERNAME")
+ password := os.Getenv("OS_PASSWORD")
+ tenantId := os.Getenv("OS_TENANT_ID")
+ tenantName := os.Getenv("OS_TENANT_NAME")
+
+ if provider == "" {
+ return "", nilOptions, ErrNoAuthUrl
+ }
+
+ if username == "" {
+ return "", nilOptions, ErrNoUsername
+ }
+
+ if password == "" && apiKey == "" {
+ return "", nilOptions, ErrNoPassword
+ }
+
+ ao := gophercloud.AuthOptions{
+ Username: username,
+ Password: password,
+ TenantId: tenantId,
+ TenantName: tenantName,
+ }
+
+ return provider, ao, nil
+}
diff --git a/osutil/region.go b/osutil/region.go
new file mode 100644
index 0000000..f7df507
--- /dev/null
+++ b/osutil/region.go
@@ -0,0 +1,9 @@
+package osutil
+
+import "os"
+
+// Region provides a means of querying the OS_REGION_NAME environment variable.
+// At present, you may also use os.Getenv("OS_REGION_NAME") as well.
+func Region() string {
+ return os.Getenv("OS_REGION_NAME")
+}
diff --git a/service_catalog.go b/service_catalog.go
index a439935..e6cf4a0 100644
--- a/service_catalog.go
+++ b/service_catalog.go
@@ -1,32 +1,36 @@
package gophercloud
import (
+ "os"
"strings"
)
// ApiCriteria provides one or more criteria for the SDK to look for appropriate endpoints.
// Fields left unspecified or otherwise set to their zero-values are assumed to not be
// relevant, and do not participate in the endpoint search.
+//
+// Name specifies the desired service catalog entry name.
+// Type specifies the desired service catalog entry type.
+// Region specifies the desired endpoint region.
+// If unset, Gophercloud will try to use the region set in the
+// OS_REGION_NAME environment variable. If that's not set,
+// region comparison will not occur. If OS_REGION_NAME is set
+// and IgnoreEnvVars is also set, OS_REGION_NAME will be ignored.
+// VersionId specifies the desired version of the endpoint.
+// Note that this field is matched exactly, and is (at present)
+// opaque to Gophercloud. Thus, requesting a version 2
+// endpoint will _not_ match a version 3 endpoint.
+// The UrlChoice field inidicates whether or not gophercloud
+// should use the public or internal endpoint URL if a
+// candidate endpoint is found.
+// IgnoreEnvVars instructs Gophercloud to ignore helpful environment variables.
type ApiCriteria struct {
- // Name specifies the desired service catalog entry name.
- Name string
-
- // Type specifies the desired service catalog entry type.
- Type string
-
- // Region specifies the desired endpoint region.
- Region string
-
- // VersionId specifies the desired version of the endpoint.
- // Note that this field is matched exactly, and is (at present)
- // opaque to Gophercloud. Thus, requesting a version 2
- // endpoint will _not_ match a version 3 endpoint.
- VersionId string
-
- // The UrlChoice field inidicates whether or not gophercloud
- // should use the public or internal endpoint URL if a
- // candidate endpoint is found.
- UrlChoice int
+ Name string
+ Type string
+ Region string
+ VersionId string
+ UrlChoice int
+ IgnoreEnvVars bool
}
// The choices available for UrlChoice. See the ApiCriteria structure for details.
@@ -42,6 +46,9 @@
// set to "").
func FindFirstEndpointByCriteria(entries []CatalogEntry, ac ApiCriteria) EntryEndpoint {
rgn := strings.ToUpper(ac.Region)
+ if (rgn == "") && !ac.IgnoreEnvVars {
+ rgn = os.Getenv("OS_REGION_NAME")
+ }
for _, entry := range entries {
if (ac.Name != "") && (ac.Name != entry.Name) {
@@ -53,7 +60,7 @@
}
for _, endpoint := range entry.Endpoints {
- if (ac.Region != "") && (rgn != strings.ToUpper(endpoint.Region)) {
+ if (rgn != "") && (rgn != strings.ToUpper(endpoint.Region)) {
continue
}
diff --git a/service_catalog_test.go b/service_catalog_test.go
index 52e2388..b78f01f 100644
--- a/service_catalog_test.go
+++ b/service_catalog_test.go
@@ -1,55 +1,68 @@
package gophercloud
import (
+ "os"
"testing"
)
-func TestFindFirstEndpointByCriteria(t *testing.T) {
- endpoint := FindFirstEndpointByCriteria([]CatalogEntry{}, ApiCriteria{Name: "test"})
- if endpoint.PublicURL != "" {
- t.Error("Not expecting to find anything in an empty service catalog.")
- return
- }
+// TestFFEBCViaEnvVariable exercises only those calls where a region
+// parameter is required, but is provided by an environment variable.
+func TestFFEBCViaEnvVariable(t *testing.T) {
+ changeRegion("RGN")
- endpoint = FindFirstEndpointByCriteria(
- []CatalogEntry{
- {Name: "test"},
- },
- ApiCriteria{Name: "test"},
- )
- if endpoint.PublicURL != "" {
- t.Error("Even though we have a matching entry, no endpoints exist")
- return
- }
-
- endpoint = FindFirstEndpointByCriteria(
+ endpoint := FindFirstEndpointByCriteria(
catalog("test", "compute", "http://localhost", "", ""),
ApiCriteria{Name: "test"},
)
- if endpoint.PublicURL != "http://localhost" {
- t.Error("Looking for an endpoint by name but without region or version ID should match first entry endpoint.")
+ if endpoint.PublicURL != "" {
+ t.Error("If provided, the Region qualifier must exclude endpoints with missing or mismatching regions.")
return
}
endpoint = FindFirstEndpointByCriteria(
- catalog("test", "compute", "http://localhost", "", ""),
- ApiCriteria{Type: "compute"},
+ catalog("test", "compute", "http://localhost", "rgn", ""),
+ ApiCriteria{Name: "test"},
)
if endpoint.PublicURL != "http://localhost" {
- t.Error("Looking for an endpoint by type but without region or version ID should match first entry endpoint.")
+ t.Error("Regions are case insensitive.")
return
}
endpoint = FindFirstEndpointByCriteria(
- catalog("test", "compute", "http://localhost", "", ""),
- ApiCriteria{Type: "identity"},
+ catalog("test", "compute", "http://localhost", "rgn", ""),
+ ApiCriteria{Name: "test", VersionId: "2"},
)
if endpoint.PublicURL != "" {
- t.Error("Returned mismatched type.")
+ t.Error("Missing version ID means no match.")
return
}
endpoint = FindFirstEndpointByCriteria(
+ catalog("test", "compute", "http://localhost", "rgn", "3"),
+ ApiCriteria{Name: "test", VersionId: "2"},
+ )
+ if endpoint.PublicURL != "" {
+ t.Error("Mismatched version ID means no match.")
+ return
+ }
+
+ endpoint = FindFirstEndpointByCriteria(
+ catalog("test", "compute", "http://localhost", "rgn", "2"),
+ ApiCriteria{Name: "test", VersionId: "2"},
+ )
+ if endpoint.PublicURL != "http://localhost" {
+ t.Error("All search criteria met; endpoint expected.")
+ return
+ }
+}
+
+// TestFFEBCViaRegionOption exercises only those calls where a region
+// parameter is specified explicitly. The region option overrides
+// any defined OS_REGION_NAME environment setting.
+func TestFFEBCViaRegionOption(t *testing.T) {
+ changeRegion("Starfleet Command")
+
+ endpoint := FindFirstEndpointByCriteria(
catalog("test", "compute", "http://localhost", "", ""),
ApiCriteria{Name: "test", Region: "RGN"},
)
@@ -93,10 +106,59 @@
t.Error("All search criteria met; endpoint expected.")
return
}
+}
+
+// TestFFEBCWithoutRegion exercises only those calls where a region
+// is irrelevant. Just to make sure, though, we enforce Gophercloud
+// from paying any attention to OS_REGION_NAME if it happens to be set.
+func TestFindFirstEndpointByCriteria(t *testing.T) {
+ endpoint := FindFirstEndpointByCriteria([]CatalogEntry{}, ApiCriteria{Name: "test", IgnoreEnvVars: true})
+ if endpoint.PublicURL != "" {
+ t.Error("Not expecting to find anything in an empty service catalog.")
+ return
+ }
+
+ endpoint = FindFirstEndpointByCriteria(
+ []CatalogEntry{
+ {Name: "test"},
+ },
+ ApiCriteria{Name: "test", IgnoreEnvVars: true},
+ )
+ if endpoint.PublicURL != "" {
+ t.Error("Even though we have a matching entry, no endpoints exist")
+ return
+ }
+
+ endpoint = FindFirstEndpointByCriteria(
+ catalog("test", "compute", "http://localhost", "", ""),
+ ApiCriteria{Name: "test", IgnoreEnvVars: true},
+ )
+ if endpoint.PublicURL != "http://localhost" {
+ t.Error("Looking for an endpoint by name but without region or version ID should match first entry endpoint.")
+ return
+ }
+
+ endpoint = FindFirstEndpointByCriteria(
+ catalog("test", "compute", "http://localhost", "", ""),
+ ApiCriteria{Type: "compute", IgnoreEnvVars: true},
+ )
+ if endpoint.PublicURL != "http://localhost" {
+ t.Error("Looking for an endpoint by type but without region or version ID should match first entry endpoint.")
+ return
+ }
+
+ endpoint = FindFirstEndpointByCriteria(
+ catalog("test", "compute", "http://localhost", "", ""),
+ ApiCriteria{Type: "identity", IgnoreEnvVars: true},
+ )
+ if endpoint.PublicURL != "" {
+ t.Error("Returned mismatched type.")
+ return
+ }
endpoint = FindFirstEndpointByCriteria(
catalog("test", "compute", "http://localhost", "ord", "2"),
- ApiCriteria{Name: "test", VersionId: "2"},
+ ApiCriteria{Name: "test", VersionId: "2", IgnoreEnvVars: true},
)
if endpoint.PublicURL != "http://localhost" {
t.Error("Sometimes, you might not care what region your stuff is in.")
@@ -119,3 +181,10 @@
},
}
}
+
+func changeRegion(r string) {
+ err := os.Setenv("OS_REGION_NAME", r)
+ if err != nil {
+ panic(err)
+ }
+}