Moving calls to client helper while I'm at it
diff --git a/_site/openstack/identity/v2/extensions/delegate.go b/_site/openstack/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..cee275f
--- /dev/null
+++ b/_site/openstack/identity/v2/extensions/delegate.go
@@ -0,0 +1,52 @@
+package extensions
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtensionPage is a single page of Extension results.
+type ExtensionPage struct {
+ common.ExtensionPage
+}
+
+// IsEmpty returns true if the current page contains at least one Extension.
+func (page ExtensionPage) IsEmpty() (bool, error) {
+ is, err := ExtractExtensions(page)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
+// elements into a slice of Extension structs.
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
+ // Identity v2 adds an intermediate "values" object.
+
+ var resp struct {
+ Extensions struct {
+ Values []common.Extension `mapstructure:"values"`
+ } `mapstructure:"extensions"`
+ }
+
+ err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
+ return resp.Extensions.Values, err
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+ return common.Get(c, alias)
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return common.List(c).WithPageCreator(func(r pagination.LastHTTPResponse) pagination.Page {
+ return ExtensionPage{
+ ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)},
+ }
+ })
+}
diff --git a/_site/openstack/identity/v2/extensions/delegate_test.go b/_site/openstack/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..504118a
--- /dev/null
+++ b/_site/openstack/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,38 @@
+package extensions
+
+import (
+ "testing"
+
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListExtensionsSuccessfully(t)
+
+ count := 0
+ err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, common.ExpectedExtensions, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ common.HandleGetExtensionSuccessfully(t)
+
+ actual, err := Get(client.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, common.SingleExtension, actual)
+}
diff --git a/_site/openstack/identity/v2/extensions/fixtures.go b/_site/openstack/identity/v2/extensions/fixtures.go
new file mode 100644
index 0000000..96cb7d2
--- /dev/null
+++ b/_site/openstack/identity/v2/extensions/fixtures.go
@@ -0,0 +1,60 @@
+// +build fixtures
+
+package extensions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single Extension result. It differs from the delegated implementation
+// by the introduction of an intermediate "values" member.
+const ListOutput = `
+{
+ "extensions": {
+ "values": [
+ {
+ "updated": "2013-01-20T00:00:00-00:00",
+ "name": "Neutron Service Type Management",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+ "alias": "service-type",
+ "description": "API for retrieving service providers for Neutron advanced services"
+ }
+ ]
+ }
+}
+`
+
+// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List
+// call.
+func HandleListExtensionsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+
+ fmt.Fprintf(w, `
+{
+ "extensions": {
+ "values": [
+ {
+ "updated": "2013-01-20T00:00:00-00:00",
+ "name": "Neutron Service Type Management",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+ "alias": "service-type",
+ "description": "API for retrieving service providers for Neutron advanced services"
+ }
+ ]
+ }
+}
+ `)
+ })
+
+}
diff --git a/_site/openstack/identity/v2/tenants/doc.go b/_site/openstack/identity/v2/tenants/doc.go
new file mode 100644
index 0000000..473a302
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/doc.go
@@ -0,0 +1,7 @@
+/*
+Package tenants contains API calls that query for information about tenants on an OpenStack deployment.
+
+See: http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
+And: http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants
+*/
+package tenants
diff --git a/_site/openstack/identity/v2/tenants/fixtures.go b/_site/openstack/identity/v2/tenants/fixtures.go
new file mode 100644
index 0000000..7f044ac
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/fixtures.go
@@ -0,0 +1,65 @@
+// +build fixtures
+
+package tenants
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Tenant results.
+const ListOutput = `
+{
+ "tenants": [
+ {
+ "id": "1234",
+ "name": "Red Team",
+ "description": "The team that is red",
+ "enabled": true
+ },
+ {
+ "id": "9876",
+ "name": "Blue Team",
+ "description": "The team that is blue",
+ "enabled": false
+ }
+ ]
+}
+`
+
+// RedTeam is a Tenant fixture.
+var RedTeam = Tenant{
+ ID: "1234",
+ Name: "Red Team",
+ Description: "The team that is red",
+ Enabled: true,
+}
+
+// BlueTeam is a Tenant fixture.
+var BlueTeam = Tenant{
+ ID: "9876",
+ Name: "Blue Team",
+ Description: "The team that is blue",
+ Enabled: false,
+}
+
+// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput.
+var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam}
+
+// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that
+// responds with a list of two tenants.
+func HandleListTenantsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, ListOutput)
+ })
+}
diff --git a/_site/openstack/identity/v2/tenants/requests.go b/_site/openstack/identity/v2/tenants/requests.go
new file mode 100644
index 0000000..5ffeaa7
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/requests.go
@@ -0,0 +1,33 @@
+package tenants
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts filters the Tenants that are returned by the List call.
+type ListOpts struct {
+ // Marker is the ID of the last Tenant on the previous page.
+ Marker string `q:"marker"`
+
+ // Limit specifies the page size.
+ Limit int `q:"limit"`
+}
+
+// List enumerates the Tenants to which the current token has access.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return TenantPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ url := listURL(client)
+ if opts != nil {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += q.String()
+ }
+
+ return pagination.NewPager(client, url, createPage)
+}
diff --git a/_site/openstack/identity/v2/tenants/requests_test.go b/_site/openstack/identity/v2/tenants/requests_test.go
new file mode 100644
index 0000000..e8f172d
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/requests_test.go
@@ -0,0 +1,29 @@
+package tenants
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListTenants(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListTenantsSuccessfully(t)
+
+ count := 0
+ err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ actual, err := ExtractTenants(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ExpectedTenantSlice, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
diff --git a/_site/openstack/identity/v2/tenants/results.go b/_site/openstack/identity/v2/tenants/results.go
new file mode 100644
index 0000000..c1220c3
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/results.go
@@ -0,0 +1,62 @@
+package tenants
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Tenant is a grouping of users in the identity service.
+type Tenant struct {
+ // ID is a unique identifier for this tenant.
+ ID string `mapstructure:"id"`
+
+ // Name is a friendlier user-facing name for this tenant.
+ Name string `mapstructure:"name"`
+
+ // Description is a human-readable explanation of this Tenant's purpose.
+ Description string `mapstructure:"description"`
+
+ // Enabled indicates whether or not a tenant is active.
+ Enabled bool `mapstructure:"enabled"`
+}
+
+// TenantPage is a single page of Tenant results.
+type TenantPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page TenantPage) IsEmpty() (bool, error) {
+ tenants, err := ExtractTenants(page)
+ if err != nil {
+ return false, err
+ }
+ return len(tenants) == 0, nil
+}
+
+// NextPageURL extracts the "next" link from the tenants_links section of the result.
+func (page TenantPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"tenants_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractTenants returns a slice of Tenants contained in a single page of results.
+func ExtractTenants(page pagination.Page) ([]Tenant, error) {
+ casted := page.(TenantPage).Body
+ var response struct {
+ Tenants []Tenant `mapstructure:"tenants"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.Tenants, err
+}
diff --git a/_site/openstack/identity/v2/tenants/urls.go b/_site/openstack/identity/v2/tenants/urls.go
new file mode 100644
index 0000000..1dd6ce0
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/urls.go
@@ -0,0 +1,7 @@
+package tenants
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("tenants")
+}
diff --git a/_site/openstack/identity/v2/tokens/doc.go b/_site/openstack/identity/v2/tokens/doc.go
new file mode 100644
index 0000000..d26f642
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/doc.go
@@ -0,0 +1,6 @@
+/*
+Package tokens contains functions that issue and manipulate identity tokens.
+
+Reference: http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
+*/
+package tokens
diff --git a/_site/openstack/identity/v2/tokens/errors.go b/_site/openstack/identity/v2/tokens/errors.go
new file mode 100644
index 0000000..3a9172e
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/errors.go
@@ -0,0 +1,30 @@
+package tokens
+
+import (
+ "errors"
+ "fmt"
+)
+
+var (
+ // ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
+ ErrUserIDProvided = unacceptedAttributeErr("UserID")
+
+ // ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey.
+ ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
+ // ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
+ ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
+
+ // ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName.
+ ErrDomainNameProvided = unacceptedAttributeErr("DomainName")
+
+ // ErrUsernameRequired is returned if you attempt ot authenticate without a Username.
+ ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.")
+
+ // ErrPasswordRequired is returned if you don't provide a password.
+ ErrPasswordRequired = errors.New("Please supply a Password in your AuthOptions.")
+)
+
+func unacceptedAttributeErr(attribute string) error {
+ return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute)
+}
diff --git a/_site/openstack/identity/v2/tokens/fixtures.go b/_site/openstack/identity/v2/tokens/fixtures.go
new file mode 100644
index 0000000..1cb0d05
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/fixtures.go
@@ -0,0 +1,128 @@
+// +build fixtures
+
+package tokens
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// ExpectedToken is the token that should be parsed from TokenCreationResponse.
+var ExpectedToken = &Token{
+ ID: "aaaabbbbccccdddd",
+ ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
+ Tenant: tenants.Tenant{
+ ID: "fc394f2ab2df4114bde39905f800dc57",
+ Name: "test",
+ Description: "There are many tenants. This one is yours.",
+ Enabled: true,
+ },
+}
+
+// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse.
+var ExpectedServiceCatalog = &ServiceCatalog{
+ Entries: []CatalogEntry{
+ CatalogEntry{
+ Name: "inscrutablewalrus",
+ Type: "something",
+ Endpoints: []Endpoint{
+ Endpoint{
+ PublicURL: "http://something0:1234/v2/",
+ Region: "region0",
+ },
+ Endpoint{
+ PublicURL: "http://something1:1234/v2/",
+ Region: "region1",
+ },
+ },
+ },
+ CatalogEntry{
+ Name: "arbitrarypenguin",
+ Type: "else",
+ Endpoints: []Endpoint{
+ Endpoint{
+ PublicURL: "http://else0:4321/v3/",
+ Region: "region0",
+ },
+ },
+ },
+ },
+}
+
+// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog.
+const TokenCreationResponse = `
+{
+ "access": {
+ "token": {
+ "issued_at": "2014-01-30T15:30:58.000000Z",
+ "expires": "2014-01-31T15:30:58Z",
+ "id": "aaaabbbbccccdddd",
+ "tenant": {
+ "description": "There are many tenants. This one is yours.",
+ "enabled": true,
+ "id": "fc394f2ab2df4114bde39905f800dc57",
+ "name": "test"
+ }
+ },
+ "serviceCatalog": [
+ {
+ "endpoints": [
+ {
+ "publicURL": "http://something0:1234/v2/",
+ "region": "region0"
+ },
+ {
+ "publicURL": "http://something1:1234/v2/",
+ "region": "region1"
+ }
+ ],
+ "type": "something",
+ "name": "inscrutablewalrus"
+ },
+ {
+ "endpoints": [
+ {
+ "publicURL": "http://else0:4321/v3/",
+ "region": "region0"
+ }
+ ],
+ "type": "else",
+ "name": "arbitrarypenguin"
+ }
+ ]
+ }
+}
+`
+
+// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been
+// constructed properly given certain auth options, and returns the result.
+func HandleTokenPost(t *testing.T, requestJSON string) {
+ th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ if requestJSON != "" {
+ th.TestJSONRequest(t, r, requestJSON)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, TokenCreationResponse)
+ })
+}
+
+// IsSuccessful ensures that a CreateResult was successful and contains the correct token and
+// service catalog.
+func IsSuccessful(t *testing.T, result CreateResult) {
+ token, err := result.ExtractToken()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedToken, token)
+
+ serviceCatalog, err := result.ExtractServiceCatalog()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog)
+}
diff --git a/_site/openstack/identity/v2/tokens/requests.go b/_site/openstack/identity/v2/tokens/requests.go
new file mode 100644
index 0000000..c25a72b
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,87 @@
+package tokens
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// AuthOptionsBuilder describes any argument that may be passed to the Create call.
+type AuthOptionsBuilder interface {
+
+ // ToTokenCreateMap assembles the Create request body, returning an error if parameters are
+ // missing or inconsistent.
+ ToTokenCreateMap() (map[string]interface{}, error)
+}
+
+// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
+// interface.
+type AuthOptions struct {
+ gophercloud.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+ return AuthOptions{AuthOptions: original}
+}
+
+// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON
+// request.
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+ // Error out if an unsupported auth option is present.
+ if auth.UserID != "" {
+ return nil, ErrUserIDProvided
+ }
+ if auth.APIKey != "" {
+ return nil, ErrAPIKeyProvided
+ }
+ if auth.DomainID != "" {
+ return nil, ErrDomainIDProvided
+ }
+ if auth.DomainName != "" {
+ return nil, ErrDomainNameProvided
+ }
+
+ // Username and Password are always required.
+ if auth.Username == "" {
+ return nil, ErrUsernameRequired
+ }
+ if auth.Password == "" {
+ return nil, ErrPasswordRequired
+ }
+
+ // Populate the request map.
+ authMap := make(map[string]interface{})
+
+ authMap["passwordCredentials"] = map[string]interface{}{
+ "username": auth.Username,
+ "password": auth.Password,
+ }
+
+ if auth.TenantID != "" {
+ authMap["tenantId"] = auth.TenantID
+ }
+ if auth.TenantName != "" {
+ authMap["tenantName"] = auth.TenantName
+ }
+
+ return map[string]interface{}{"auth": authMap}, nil
+}
+
+// Create authenticates to the identity service and attempts to acquire a Token.
+// If successful, the CreateResult
+// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(),
+// which abstracts all of the gory details about navigating service catalogs and such.
+func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateResult {
+ request, err := auth.ToTokenCreateMap()
+ if err != nil {
+ return CreateResult{gophercloud.CommonResult{Err: err}}
+ }
+
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", CreateURL(client), perigee.Options{
+ ReqBody: &request,
+ Results: &result.Resp,
+ OkCodes: []int{200, 203},
+ })
+ return result
+}
diff --git a/_site/openstack/identity/v2/tokens/requests_test.go b/_site/openstack/identity/v2/tokens/requests_test.go
new file mode 100644
index 0000000..2f02825
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/requests_test.go
@@ -0,0 +1,140 @@
+package tokens
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) CreateResult {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleTokenPost(t, requestJSON)
+
+ return Create(client.ServiceClient(), AuthOptions{options})
+}
+
+func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleTokenPost(t, "")
+
+ actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err
+ th.CheckEquals(t, expectedErr, actualErr)
+}
+
+func TestCreateWithPassword(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "swordfish",
+ }
+
+ IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "passwordCredentials": {
+ "username": "me",
+ "password": "swordfish"
+ }
+ }
+ }
+ `))
+}
+
+func TestCreateTokenWithTenantID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "opensesame",
+ TenantID: "fc394f2ab2df4114bde39905f800dc57",
+ }
+
+ IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "tenantId": "fc394f2ab2df4114bde39905f800dc57",
+ "passwordCredentials": {
+ "username": "me",
+ "password": "opensesame"
+ }
+ }
+ }
+ `))
+}
+
+func TestCreateTokenWithTenantName(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "opensesame",
+ TenantName: "demo",
+ }
+
+ IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "tenantName": "demo",
+ "passwordCredentials": {
+ "username": "me",
+ "password": "opensesame"
+ }
+ }
+ }
+ `))
+}
+
+func TestProhibitUserID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ UserID: "1234",
+ Password: "thing",
+ }
+
+ tokenPostErr(t, options, ErrUserIDProvided)
+}
+
+func TestProhibitAPIKey(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ APIKey: "123412341234",
+ }
+
+ tokenPostErr(t, options, ErrAPIKeyProvided)
+}
+
+func TestProhibitDomainID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ DomainID: "1234",
+ }
+
+ tokenPostErr(t, options, ErrDomainIDProvided)
+}
+
+func TestProhibitDomainName(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ DomainName: "wat",
+ }
+
+ tokenPostErr(t, options, ErrDomainNameProvided)
+}
+
+func TestRequireUsername(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "thing",
+ }
+
+ tokenPostErr(t, options, ErrUsernameRequired)
+}
+
+func TestRequirePassword(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ }
+
+ tokenPostErr(t, options, ErrPasswordRequired)
+}
diff --git a/_site/openstack/identity/v2/tokens/results.go b/_site/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..e88b2c7
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/results.go
@@ -0,0 +1,133 @@
+package tokens
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+)
+
+// Token provides only the most basic information related to an authentication token.
+type Token struct {
+ // 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.
+ ID string
+
+ // ExpiresAt 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.
+ ExpiresAt time.Time
+
+ // Tenant provides information about the tenant to which this token grants access.
+ Tenant tenants.Tenant
+}
+
+// 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 `mapstructure:"tenantId"`
+ PublicURL string `mapstructure:"publicURL"`
+ InternalURL string `mapstructure:"internalURL"`
+ AdminURL string `mapstructure:"adminURL"`
+ Region string `mapstructure:"region"`
+ VersionID string `mapstructure:"versionId"`
+ VersionInfo string `mapstructure:"versionInfo"`
+ VersionList string `mapstructure:"versionList"`
+}
+
+// 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.
+//
+// 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 will contain the provider-specified name for the service.
+ Name string `mapstructure:"name"`
+
+ // Type will contain a type string if OpenStack defines a type for the service.
+ // Otherwise, for provider-specific services, the provider may assign their own type strings.
+ Type string `mapstructure:"type"`
+
+ // Endpoints will let the caller iterate over all the different endpoints that may exist for
+ // the service.
+ Endpoints []Endpoint `mapstructure:"endpoints"`
+}
+
+// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
+type ServiceCatalog struct {
+ Entries []CatalogEntry
+}
+
+// CreateResult defers the interpretation of a created token.
+// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
+type CreateResult struct {
+ gophercloud.CommonResult
+}
+
+// ExtractToken returns the just-created Token from a CreateResult.
+func (result CreateResult) ExtractToken() (*Token, error) {
+ if result.Err != nil {
+ return nil, result.Err
+ }
+
+ var response struct {
+ Access struct {
+ Token struct {
+ Expires string `mapstructure:"expires"`
+ ID string `mapstructure:"id"`
+ Tenant tenants.Tenant `mapstructure:"tenant"`
+ } `mapstructure:"token"`
+ } `mapstructure:"access"`
+ }
+
+ err := mapstructure.Decode(result.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Token{
+ ID: response.Access.Token.ID,
+ ExpiresAt: expiresTs,
+ Tenant: response.Access.Token.Tenant,
+ }, nil
+}
+
+// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
+func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
+ if result.Err != nil {
+ return nil, result.Err
+ }
+
+ var response struct {
+ Access struct {
+ Entries []CatalogEntry `mapstructure:"serviceCatalog"`
+ } `mapstructure:"access"`
+ }
+
+ err := mapstructure.Decode(result.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ return &ServiceCatalog{Entries: response.Access.Entries}, nil
+}
+
+// createErr quickly packs an error in a CreateResult.
+func createErr(err error) CreateResult {
+ return CreateResult{gophercloud.CommonResult{Err: err}}
+}
diff --git a/_site/openstack/identity/v2/tokens/urls.go b/_site/openstack/identity/v2/tokens/urls.go
new file mode 100644
index 0000000..cd4c696
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/urls.go
@@ -0,0 +1,8 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+// CreateURL generates the URL used to create new Tokens.
+func CreateURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("tokens")
+}
diff --git a/_site/openstack/identity/v3/endpoints/doc.go b/_site/openstack/identity/v3/endpoints/doc.go
new file mode 100644
index 0000000..7d38ee3
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/doc.go
@@ -0,0 +1,3 @@
+// Package endpoints queries and manages service endpoints.
+// Reference: http://developer.openstack.org/api-ref-identity-v3.html#endpoints-v3
+package endpoints
diff --git a/_site/openstack/identity/v3/endpoints/errors.go b/_site/openstack/identity/v3/endpoints/errors.go
new file mode 100644
index 0000000..854957f
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/errors.go
@@ -0,0 +1,21 @@
+package endpoints
+
+import "fmt"
+
+func requiredAttribute(attribute string) error {
+ return fmt.Errorf("You must specify %s for this endpoint.", attribute)
+}
+
+var (
+ // ErrAvailabilityRequired is reported if an Endpoint is created without an Availability.
+ ErrAvailabilityRequired = requiredAttribute("an availability")
+
+ // ErrNameRequired is reported if an Endpoint is created without a Name.
+ ErrNameRequired = requiredAttribute("a name")
+
+ // ErrURLRequired is reported if an Endpoint is created without a URL.
+ ErrURLRequired = requiredAttribute("a URL")
+
+ // ErrServiceIDRequired is reported if an Endpoint is created without a ServiceID.
+ ErrServiceIDRequired = requiredAttribute("a serviceID")
+)
diff --git a/_site/openstack/identity/v3/endpoints/requests.go b/_site/openstack/identity/v3/endpoints/requests.go
new file mode 100644
index 0000000..eb52573
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/requests.go
@@ -0,0 +1,144 @@
+package endpoints
+
+import (
+ "strconv"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// EndpointOpts contains the subset of Endpoint attributes that should be used to create or update an Endpoint.
+type EndpointOpts struct {
+ Availability gophercloud.Availability
+ Name string
+ Region string
+ URL string
+ ServiceID string
+}
+
+// Create inserts a new Endpoint into the service catalog.
+// Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required.
+func Create(client *gophercloud.ServiceClient, opts EndpointOpts) CreateResult {
+ // Redefined so that Region can be re-typed as a *string, which can be omitted from the JSON output.
+ type endpoint struct {
+ Interface string `json:"interface"`
+ Name string `json:"name"`
+ Region *string `json:"region,omitempty"`
+ URL string `json:"url"`
+ ServiceID string `json:"service_id"`
+ }
+
+ type request struct {
+ Endpoint endpoint `json:"endpoint"`
+ }
+
+ // Ensure that EndpointOpts is fully populated.
+ if opts.Availability == "" {
+ return createErr(ErrAvailabilityRequired)
+ }
+ if opts.Name == "" {
+ return createErr(ErrNameRequired)
+ }
+ if opts.URL == "" {
+ return createErr(ErrURLRequired)
+ }
+ if opts.ServiceID == "" {
+ return createErr(ErrServiceIDRequired)
+ }
+
+ // Populate the request body.
+ reqBody := request{
+ Endpoint: endpoint{
+ Interface: string(opts.Availability),
+ Name: opts.Name,
+ URL: opts.URL,
+ ServiceID: opts.ServiceID,
+ },
+ }
+ reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
+
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &result.Resp,
+ OkCodes: []int{201},
+ })
+ return result
+}
+
+// ListOpts allows finer control over the the endpoints returned by a List call.
+// All fields are optional.
+type ListOpts struct {
+ Availability gophercloud.Availability
+ ServiceID string
+ Page int
+ PerPage int
+}
+
+// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria.
+func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q := make(map[string]string)
+ if opts.Availability != "" {
+ q["interface"] = string(opts.Availability)
+ }
+ if opts.ServiceID != "" {
+ q["service_id"] = opts.ServiceID
+ }
+ if opts.Page != 0 {
+ q["page"] = strconv.Itoa(opts.Page)
+ }
+ if opts.PerPage != 0 {
+ q["per_page"] = strconv.Itoa(opts.Page)
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return EndpointPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ u := listURL(client) + utils.BuildQuery(q)
+ return pagination.NewPager(client, u, createPage)
+}
+
+// Update changes an existing endpoint with new data.
+// All fields are optional in the provided EndpointOpts.
+func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) UpdateResult {
+ type endpoint struct {
+ Interface *string `json:"interface,omitempty"`
+ Name *string `json:"name,omitempty"`
+ Region *string `json:"region,omitempty"`
+ URL *string `json:"url,omitempty"`
+ ServiceID *string `json:"service_id,omitempty"`
+ }
+
+ type request struct {
+ Endpoint endpoint `json:"endpoint"`
+ }
+
+ reqBody := request{Endpoint: endpoint{}}
+ reqBody.Endpoint.Interface = gophercloud.MaybeString(string(opts.Availability))
+ reqBody.Endpoint.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
+ reqBody.Endpoint.URL = gophercloud.MaybeString(opts.URL)
+ reqBody.Endpoint.ServiceID = gophercloud.MaybeString(opts.ServiceID)
+
+ var result UpdateResult
+ _, result.Err = perigee.Request("PATCH", endpointURL(client, endpointID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &result.Resp,
+ OkCodes: []int{200},
+ })
+ return result
+}
+
+// Delete removes an endpoint from the service catalog.
+func Delete(client *gophercloud.ServiceClient, endpointID string) error {
+ _, err := perigee.Request("DELETE", endpointURL(client, endpointID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return err
+}
diff --git a/_site/openstack/identity/v3/endpoints/requests_test.go b/_site/openstack/identity/v3/endpoints/requests_test.go
new file mode 100644
index 0000000..c30bd55
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/requests_test.go
@@ -0,0 +1,243 @@
+package endpoints
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "abcabcabcabc"
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestCreateSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `
+ {
+ "endpoint": {
+ "interface": "public",
+ "name": "the-endiest-of-points",
+ "region": "underground",
+ "url": "https://1.2.3.4:9000/",
+ "service_id": "asdfasdfasdfasdf"
+ }
+ }
+ `)
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `
+ {
+ "endpoint": {
+ "id": "12",
+ "interface": "public",
+ "links": {
+ "self": "https://localhost:5000/v3/endpoints/12"
+ },
+ "name": "the-endiest-of-points",
+ "region": "underground",
+ "service_id": "asdfasdfasdfasdf",
+ "url": "https://1.2.3.4:9000/"
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ actual, err := Create(client, EndpointOpts{
+ Availability: gophercloud.AvailabilityPublic,
+ Name: "the-endiest-of-points",
+ Region: "underground",
+ URL: "https://1.2.3.4:9000/",
+ ServiceID: "asdfasdfasdfasdf",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unable to create an endpoint: %v", err)
+ }
+
+ expected := &Endpoint{
+ ID: "12",
+ Availability: gophercloud.AvailabilityPublic,
+ Name: "the-endiest-of-points",
+ Region: "underground",
+ ServiceID: "asdfasdfasdfasdf",
+ URL: "https://1.2.3.4:9000/",
+ }
+
+ if !reflect.DeepEqual(actual, expected) {
+ t.Errorf("Expected %#v, was %#v", expected, actual)
+ }
+}
+
+func TestListEndpoints(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "endpoints": [
+ {
+ "id": "12",
+ "interface": "public",
+ "links": {
+ "self": "https://localhost:5000/v3/endpoints/12"
+ },
+ "name": "the-endiest-of-points",
+ "region": "underground",
+ "service_id": "asdfasdfasdfasdf",
+ "url": "https://1.2.3.4:9000/"
+ },
+ {
+ "id": "13",
+ "interface": "internal",
+ "links": {
+ "self": "https://localhost:5000/v3/endpoints/13"
+ },
+ "name": "shhhh",
+ "region": "underground",
+ "service_id": "asdfasdfasdfasdf",
+ "url": "https://1.2.3.4:9001/"
+ }
+ ],
+ "links": {
+ "next": null,
+ "previous": null
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ count := 0
+ List(client, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractEndpoints(page)
+ if err != nil {
+ t.Errorf("Failed to extract endpoints: %v", err)
+ return false, err
+ }
+
+ expected := []Endpoint{
+ Endpoint{
+ ID: "12",
+ Availability: gophercloud.AvailabilityPublic,
+ Name: "the-endiest-of-points",
+ Region: "underground",
+ ServiceID: "asdfasdfasdfasdf",
+ URL: "https://1.2.3.4:9000/",
+ },
+ Endpoint{
+ ID: "13",
+ Availability: gophercloud.AvailabilityInternal,
+ Name: "shhhh",
+ Region: "underground",
+ ServiceID: "asdfasdfasdfasdf",
+ URL: "https://1.2.3.4:9001/",
+ },
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, got %#v", expected, actual)
+ }
+
+ return true, nil
+ })
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestUpdateEndpoint(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PATCH")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `
+ {
+ "endpoint": {
+ "name": "renamed",
+ "region": "somewhere-else"
+ }
+ }
+ `)
+
+ fmt.Fprintf(w, `
+ {
+ "endpoint": {
+ "id": "12",
+ "interface": "public",
+ "links": {
+ "self": "https://localhost:5000/v3/endpoints/12"
+ },
+ "name": "renamed",
+ "region": "somewhere-else",
+ "service_id": "asdfasdfasdfasdf",
+ "url": "https://1.2.3.4:9000/"
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+ actual, err := Update(client, "12", EndpointOpts{
+ Name: "renamed",
+ Region: "somewhere-else",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected error from Update: %v", err)
+ }
+
+ expected := &Endpoint{
+ ID: "12",
+ Availability: gophercloud.AvailabilityPublic,
+ Name: "renamed",
+ Region: "somewhere-else",
+ ServiceID: "asdfasdfasdfasdf",
+ URL: "https://1.2.3.4:9000/",
+ }
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, was %#v", expected, actual)
+ }
+}
+
+func TestDeleteEndpoint(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/endpoints/34", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+
+ err := Delete(client, "34")
+ if err != nil {
+ t.Fatalf("Unexpected error from Delete: %v", err)
+ }
+}
diff --git a/_site/openstack/identity/v3/endpoints/results.go b/_site/openstack/identity/v3/endpoints/results.go
new file mode 100644
index 0000000..d1c2472
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/results.go
@@ -0,0 +1,77 @@
+package endpoints
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Endpoint, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Endpoint `json:"endpoint"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return &res.Endpoint, err
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// createErr quickly wraps an error in a CreateResult.
+func createErr(err error) CreateResult {
+ return CreateResult{commonResult{gophercloud.CommonResult{Err: err}}}
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+ commonResult
+}
+
+// Endpoint describes the entry point for another service's API.
+type Endpoint struct {
+ ID string `mapstructure:"id" json:"id"`
+ Availability gophercloud.Availability `mapstructure:"interface" json:"interface"`
+ Name string `mapstructure:"name" json:"name"`
+ Region string `mapstructure:"region" json:"region"`
+ ServiceID string `mapstructure:"service_id" json:"service_id"`
+ URL string `mapstructure:"url" json:"url"`
+}
+
+// EndpointPage is a single page of Endpoint results.
+type EndpointPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if no Endpoints were returned.
+func (p EndpointPage) IsEmpty() (bool, error) {
+ es, err := ExtractEndpoints(p)
+ if err != nil {
+ return true, err
+ }
+ return len(es) == 0, nil
+}
+
+// ExtractEndpoints extracts an Endpoint slice from a Page.
+func ExtractEndpoints(page pagination.Page) ([]Endpoint, error) {
+ var response struct {
+ Endpoints []Endpoint `mapstructure:"endpoints"`
+ }
+
+ err := mapstructure.Decode(page.(EndpointPage).Body, &response)
+
+ return response.Endpoints, err
+}
diff --git a/_site/openstack/identity/v3/endpoints/urls.go b/_site/openstack/identity/v3/endpoints/urls.go
new file mode 100644
index 0000000..547d7b1
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/urls.go
@@ -0,0 +1,11 @@
+package endpoints
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("endpoints")
+}
+
+func endpointURL(client *gophercloud.ServiceClient, endpointID string) string {
+ return client.ServiceURL("endpoints", endpointID)
+}
diff --git a/_site/openstack/identity/v3/endpoints/urls_test.go b/_site/openstack/identity/v3/endpoints/urls_test.go
new file mode 100644
index 0000000..0b183b7
--- /dev/null
+++ b/_site/openstack/identity/v3/endpoints/urls_test.go
@@ -0,0 +1,23 @@
+package endpoints
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func TestGetListURL(t *testing.T) {
+ client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+ url := listURL(&client)
+ if url != "http://localhost:5000/v3/endpoints" {
+ t.Errorf("Unexpected list URL generated: [%s]", url)
+ }
+}
+
+func TestGetEndpointURL(t *testing.T) {
+ client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+ url := endpointURL(&client, "1234")
+ if url != "http://localhost:5000/v3/endpoints/1234" {
+ t.Errorf("Unexpected service URL generated: [%s]", url)
+ }
+}
diff --git a/_site/openstack/identity/v3/services/doc.go b/_site/openstack/identity/v3/services/doc.go
new file mode 100644
index 0000000..c4772c0
--- /dev/null
+++ b/_site/openstack/identity/v3/services/doc.go
@@ -0,0 +1,4 @@
+/*
+Package services queries and manages the service catalog.
+*/
+package services
diff --git a/_site/openstack/identity/v3/services/requests.go b/_site/openstack/identity/v3/services/requests.go
new file mode 100644
index 0000000..7816aca
--- /dev/null
+++ b/_site/openstack/identity/v3/services/requests.go
@@ -0,0 +1,99 @@
+package services
+
+import (
+ "strconv"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type response struct {
+ Service Service `json:"service"`
+}
+
+// Create adds a new service of the requested type to the catalog.
+func Create(client *gophercloud.ServiceClient, serviceType string) CreateResult {
+ type request struct {
+ Type string `json:"type"`
+ }
+
+ req := request{Type: serviceType}
+
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &req,
+ Results: &result.Resp,
+ OkCodes: []int{201},
+ })
+ return result
+}
+
+// ListOpts allows you to query the List method.
+type ListOpts struct {
+ ServiceType string
+ PerPage int
+ Page int
+}
+
+// List enumerates the services available to a specific user.
+func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q := make(map[string]string)
+ if opts.ServiceType != "" {
+ q["type"] = opts.ServiceType
+ }
+ if opts.Page != 0 {
+ q["page"] = strconv.Itoa(opts.Page)
+ }
+ if opts.PerPage != 0 {
+ q["perPage"] = strconv.Itoa(opts.PerPage)
+ }
+ u := listURL(client) + utils.BuildQuery(q)
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ServicePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ return pagination.NewPager(client, u, createPage)
+}
+
+// Get returns additional information about a service, given its ID.
+func Get(client *gophercloud.ServiceClient, serviceID string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", serviceURL(client, serviceID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ Results: &result.Resp,
+ OkCodes: []int{200},
+ })
+ return result
+}
+
+// Update changes the service type of an existing service.
+func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) UpdateResult {
+ type request struct {
+ Type string `json:"type"`
+ }
+
+ req := request{Type: serviceType}
+
+ var result UpdateResult
+ _, result.Err = perigee.Request("PATCH", serviceURL(client, serviceID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &req,
+ Results: &result.Resp,
+ OkCodes: []int{200},
+ })
+ return result
+}
+
+// Delete removes an existing service.
+// It either deletes all associated endpoints, or fails until all endpoints are deleted.
+func Delete(client *gophercloud.ServiceClient, serviceID string) error {
+ _, err := perigee.Request("DELETE", serviceURL(client, serviceID), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return err
+}
diff --git a/_site/openstack/identity/v3/services/requests_test.go b/_site/openstack/identity/v3/services/requests_test.go
new file mode 100644
index 0000000..a3d345b
--- /dev/null
+++ b/_site/openstack/identity/v3/services/requests_test.go
@@ -0,0 +1,232 @@
+package services
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "111111"
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: tokenID,
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestCreateSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "type": "compute" }`)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "service": {
+ "description": "Here's your service",
+ "id": "1234",
+ "name": "InscrutableOpenStackProjectName",
+ "type": "compute"
+ }
+ }`)
+ })
+
+ client := serviceClient()
+
+ result, err := Create(client, "compute").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected error from Create: %v", err)
+ }
+
+ if result.Description == nil || *result.Description != "Here's your service" {
+ t.Errorf("Service description was unexpected [%s]", result.Description)
+ }
+ if result.ID != "1234" {
+ t.Errorf("Service ID was unexpected [%s]", result.ID)
+ }
+ if result.Name != "InscrutableOpenStackProjectName" {
+ t.Errorf("Service name was unexpected [%s]", result.Name)
+ }
+ if result.Type != "compute" {
+ t.Errorf("Service type was unexpected [%s]", result.Type)
+ }
+}
+
+func TestListSinglePage(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "links": {
+ "next": null,
+ "previous": null
+ },
+ "services": [
+ {
+ "description": "Service One",
+ "id": "1234",
+ "name": "service-one",
+ "type": "identity"
+ },
+ {
+ "description": "Service Two",
+ "id": "9876",
+ "name": "service-two",
+ "type": "compute"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ count := 0
+ err := List(client, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractServices(page)
+ if err != nil {
+ return false, err
+ }
+
+ desc0 := "Service One"
+ desc1 := "Service Two"
+ expected := []Service{
+ Service{
+ Description: &desc0,
+ ID: "1234",
+ Name: "service-one",
+ Type: "identity",
+ },
+ Service{
+ Description: &desc1,
+ ID: "9876",
+ Name: "service-two",
+ Type: "compute",
+ },
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, got %#v", expected, actual)
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ t.Errorf("Unexpected error while paging: %v", err)
+ }
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGetSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "service": {
+ "description": "Service One",
+ "id": "12345",
+ "name": "service-one",
+ "type": "identity"
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ result, err := Get(client, "12345").Extract()
+ if err != nil {
+ t.Fatalf("Error fetching service information: %v", err)
+ }
+
+ if result.ID != "12345" {
+ t.Errorf("Unexpected service ID: %s", result.ID)
+ }
+ if *result.Description != "Service One" {
+ t.Errorf("Unexpected service description: [%s]", *result.Description)
+ }
+ if result.Name != "service-one" {
+ t.Errorf("Unexpected service name: [%s]", result.Name)
+ }
+ if result.Type != "identity" {
+ t.Errorf("Unexpected service type: [%s]", result.Type)
+ }
+}
+
+func TestUpdateSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PATCH")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "type": "lasermagic" }`)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "service": {
+ "id": "12345",
+ "type": "lasermagic"
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+
+ result, err := Update(client, "12345", "lasermagic").Extract()
+ if err != nil {
+ t.Fatalf("Unable to update service: %v", err)
+ }
+
+ if result.ID != "12345" {
+ t.Fatalf("Expected ID 12345, was %s", result.ID)
+ }
+}
+
+func TestDeleteSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+
+ err := Delete(client, "12345")
+ if err != nil {
+ t.Fatalf("Unable to delete service: %v", err)
+ }
+}
diff --git a/_site/openstack/identity/v3/services/results.go b/_site/openstack/identity/v3/services/results.go
new file mode 100644
index 0000000..e4e068b
--- /dev/null
+++ b/_site/openstack/identity/v3/services/results.go
@@ -0,0 +1,75 @@
+package services
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Service, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Service `json:"service"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+
+ return &res.Service, err
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult is the deferred result of a Get call.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+ commonResult
+}
+
+// Service is the result of a list or information query.
+type Service struct {
+ Description *string `json:"description,omitempty"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+// ServicePage is a single page of Service results.
+type ServicePage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if the page contains no results.
+func (p ServicePage) IsEmpty() (bool, error) {
+ services, err := ExtractServices(p)
+ if err != nil {
+ return true, err
+ }
+ return len(services) == 0, nil
+}
+
+// ExtractServices extracts a slice of Services from a Collection acquired from List.
+func ExtractServices(page pagination.Page) ([]Service, error) {
+ var response struct {
+ Services []Service `mapstructure:"services"`
+ }
+
+ err := mapstructure.Decode(page.(ServicePage).Body, &response)
+ return response.Services, err
+}
diff --git a/_site/openstack/identity/v3/services/urls.go b/_site/openstack/identity/v3/services/urls.go
new file mode 100644
index 0000000..85443a4
--- /dev/null
+++ b/_site/openstack/identity/v3/services/urls.go
@@ -0,0 +1,11 @@
+package services
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("services")
+}
+
+func serviceURL(client *gophercloud.ServiceClient, serviceID string) string {
+ return client.ServiceURL("services", serviceID)
+}
diff --git a/_site/openstack/identity/v3/services/urls_test.go b/_site/openstack/identity/v3/services/urls_test.go
new file mode 100644
index 0000000..5a31b32
--- /dev/null
+++ b/_site/openstack/identity/v3/services/urls_test.go
@@ -0,0 +1,23 @@
+package services
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func TestListURL(t *testing.T) {
+ client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+ url := listURL(&client)
+ if url != "http://localhost:5000/v3/services" {
+ t.Errorf("Unexpected list URL generated: [%s]", url)
+ }
+}
+
+func TestServiceURL(t *testing.T) {
+ client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+ url := serviceURL(&client, "1234")
+ if url != "http://localhost:5000/v3/services/1234" {
+ t.Errorf("Unexpected service URL generated: [%s]", url)
+ }
+}
diff --git a/_site/openstack/identity/v3/tokens/doc.go b/_site/openstack/identity/v3/tokens/doc.go
new file mode 100644
index 0000000..02fce0d
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/doc.go
@@ -0,0 +1,6 @@
+/*
+Package tokens defines operations performed on the token resource.
+
+Documentation: http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3
+*/
+package tokens
diff --git a/_site/openstack/identity/v3/tokens/errors.go b/_site/openstack/identity/v3/tokens/errors.go
new file mode 100644
index 0000000..4476109
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/errors.go
@@ -0,0 +1,72 @@
+package tokens
+
+import (
+ "errors"
+ "fmt"
+)
+
+func unacceptedAttributeErr(attribute string) error {
+ return fmt.Errorf("The base Identity V3 API does not accept authentication by %s", attribute)
+}
+
+func redundantWithTokenErr(attribute string) error {
+ return fmt.Errorf("%s may not be provided when authenticating with a TokenID", attribute)
+}
+
+func redundantWithUserID(attribute string) error {
+ return fmt.Errorf("%s may not be provided when authenticating with a UserID", attribute)
+}
+
+var (
+ // ErrAPIKeyProvided indicates that an APIKey was provided but can't be used.
+ ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
+ // ErrTenantIDProvided indicates that a TenantID was provided but can't be used.
+ ErrTenantIDProvided = unacceptedAttributeErr("TenantID")
+
+ // ErrTenantNameProvided indicates that a TenantName was provided but can't be used.
+ ErrTenantNameProvided = unacceptedAttributeErr("TenantName")
+
+ // ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead.
+ ErrUsernameWithToken = redundantWithTokenErr("Username")
+
+ // ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead.
+ ErrUserIDWithToken = redundantWithTokenErr("UserID")
+
+ // ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead.
+ ErrDomainIDWithToken = redundantWithTokenErr("DomainID")
+
+ // ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s
+ ErrDomainNameWithToken = redundantWithTokenErr("DomainName")
+
+ // ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once.
+ ErrUsernameOrUserID = errors.New("Exactly one of Username and UserID must be provided for password authentication")
+
+ // ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used.
+ ErrDomainIDWithUserID = redundantWithUserID("DomainID")
+
+ // ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used.
+ ErrDomainNameWithUserID = redundantWithUserID("DomainName")
+
+ // ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it.
+ // It may also indicate that both a DomainID and a DomainName were provided at once.
+ ErrDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName to authenticate by Username")
+
+ // ErrMissingPassword indicates that no password was provided and no token is available.
+ ErrMissingPassword = errors.New("You must provide a password to authenticate")
+
+ // ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present.
+ ErrScopeDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName in a Scope with ProjectName")
+
+ // ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope.
+ ErrScopeProjectIDOrProjectName = errors.New("You must provide at most one of ProjectID or ProjectName in a Scope")
+
+ // ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope.
+ ErrScopeProjectIDAlone = errors.New("ProjectID must be supplied alone in a Scope")
+
+ // ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
+ ErrScopeDomainName = errors.New("DomainName must be supplied with a ProjectName or ProjectID in a Scope.")
+
+ // ErrScopeEmpty indicates that no credentials were provided in a Scope.
+ ErrScopeEmpty = errors.New("You must provide either a Project or Domain in a Scope")
+)
diff --git a/_site/openstack/identity/v3/tokens/requests.go b/_site/openstack/identity/v3/tokens/requests.go
new file mode 100644
index 0000000..c8587b6
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/requests.go
@@ -0,0 +1,285 @@
+package tokens
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// Scope allows a created token to be limited to a specific domain or project.
+type Scope struct {
+ ProjectID string
+ ProjectName string
+ DomainID string
+ DomainName string
+}
+
+func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
+ h := c.Provider.AuthenticatedHeaders()
+ h["X-Subject-Token"] = subjectToken
+ return h
+}
+
+// Create authenticates and either generates a new token, or changes the Scope of an existing token.
+func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
+ type domainReq struct {
+ ID *string `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ }
+
+ type projectReq struct {
+ Domain *domainReq `json:"domain,omitempty"`
+ Name *string `json:"name,omitempty"`
+ ID *string `json:"id,omitempty"`
+ }
+
+ type userReq struct {
+ ID *string `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ Password string `json:"password"`
+ Domain *domainReq `json:"domain,omitempty"`
+ }
+
+ type passwordReq struct {
+ User userReq `json:"user"`
+ }
+
+ type tokenReq struct {
+ ID string `json:"id"`
+ }
+
+ type identityReq struct {
+ Methods []string `json:"methods"`
+ Password *passwordReq `json:"password,omitempty"`
+ Token *tokenReq `json:"token,omitempty"`
+ }
+
+ type scopeReq struct {
+ Domain *domainReq `json:"domain,omitempty"`
+ Project *projectReq `json:"project,omitempty"`
+ }
+
+ type authReq struct {
+ Identity identityReq `json:"identity"`
+ Scope *scopeReq `json:"scope,omitempty"`
+ }
+
+ type request struct {
+ Auth authReq `json:"auth"`
+ }
+
+ // Populate the request structure based on the provided arguments. Create and return an error
+ // if insufficient or incompatible information is present.
+ var req request
+
+ // Test first for unrecognized arguments.
+ if options.APIKey != "" {
+ return createErr(ErrAPIKeyProvided)
+ }
+ if options.TenantID != "" {
+ return createErr(ErrTenantIDProvided)
+ }
+ if options.TenantName != "" {
+ return createErr(ErrTenantNameProvided)
+ }
+
+ if options.Password == "" {
+ if c.Provider.TokenID != "" {
+ // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
+ // parameters.
+ if options.Username != "" {
+ return createErr(ErrUsernameWithToken)
+ }
+ if options.UserID != "" {
+ return createErr(ErrUserIDWithToken)
+ }
+ if options.DomainID != "" {
+ return createErr(ErrDomainIDWithToken)
+ }
+ if options.DomainName != "" {
+ return createErr(ErrDomainNameWithToken)
+ }
+
+ // Configure the request for Token authentication.
+ req.Auth.Identity.Methods = []string{"token"}
+ req.Auth.Identity.Token = &tokenReq{
+ ID: c.Provider.TokenID,
+ }
+ } else {
+ // If no password or token ID are available, authentication can't continue.
+ return createErr(ErrMissingPassword)
+ }
+ } else {
+ // Password authentication.
+ req.Auth.Identity.Methods = []string{"password"}
+
+ // At least one of Username and UserID must be specified.
+ if options.Username == "" && options.UserID == "" {
+ return createErr(ErrUsernameOrUserID)
+ }
+
+ if options.Username != "" {
+ // If Username is provided, UserID may not be provided.
+ if options.UserID != "" {
+ return createErr(ErrUsernameOrUserID)
+ }
+
+ // Either DomainID or DomainName must also be specified.
+ if options.DomainID == "" && options.DomainName == "" {
+ return createErr(ErrDomainIDOrDomainName)
+ }
+
+ if options.DomainID != "" {
+ if options.DomainName != "" {
+ return createErr(ErrDomainIDOrDomainName)
+ }
+
+ // Configure the request for Username and Password authentication with a DomainID.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{
+ Name: &options.Username,
+ Password: options.Password,
+ Domain: &domainReq{ID: &options.DomainID},
+ },
+ }
+ }
+
+ if options.DomainName != "" {
+ // Configure the request for Username and Password authentication with a DomainName.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{
+ Name: &options.Username,
+ Password: options.Password,
+ Domain: &domainReq{Name: &options.DomainName},
+ },
+ }
+ }
+ }
+
+ if options.UserID != "" {
+ // If UserID is specified, neither DomainID nor DomainName may be.
+ if options.DomainID != "" {
+ return createErr(ErrDomainIDWithUserID)
+ }
+ if options.DomainName != "" {
+ return createErr(ErrDomainNameWithUserID)
+ }
+
+ // Configure the request for UserID and Password authentication.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{ID: &options.UserID, Password: options.Password},
+ }
+ }
+ }
+
+ // Add a "scope" element if a Scope has been provided.
+ if scope != nil {
+ if scope.ProjectName != "" {
+ // ProjectName provided: either DomainID or DomainName must also be supplied.
+ // ProjectID may not be supplied.
+ if scope.DomainID == "" && scope.DomainName == "" {
+ return createErr(ErrScopeDomainIDOrDomainName)
+ }
+ if scope.ProjectID != "" {
+ return createErr(ErrScopeProjectIDOrProjectName)
+ }
+
+ if scope.DomainID != "" {
+ // ProjectName + DomainID
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{
+ Name: &scope.ProjectName,
+ Domain: &domainReq{ID: &scope.DomainID},
+ },
+ }
+ }
+
+ if scope.DomainName != "" {
+ // ProjectName + DomainName
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{
+ Name: &scope.ProjectName,
+ Domain: &domainReq{Name: &scope.DomainName},
+ },
+ }
+ }
+ } else if scope.ProjectID != "" {
+ // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
+ if scope.DomainID != "" {
+ return createErr(ErrScopeProjectIDAlone)
+ }
+ if scope.DomainName != "" {
+ return createErr(ErrScopeProjectIDAlone)
+ }
+
+ // ProjectID
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{ID: &scope.ProjectID},
+ }
+ } else if scope.DomainID != "" {
+ // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
+ if scope.DomainName != "" {
+ return createErr(ErrScopeDomainIDOrDomainName)
+ }
+
+ // DomainID
+ req.Auth.Scope = &scopeReq{
+ Domain: &domainReq{ID: &scope.DomainID},
+ }
+ } else if scope.DomainName != "" {
+ return createErr(ErrScopeDomainName)
+ } else {
+ return createErr(ErrScopeEmpty)
+ }
+ }
+
+ var result CreateResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("POST", tokenURL(c), perigee.Options{
+ ReqBody: &req,
+ Results: &result.Resp,
+ OkCodes: []int{201},
+ })
+ if result.Err != nil {
+ return result
+ }
+ result.header = response.HttpResponse.Header
+ return result
+}
+
+// Get validates and retrieves information about another token.
+func Get(c *gophercloud.ServiceClient, token string) GetResult {
+ var result GetResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("GET", tokenURL(c), perigee.Options{
+ MoreHeaders: subjectTokenHeaders(c, token),
+ Results: &result.Resp,
+ OkCodes: []int{200, 203},
+ })
+ if result.Err != nil {
+ return result
+ }
+ result.header = response.HttpResponse.Header
+ return result
+}
+
+// Validate determines if a specified token is valid or not.
+func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
+ response, err := perigee.Request("HEAD", tokenURL(c), perigee.Options{
+ MoreHeaders: subjectTokenHeaders(c, token),
+ OkCodes: []int{204, 404},
+ })
+ if err != nil {
+ return false, err
+ }
+
+ return response.StatusCode == 204, nil
+}
+
+// Revoke immediately makes specified token invalid.
+func Revoke(c *gophercloud.ServiceClient, token string) error {
+ _, err := perigee.Request("DELETE", tokenURL(c), perigee.Options{
+ MoreHeaders: subjectTokenHeaders(c, token),
+ OkCodes: []int{204},
+ })
+ return err
+}
diff --git a/_site/openstack/identity/v3/tokens/requests_test.go b/_site/openstack/identity/v3/tokens/requests_test.go
new file mode 100644
index 0000000..367c73c
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/requests_test.go
@@ -0,0 +1,516 @@
+package tokens
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+// authTokenPost verifies that providing certain AuthOptions and Scope results in an expected JSON structure.
+func authTokenPost(t *testing.T, options gophercloud.AuthOptions, scope *Scope, requestJSON string) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: "12345abcdef",
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "Content-Type", "application/json")
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestJSONRequest(t, r, requestJSON)
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
+ })
+
+ _, err := Create(&client, options, scope).Extract()
+ if err != nil {
+ t.Errorf("Create returned an error: %v", err)
+ }
+}
+
+func authTokenPostErr(t *testing.T, options gophercloud.AuthOptions, scope *Scope, includeToken bool, expectedErr error) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{},
+ Endpoint: testhelper.Endpoint(),
+ }
+ if includeToken {
+ client.Provider.TokenID = "abcdef123456"
+ }
+
+ _, err := Create(&client, options, scope).Extract()
+ if err == nil {
+ t.Errorf("Create did NOT return an error")
+ }
+ if err != expectedErr {
+ t.Errorf("Create returned an unexpected error: wanted %v, got %v", expectedErr, err)
+ }
+}
+
+func TestCreateUserIDAndPassword(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{UserID: "me", Password: "squirrel!"}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": { "id": "me", "password": "squirrel!" }
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateUsernameDomainIDPassword(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{Username: "fakey", Password: "notpassword", DomainID: "abc123"}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "domain": {
+ "id": "abc123"
+ },
+ "name": "fakey",
+ "password": "notpassword"
+ }
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateUsernameDomainNamePassword(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{Username: "frank", Password: "swordfish", DomainName: "spork.net"}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "domain": {
+ "name": "spork.net"
+ },
+ "name": "frank",
+ "password": "swordfish"
+ }
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateTokenID(t *testing.T) {
+ authTokenPost(t, gophercloud.AuthOptions{}, nil, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["token"],
+ "token": {
+ "id": "12345abcdef"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateProjectIDScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{ProjectID: "123456"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "id": "123456"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateDomainIDScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{DomainID: "1000"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "domain": {
+ "id": "1000"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateProjectNameAndDomainIDScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{ProjectName: "world-domination", DomainID: "1000"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "domain": {
+ "id": "1000"
+ },
+ "name": "world-domination"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateProjectNameAndDomainNameScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+ scope := &Scope{ProjectName: "world-domination", DomainName: "evil-plans"}
+ authTokenPost(t, options, scope, `
+ {
+ "auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "id": "fenris",
+ "password": "g0t0h311"
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "domain": {
+ "name": "evil-plans"
+ },
+ "name": "world-domination"
+ }
+ }
+ }
+ }
+ `)
+}
+
+func TestCreateExtractsTokenFromResponse(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{},
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("X-Subject-Token", "aaa111")
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
+ })
+
+ options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"}
+ token, err := Create(&client, options, nil).Extract()
+ if err != nil {
+ t.Fatalf("Create returned an error: %v", err)
+ }
+
+ if token.ID != "aaa111" {
+ t.Errorf("Expected token to be aaa111, but was %s", token.ID)
+ }
+}
+
+func TestCreateFailureEmptyAuth(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{}, nil, false, ErrMissingPassword)
+}
+
+func TestCreateFailureAPIKey(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{APIKey: "something"}, nil, false, ErrAPIKeyProvided)
+}
+
+func TestCreateFailureTenantID(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{TenantID: "something"}, nil, false, ErrTenantIDProvided)
+}
+
+func TestCreateFailureTenantName(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{TenantName: "something"}, nil, false, ErrTenantNameProvided)
+}
+
+func TestCreateFailureTokenIDUsername(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{Username: "something"}, nil, true, ErrUsernameWithToken)
+}
+
+func TestCreateFailureTokenIDUserID(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{UserID: "something"}, nil, true, ErrUserIDWithToken)
+}
+
+func TestCreateFailureTokenIDDomainID(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{DomainID: "something"}, nil, true, ErrDomainIDWithToken)
+}
+
+func TestCreateFailureTokenIDDomainName(t *testing.T) {
+ authTokenPostErr(t, gophercloud.AuthOptions{DomainName: "something"}, nil, true, ErrDomainNameWithToken)
+}
+
+func TestCreateFailureMissingUser(t *testing.T) {
+ options := gophercloud.AuthOptions{Password: "supersecure"}
+ authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID)
+}
+
+func TestCreateFailureBothUser(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "supersecure",
+ Username: "oops",
+ UserID: "redundancy",
+ }
+ authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID)
+}
+
+func TestCreateFailureMissingDomain(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "supersecure",
+ Username: "notuniqueenough",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName)
+}
+
+func TestCreateFailureBothDomain(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "supersecure",
+ Username: "someone",
+ DomainID: "hurf",
+ DomainName: "durf",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName)
+}
+
+func TestCreateFailureUserIDDomainID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ UserID: "100",
+ Password: "stuff",
+ DomainID: "oops",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainIDWithUserID)
+}
+
+func TestCreateFailureUserIDDomainName(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ UserID: "100",
+ Password: "sssh",
+ DomainName: "oops",
+ }
+ authTokenPostErr(t, options, nil, false, ErrDomainNameWithUserID)
+}
+
+func TestCreateFailureScopeProjectNameAlone(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectName: "notenough"}
+ authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName)
+}
+
+func TestCreateFailureScopeProjectNameAndID(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectName: "whoops", ProjectID: "toomuch", DomainID: "1234"}
+ authTokenPostErr(t, options, scope, false, ErrScopeProjectIDOrProjectName)
+}
+
+func TestCreateFailureScopeProjectIDAndDomainID(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectID: "toomuch", DomainID: "notneeded"}
+ authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone)
+}
+
+func TestCreateFailureScopeProjectIDAndDomainNAme(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{ProjectID: "toomuch", DomainName: "notneeded"}
+ authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone)
+}
+
+func TestCreateFailureScopeDomainIDAndDomainName(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{DomainID: "toomuch", DomainName: "notneeded"}
+ authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName)
+}
+
+func TestCreateFailureScopeDomainNameAlone(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{DomainName: "notenough"}
+ authTokenPostErr(t, options, scope, false, ErrScopeDomainName)
+}
+
+func TestCreateFailureEmptyScope(t *testing.T) {
+ options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+ scope := &Scope{}
+ authTokenPostErr(t, options, scope, false, ErrScopeEmpty)
+}
+
+func TestGetRequest(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: "12345abcdef",
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "Content-Type", "")
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+ testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ { "token": { "expires_at": "2014-08-29T13:10:01.000000Z" } }
+ `)
+ })
+
+ token, err := Get(&client, "abcdef12345").Extract()
+ if err != nil {
+ t.Errorf("Info returned an error: %v", err)
+ }
+
+ expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014")
+ if token.ExpiresAt != expected {
+ t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate))
+ }
+}
+
+func prepareAuthTokenHandler(t *testing.T, expectedMethod string, status int) gophercloud.ServiceClient {
+ client := gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: "12345abcdef",
+ },
+ Endpoint: testhelper.Endpoint(),
+ }
+
+ testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, expectedMethod)
+ testhelper.TestHeader(t, r, "Content-Type", "")
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+ testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+
+ w.WriteHeader(status)
+ })
+
+ return client
+}
+
+func TestValidateRequestSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "HEAD", http.StatusNoContent)
+
+ ok, err := Validate(&client, "abcdef12345")
+ if err != nil {
+ t.Errorf("Unexpected error from Validate: %v", err)
+ }
+
+ if !ok {
+ t.Errorf("Validate returned false for a valid token")
+ }
+}
+
+func TestValidateRequestFailure(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "HEAD", http.StatusNotFound)
+
+ ok, err := Validate(&client, "abcdef12345")
+ if err != nil {
+ t.Errorf("Unexpected error from Validate: %v", err)
+ }
+
+ if ok {
+ t.Errorf("Validate returned true for an invalid token")
+ }
+}
+
+func TestValidateRequestError(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "HEAD", http.StatusUnauthorized)
+
+ _, err := Validate(&client, "abcdef12345")
+ if err == nil {
+ t.Errorf("Missing expected error from Validate")
+ }
+}
+
+func TestRevokeRequestSuccessful(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "DELETE", http.StatusNoContent)
+
+ err := Revoke(&client, "abcdef12345")
+ if err != nil {
+ t.Errorf("Unexpected error from Revoke: %v", err)
+ }
+}
+
+func TestRevokeRequestError(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ client := prepareAuthTokenHandler(t, "DELETE", http.StatusNotFound)
+
+ err := Revoke(&client, "abcdef12345")
+ if err == nil {
+ t.Errorf("Missing expected error from Revoke")
+ }
+}
diff --git a/_site/openstack/identity/v3/tokens/results.go b/_site/openstack/identity/v3/tokens/results.go
new file mode 100644
index 0000000..1be98cb
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/results.go
@@ -0,0 +1,75 @@
+package tokens
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+)
+
+// commonResult is the deferred result of a Create or a Get call.
+type commonResult struct {
+ gophercloud.CommonResult
+
+ // header stores the headers from the original HTTP response because token responses are returned in an X-Subject-Token header.
+ header http.Header
+}
+
+// Extract interprets a commonResult as a Token.
+func (r commonResult) Extract() (*Token, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Token struct {
+ ExpiresAt string `mapstructure:"expires_at"`
+ } `mapstructure:"token"`
+ }
+
+ var token Token
+
+ // Parse the token itself from the stored headers.
+ token.ID = r.header.Get("X-Subject-Token")
+
+ err := mapstructure.Decode(r.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ // Attempt to parse the timestamp.
+ token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
+
+ return &token, err
+}
+
+// CreateResult is the deferred response from a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// createErr quickly creates a CreateResult that reports an error.
+func createErr(err error) CreateResult {
+ return CreateResult{
+ commonResult: commonResult{
+ CommonResult: gophercloud.CommonResult{Err: err},
+ header: nil,
+ },
+ }
+}
+
+// GetResult is the deferred response from a Get call.
+type GetResult struct {
+ commonResult
+}
+
+// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
+// Each Token is valid for a set length of time.
+type Token struct {
+ // ID is the issued token.
+ ID string
+
+ // ExpiresAt is the timestamp at which this token will no longer be accepted.
+ ExpiresAt time.Time
+}
diff --git a/_site/openstack/identity/v3/tokens/urls.go b/_site/openstack/identity/v3/tokens/urls.go
new file mode 100644
index 0000000..360b60a
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/urls.go
@@ -0,0 +1,7 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+func tokenURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("auth", "tokens")
+}
diff --git a/_site/openstack/identity/v3/tokens/urls_test.go b/_site/openstack/identity/v3/tokens/urls_test.go
new file mode 100644
index 0000000..549c398
--- /dev/null
+++ b/_site/openstack/identity/v3/tokens/urls_test.go
@@ -0,0 +1,21 @@
+package tokens
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTokenURL(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()}
+
+ expected := testhelper.Endpoint() + "auth/tokens"
+ actual := tokenURL(&client)
+ if actual != expected {
+ t.Errorf("Expected URL %s, but was %s", expected, actual)
+ }
+}