Re-implement basic identity API for v0.2.0.
diff --git a/openstack/identity/common_test.go b/openstack/identity/common_test.go
index 81d37cc..95a2ccb 100644
--- a/openstack/identity/common_test.go
+++ b/openstack/identity/common_test.go
@@ -88,3 +88,87 @@
]
}
}`
+
+// Taken from: http://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listExtensions_v2.0_extensions_.html#GET_listExtensions_v2.0_extensions_-Request
+const queryResults = `{
+ "extensions":[{
+ "name": "Reset Password Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
+ "alias": "RS-RPE",
+ "updated": "2011-01-22T13:25:27-06:00",
+ "description": "Adds the capability to reset a user's password. The user is emailed when the password has been reset.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe-20111111.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe.wadl"
+ }
+ ]
+ },
+ {
+ "name": "User Metadata Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
+ "alias": "RS-META",
+ "updated": "2011-01-12T11:22:33-06:00",
+ "description": "Allows associating arbritrary metadata with a user.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta-20111201.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta.wadl"
+ }
+ ]
+ }
+ ],
+ "extensions_links":[]
+}`
+
+// Same as queryResults above, but with a bogus JSON envelop.
+const bogusExtensionsResults = `{
+ "explosions":[{
+ "name": "Reset Password Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
+ "alias": "RS-RPE",
+ "updated": "2011-01-22T13:25:27-06:00",
+ "description": "Adds the capability to reset a user's password. The user is emailed when the password has been reset.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe-20111111.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe.wadl"
+ }
+ ]
+ },
+ {
+ "name": "User Metadata Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
+ "alias": "RS-META",
+ "updated": "2011-01-12T11:22:33-06:00",
+ "description": "Allows associating arbritrary metadata with a user.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta-20111201.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta.wadl"
+ }
+ ]
+ }
+ ],
+ "extensions_links":[]
+}`
diff --git a/openstack/identity/doc.go b/openstack/identity/doc.go
new file mode 100644
index 0000000..081950d
--- /dev/null
+++ b/openstack/identity/doc.go
@@ -0,0 +1,120 @@
+/*
+The Identity package provides convenient OpenStack Identity V2 API client access.
+This package currently doesn't support the administrative access endpoints, but may appear in the future based on demand.
+
+Authentication
+
+Established convention in the OpenStack community suggests the use of environment variables to hold authentication parameters.
+For example, the following settings would be sufficient to authenticate against Rackspace:
+
+ # assumes Bash shell on a POSIX environment; use SET command for Windows.
+ export OS_AUTH_URL=https://identity.api.rackspacecloud.com/v2.0
+ export OS_USERNAME=xxxx
+ export OS_PASSWORD=yyyy
+
+while you'd need these additional settings to authenticate against, e.g., Nebula One:
+
+ export OS_TENANT_ID=zzzz
+ export OS_TENANT_NAME=wwww
+
+Be sure to consult with your provider to see which settings you'll need to authenticate with.
+
+A skeletal client gets started with Gophercloud by authenticating against his/her provider, like so:
+
+ package main
+
+ import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ )
+
+ func main() {
+ // Create an initialized set of authentication options based on available OS_*
+ // environment variables.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ panic(err)
+ }
+
+ // Attempt to authenticate with them.
+ r, err := identity.Authenticate(ao)
+ if err != nil {
+ panic(err)
+ }
+
+ // With each authentication, you receive a master directory of all the services
+ // your account can access. This "service catalog", as OpenStack calls it,
+ // provides you the means to exploit other OpenStack services.
+ sc, err := identity.GetServiceCatalog(r)
+ if err != nil {
+ panic(err)
+ }
+
+ // Find the desired service(s) for our application.
+ computeService, err := findService(sc, "compute", ...)
+ if err != nil {
+ panic(err)
+ }
+
+ blockStorage, err := findService(sc, "block-storage", ...)
+ if err != nil {
+ panic(err)
+ }
+
+ // ... etc ...
+ }
+
+NOTE!
+Unlike versions 0.1.x of the Gophercloud API,
+0.2.0 and later will not provide a service look-up mechanism as a built-in feature of the Identity SDK binding.
+The 0.1.x behavior potentially opened its non-US users to legal liability by potentially selecting endpoints in undesirable regions
+in a non-obvious manner if a specific region was not explicitly specified.
+Starting with 0.2.0 and beyond, you'll need to use either your own service catalog query function or one in a separate package.
+This makes it plainly visible to a code auditor that if you indeed desired automatic selection of an arbitrary region,
+you made the conscious choice to use that feature.
+
+Extensions
+
+Some OpenStack deployments may support features that other deployments do not.
+Anything beyond the scope of standard OpenStack must be scoped by an "extension," a named, yet well-known, change to the API.
+Users may invoke IsExtensionAvailable() after grabbing a list of extensions from the server with GetExtensions().
+This of course assumes you know the name of the extension ahead of time.
+
+Here's a simple example of listing all the aliases for supported extensions.
+Once you have an alias to an extension, everything else about it may be queried through accessors.
+
+ package main
+
+ import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ )
+
+ func main() {
+ // Create an initialized set of authentication options based on available OS_*
+ // environment variables.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ panic(err)
+ }
+
+ // Attempt to query extensions.
+ exts, err := identity.GetExtensions(ao)
+ if err != nil {
+ panic(err)
+ }
+
+ // Print out a summary of supported extensions
+ aliases, err := exts.Aliases()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println("Extension Aliases:")
+ for _, alias := range aliases {
+ fmt.Printf(" %s\n", alias)
+ }
+ }
+*/
+package identity
diff --git a/openstack/identity/errors.go b/openstack/identity/errors.go
index b15a4a2..efa7c85 100644
--- a/openstack/identity/errors.go
+++ b/openstack/identity/errors.go
@@ -2,6 +2,16 @@
import "fmt"
-var ErrNotImplemented = fmt.Errorf("Identity feature not yet implemented")
-var ErrEndpoint = fmt.Errorf("Improper or missing Identity endpoint")
-var ErrCredentials = fmt.Errorf("Improper or missing Identity credentials")
+// ErrNotImplemented errors may occur in two contexts:
+// (1) development versions of this package may return this error for endpoints which are defined but not yet completed, and,
+// (2) production versions of this package may return this error when a provider fails to offer the requested Identity extension.
+//
+// ErrEndpoint errors occur when the authentication URL provided to Authenticate() either isn't valid
+// or the endpoint provided doesn't respond like an Identity V2 API endpoint should.
+//
+// ErrCredentials errors occur when authentication fails due to the caller possessing insufficient access privileges.
+var (
+ ErrNotImplemented = fmt.Errorf("Identity feature not yet implemented")
+ ErrEndpoint = fmt.Errorf("Improper or missing Identity endpoint")
+ ErrCredentials = fmt.Errorf("Improper or missing Identity credentials")
+)
diff --git a/openstack/identity/extensions.go b/openstack/identity/extensions.go
index 4a861ff..cfd8f24 100644
--- a/openstack/identity/extensions.go
+++ b/openstack/identity/extensions.go
@@ -1,14 +1,18 @@
package identity
import (
- "fmt"
"github.com/mitchellh/mapstructure"
)
-var (
- ErrNotFound = fmt.Errorf("Identity extension not found")
-)
-
+// ExtensionDetails provides the details offered by the OpenStack Identity V2 extensions API
+// for a named extension.
+//
+// Name provides the name, presumably the same as that used to query the API with.
+//
+// Updated provides, in a sense, the version of the extension supported. It gives the timestamp
+// of the most recent extension deployment.
+//
+// Description provides a more customer-oriented description of the extension.
type ExtensionDetails struct {
Name string
Namespace string
@@ -16,37 +20,71 @@
Description string
}
-// ExtensionsDesc structures are returned by the Extensions() function for valid input.
-// This structure implements the ExtensionInquisitor interface.
-type ExtensionsDesc struct {
- extensions []interface{}
-}
+// ExtensionsResult encapsulates the raw data returned by a call to
+// GetExtensions(). As OpenStack extensions may freely alter the response
+// bodies of structures returned to the client, you may only safely access the
+// data provided through separate, type-safe accessors or methods.
+type ExtensionsResult map[string]interface{}
-func Extensions(m map[string]interface{}) *ExtensionsDesc {
- return &ExtensionsDesc{extensions: m["extensions"].([]interface{})}
-}
-
-func extensionIndexByAlias(e *ExtensionsDesc, alias string) (int, error) {
- for i, ee := range e.extensions {
- extensionRecord := ee.(map[string]interface{})
- if extensionRecord["alias"] == alias {
- return i, nil
- }
+// IsExtensionAvailable returns true if and only if the provider supports the named extension.
+func (er ExtensionsResult) IsExtensionAvailable(alias string) bool {
+ e, err := extensions(er)
+ if err != nil {
+ return false
}
- return 0, ErrNotFound
-}
-
-func (e *ExtensionsDesc) IsExtensionAvailable(alias string) bool {
- _, err := extensionIndexByAlias(e, alias)
+ _, err = extensionIndexByAlias(e, alias)
return err == nil
}
-func (e *ExtensionsDesc) ExtensionDetailsByAlias(alias string) (*ExtensionDetails, error) {
+// ExtensionDetailsByAlias returns more detail than the mere presence of an extension by the provider.
+// See the ExtensionDetails structure.
+func (er ExtensionsResult) ExtensionDetailsByAlias(alias string) (*ExtensionDetails, error) {
+ e, err := extensions(er)
+ if err != nil {
+ return nil, err
+ }
i, err := extensionIndexByAlias(e, alias)
if err != nil {
return nil, err
}
ed := &ExtensionDetails{}
- err = mapstructure.Decode(e.extensions[i], ed)
+ err = mapstructure.Decode(e[i], ed)
return ed, err
}
+
+func extensionIndexByAlias(records []interface{}, alias string) (int, error) {
+ for i, er := range records {
+ extensionRecord := er.(map[string]interface{})
+ if extensionRecord["alias"] == alias {
+ return i, nil
+ }
+ }
+ return 0, ErrNotImplemented
+}
+
+func extensions(er ExtensionsResult) ([]interface{}, error) {
+ e, ok := er["extensions"]
+ if !ok {
+ return nil, ErrNotImplemented
+ }
+ return e.([]interface{}), nil
+}
+
+// Aliases returns the set of extension handles, or "aliases" as OpenStack calls them.
+// These are not the names of the extensions, but rather opaque, symbolic monikers for their corresponding extension.
+// Use the ExtensionDetailsByAlias() method to query more information for an extension if desired.
+func (er ExtensionsResult) Aliases() ([]string, error) {
+ e, err := extensions(er)
+ if err != nil {
+ return nil, err
+ }
+ aliases := make([]string, len(e))
+ for i, ex := range e {
+ ext := ex.(map[string]interface{})
+ extn, ok := ext["alias"]
+ if ok {
+ aliases[i] = extn.(string)
+ }
+ }
+ return aliases, nil
+}
diff --git a/openstack/identity/extensions_test.go b/openstack/identity/extensions_test.go
index a5eede2..009d29d 100644
--- a/openstack/identity/extensions_test.go
+++ b/openstack/identity/extensions_test.go
@@ -5,48 +5,6 @@
"testing"
)
-// Taken from: http://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listExtensions_v2.0_extensions_.html#GET_listExtensions_v2.0_extensions_-Request
-const queryResults = `{
- "extensions":[{
- "name": "Reset Password Extension",
- "namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
- "alias": "RS-RPE",
- "updated": "2011-01-22T13:25:27-06:00",
- "description": "Adds the capability to reset a user's password. The user is emailed when the password has been reset.",
- "links":[{
- "rel": "describedby",
- "type": "application/pdf",
- "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe-20111111.pdf"
- },
- {
- "rel": "describedby",
- "type": "application/vnd.sun.wadl+xml",
- "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe.wadl"
- }
- ]
- },
- {
- "name": "User Metadata Extension",
- "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
- "alias": "RS-META",
- "updated": "2011-01-12T11:22:33-06:00",
- "description": "Allows associating arbritrary metadata with a user.",
- "links":[{
- "rel": "describedby",
- "type": "application/pdf",
- "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta-20111201.pdf"
- },
- {
- "rel": "describedby",
- "type": "application/vnd.sun.wadl+xml",
- "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta.wadl"
- }
- ]
- }
- ],
- "extensions_links":[]
-}`
-
func TestIsExtensionAvailable(t *testing.T) {
// Make a response as we'd expect from the IdentityService.GetExtensions() call.
getExtensionsResults := make(map[string]interface{})
@@ -56,7 +14,7 @@
return
}
- e := Extensions(getExtensionsResults)
+ e := ExtensionsResult(getExtensionsResults)
for _, alias := range []string{"RS-RPE", "RS-META"} {
if !e.IsExtensionAvailable(alias) {
t.Errorf("Expected extension %s present.", alias)
@@ -78,7 +36,7 @@
return
}
- e := Extensions(getExtensionsResults)
+ e := ExtensionsResult(getExtensionsResults)
ed, err := e.ExtensionDetailsByAlias("RS-META")
if err != nil {
t.Error(err)
@@ -106,3 +64,48 @@
}
}
}
+
+func TestMalformedResponses(t *testing.T) {
+ getExtensionsResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(bogusExtensionsResults), &getExtensionsResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ e := ExtensionsResult(getExtensionsResults)
+
+ _, err = e.ExtensionDetailsByAlias("RS-META")
+ if err == nil {
+ t.Error("Expected ErrNotImplemented at least")
+ return
+ }
+ if err != ErrNotImplemented {
+ t.Error("Expected ErrNotImplemented")
+ return
+ }
+
+ if e.IsExtensionAvailable("anything at all") {
+ t.Error("No extensions are available with a bogus result.")
+ return
+ }
+}
+
+func TestAliases(t *testing.T) {
+ getExtensionsResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ e := ExtensionsResult(getExtensionsResults)
+ aliases, err := e.Aliases()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(aliases) != len(e) {
+ t.Error("Expected one alias name per extension")
+ return
+ }
+}
diff --git a/openstack/identity/requests.go b/openstack/identity/requests.go
index 0012a17..dbd367e 100644
--- a/openstack/identity/requests.go
+++ b/openstack/identity/requests.go
@@ -4,42 +4,56 @@
"github.com/racker/perigee"
)
+// AuthResults encapsulates the raw results from an authentication request.
+// As OpenStack allows extensions to influence the structure returned in
+// ways that Gophercloud cannot predict at compile-time, you should use
+// type-safe accessors to work with the data represented by this type,
+// such as ServiceCatalog() and Token().
type AuthResults map[string]interface{}
-// AuthOptions lets anyone calling Authenticate() supply the required access credentials.
-// At present, only Identity V2 API support exists; therefore, only Username, Password,
-// and optionally, TenantId are provided. If future Identity API versions become available,
-// alternative fields unique to those versions may appear here.
+// AuthOptions lets anyone calling Authenticate() supply the required access
+// credentials. At present, only Identity V2 API support exists; therefore,
+// only Username, Password, and optionally, TenantId are provided. If future
+// Identity API versions become available, alternative fields unique to those
+// versions may appear here.
+//
+// Endpoint specifies the HTTP endpoint offering the Identity V2 API.
+// Required.
+//
+// Username is required if using Identity V2 API. Consult with your provider's
+// control panel to discover your account's username.
+//
+// At most one of Password or ApiKey is required if using Identity V2 API.
+// Consult with your provider's control panel to discover your account's
+// preferred method of authentication.
+//
+// The TenantId and TenantName fields are optional for the Identity V2 API.
+// Some providers allow you to specify a TenantName instead of the TenantId.
+// Some require both. Your provider's authentication policies will determine
+// how these fields influence authentication.
+//
+// AllowReauth should be set to true if you grant permission for Gophercloud to
+// cache your credentials in memory, and to allow Gophercloud to attempt to
+// re-authenticate automatically if/when your token expires. If you set it to
+// false, it will not cache these settings, but re-authentication will not be
+// possible. This setting defaults to false.
type AuthOptions struct {
- // Endpoint specifies the HTTP endpoint offering the Identity V2 API.
- // Required.
- Endpoint string
-
- // Username is required if using Identity V2 API.
- // Consult with your provider's control panel to discover your
- // account's username.
- Username string
-
- // At most one of Password or ApiKey is required if using Identity V2 API.
- // Consult with your provider's control panel to discover your
- // account's preferred method of authentication.
+ Endpoint string
+ Username string
Password, ApiKey string
-
- // The TenantId field is optional for the Identity V2 API.
- TenantId string
-
- // The TenantName can be specified instead of the TenantId
- TenantName string
-
- // AllowReauth should be set to true if you grant permission for Gophercloud to cache
- // your credentials in memory, and to allow Gophercloud to attempt to re-authenticate
- // automatically if/when your token expires. If you set it to false, it will not cache
- // these settings, but re-authentication will not be possible. This setting defaults
- // to false.
- AllowReauth bool
+ TenantId string
+ TenantName string
+ AllowReauth bool
}
+// Authenticate passes the supplied credentials to the OpenStack provider for authentication.
+// If successful, the caller may use Token() to retrieve the authentication token,
+// and ServiceCatalog() to retrieve the set of services available to the API user.
func Authenticate(options AuthOptions) (AuthResults, error) {
+ type AuthContainer struct {
+ Auth auth `json:"auth"`
+ }
+
var ar AuthResults
if options.Endpoint == "" {
@@ -50,7 +64,8 @@
return nil, ErrCredentials
}
- err := perigee.Post(options.Endpoint, perigee.Options{
+ url := options.Endpoint + "/tokens"
+ err := perigee.Post(url, perigee.Options{
ReqBody: &AuthContainer{
Auth: getAuthCredentials(options),
},
@@ -59,9 +74,9 @@
return ar, err
}
-func getAuthCredentials(options AuthOptions) Auth {
+func getAuthCredentials(options AuthOptions) auth {
if options.ApiKey == "" {
- return Auth{
+ return auth{
PasswordCredentials: &struct {
Username string `json:"username"`
Password string `json:"password"`
@@ -73,7 +88,7 @@
TenantName: options.TenantName,
}
} else {
- return Auth{
+ return auth{
ApiKeyCredentials: &struct {
Username string `json:"username"`
ApiKey string `json:"apiKey"`
@@ -87,17 +102,19 @@
}
}
-// AuthContainer provides a JSON encoding wrapper for passing credentials to the Identity
-// service. You will not work with this structure directly.
-type AuthContainer struct {
- Auth Auth `json:"auth"`
-}
-
-// Auth provides a JSON encoding wrapper for passing credentials to the Identity
-// service. You will not work with this structure directly.
-type Auth struct {
+type auth struct {
PasswordCredentials interface{} `json:"passwordCredentials,omitempty"`
ApiKeyCredentials interface{} `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
TenantId string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"`
}
+
+func GetExtensions(options AuthOptions) (ExtensionsResult, error) {
+ var exts ExtensionsResult
+
+ url := options.Endpoint + "/extensions"
+ err := perigee.Get(url, perigee.Options{
+ Results: &exts,
+ })
+ return exts, err
+}
diff --git a/openstack/identity/service_catalog.go b/openstack/identity/service_catalog.go
index af6bbdb..035f671 100644
--- a/openstack/identity/service_catalog.go
+++ b/openstack/identity/service_catalog.go
@@ -2,16 +2,43 @@
import "github.com/mitchellh/mapstructure"
-type ServiceCatalogDesc struct {
+// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
+// OpenStack extensions may alter the structure of the service catalog in ways unpredictable to Go at compile-time,
+// so this structure serves as a convenient anchor for type-safe accessors and methods.
+type ServiceCatalog struct {
serviceDescriptions []interface{}
}
+// CatalogEntry provides a type-safe interface to an Identity API V2 service
+// catalog listing. Each class of service, such as cloud DNS or block storage
+// services, will have a single CatalogEntry representing it.
+//
+// Name will contain the provider-specified name for the service.
+//
+// If OpenStack defines a type for the service, this field will contain that
+// type string. Otherwise, for provider-specific services, the provider may
+// assign their own type strings.
+//
+// Endpoints will let the caller iterate over all the different endpoints that
+// may exist for the service.
+//
+// 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 {
Name string
Type string
Endpoints []Endpoint
}
+// Endpoint represents a single API endpoint offered by a service.
+// It provides the public and internal URLs, if supported, along with a region specifier, again if provided.
+// The significance of the Region field will depend upon your provider.
+//
+// In addition, the interface offered by the service will have version information associated with it
+// through the VersionId, VersionInfo, and VersionList fields, if provided or supported.
+//
+// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value ("").
type Endpoint struct {
TenantId string
PublicURL string
@@ -22,20 +49,27 @@
VersionList string
}
-func ServiceCatalog(ar AuthResults) (*ServiceCatalogDesc, error) {
+// GetServiceCatalog acquires the service catalog from a successful authentication's results.
+func GetServiceCatalog(ar AuthResults) (*ServiceCatalog, error) {
access := ar["access"].(map[string]interface{})
sds := access["serviceCatalog"].([]interface{})
- sc := &ServiceCatalogDesc{
+ sc := &ServiceCatalog{
serviceDescriptions: sds,
}
return sc, nil
}
-func (sc *ServiceCatalogDesc) NumberOfServices() int {
+// NumberOfServices yields the number of services the caller may use. Note
+// that this does not necessarily equal the number of endpoints available for
+// use.
+func (sc *ServiceCatalog) NumberOfServices() int {
return len(sc.serviceDescriptions)
}
-func (sc *ServiceCatalogDesc) CatalogEntries() ([]CatalogEntry, error) {
+// CatalogEntries returns a slice of service catalog entries.
+// Each entry corresponds to a specific class of service offered by the API provider.
+// See the CatalogEntry structure for more details.
+func (sc *ServiceCatalog) CatalogEntries() ([]CatalogEntry, error) {
var err error
ces := make([]CatalogEntry, sc.NumberOfServices())
for i, sd := range sc.serviceDescriptions {
diff --git a/openstack/identity/service_catalog_test.go b/openstack/identity/service_catalog_test.go
index 82e4d9a..f810609 100644
--- a/openstack/identity/service_catalog_test.go
+++ b/openstack/identity/service_catalog_test.go
@@ -13,7 +13,7 @@
return
}
- sc, err := ServiceCatalog(authResults)
+ sc, err := GetServiceCatalog(authResults)
if err != nil {
panic(err)
}
diff --git a/openstack/identity/token.go b/openstack/identity/token.go
index aaea184..d50cce0 100644
--- a/openstack/identity/token.go
+++ b/openstack/identity/token.go
@@ -4,40 +4,69 @@
"github.com/mitchellh/mapstructure"
)
-type TenantDesc struct {
- Id string
- Name string
+// Token provides only the most basic information related to an authentication token.
+//
+// Id provides the primary means of identifying a user to the OpenStack API.
+// OpenStack defines this field as an opaque value, so do not depend on its content.
+// It is safe, however, to compare for equality.
+//
+// Expires provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid.
+// After this point in time, future API requests made using this authentication token will respond with errors.
+// Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication.
+// See the AuthOptions structure for more details.
+//
+// TenantId provides the canonical means of identifying a tenant.
+// As with Id, this field is defined to be opaque, so do not depend on its content.
+// It is safe, however, to compare for equality.
+//
+// TenantName provides a human-readable tenant name corresponding to the TenantId.
+type Token struct {
+ Id, Expires string
+ TenantId, TenantName string
}
-type TokenDesc struct {
- Id_ string `mapstructure:"Id"`
- Expires_ string `mapstructure:"Expires"`
- Tenant TenantDesc
-}
+// GetToken, if successful, yields an unpacked collection of fields related to the user's access credentials, called a "token."
+// See the Token structure for more details.
+func GetToken(m AuthResults) (*Token, error) {
+ type (
+ Tenant struct {
+ Id string
+ Name string
+ }
-func Token(m AuthResults) (*TokenDesc, error) {
- accessMap := m["access"].(map[string]interface{})
- tokenMap := accessMap["token"].(map[string]interface{})
- td := &TokenDesc{}
- err := mapstructure.Decode(tokenMap, td)
+ TokenDesc struct {
+ Id string `mapstructure:"id"`
+ Expires string `mapstructure:"expires"`
+ Tenant
+ }
+ )
+
+ accessMap, err := getSubmap(m, "access")
if err != nil {
return nil, err
}
+ tokenMap, err := getSubmap(accessMap, "token")
+ if err != nil {
+ return nil, err
+ }
+ t := &TokenDesc{}
+ err = mapstructure.Decode(tokenMap, t)
+ if err != nil {
+ return nil, err
+ }
+ td := &Token{
+ Id: t.Id,
+ Expires: t.Expires,
+ TenantId: t.Tenant.Id,
+ TenantName: t.Tenant.Name,
+ }
return td, nil
}
-func (td *TokenDesc) Id() string {
- return td.Id_
-}
-
-func (td *TokenDesc) Expires() string {
- return td.Expires_
-}
-
-func (td *TokenDesc) TenantId() string {
- return td.Tenant.Id
-}
-
-func (td *TokenDesc) TenantName() string {
- return td.Tenant.Name
+func getSubmap(m map[string]interface{}, name string) (map[string]interface{}, error) {
+ entry, ok := m[name]
+ if !ok {
+ return nil, ErrNotImplemented
+ }
+ return entry.(map[string]interface{}), nil
}
diff --git a/openstack/identity/token_test.go b/openstack/identity/token_test.go
index 4fdf953..5e96496 100644
--- a/openstack/identity/token_test.go
+++ b/openstack/identity/token_test.go
@@ -13,13 +13,13 @@
return
}
- tok, err := Token(authResults)
+ tok, err := GetToken(authResults)
if err != nil {
t.Error(err)
return
}
- if tok.Id() != "ab48a9efdfedb23ty3494" {
- t.Errorf("Expected token \"ab48a9efdfedb23ty3494\"; got \"%s\" instead", tok.Id())
+ if tok.Id != "ab48a9efdfedb23ty3494" {
+ t.Errorf("Expected token \"ab48a9efdfedb23ty3494\"; got \"%s\" instead", tok.Id)
return
}
}