Merge pull request #9 from rackspace/authentication
Add Authentication functionality.
Now that I have the time to write this up, here's the description that should have gone into the git commit message.
gophercloud needs to authenticate against a provider. However, gorax's API isn't ideal from a multi-provider perspective. Thus, instead of requiring the user to instantiate Identity objects, configuring them, and then authenticating in a 3-step process, I create a single public function, Authenticate(), which performs (essentially) these tasks.
I cannot predict the future, and cannot guarantee Identity V3 compatibility in its current form. However, in an attempt to anticipate the future, the Authenticate function is designed to automatically guess which Identity API you intend on using based on which set of credentials you provide it. The underlying assumption is that a V3 token is compatible with a V2 token; once we have the token, it should be usable with other V2 and V3 APIs as appropriate.
Unlike Ruby or Python, Go lacks support for keyword arguments. There are two ways to overcome this deficiency: (1) Make a function that accepts one or more interface{} types, and rely on type-checks to disambiguate meaning from supplied parameters; and, (2) use a structure and rely upon Go's automatic initialization of unspecified fields to well-known "zero" values. Here's a comparison of the two approaches from the point of view of the caller:
// option 1 -- use list of interface{} types
acc, err := gophercloud.Authenticate("rackspace-us", gophercloud.Username("sfalvo"), gophercloud.Password("my-pass-here"), gophercloud.TenantId("blah"))
// option 2 -- use of a dedicated options structure
accRackspace, err := gophercloud.Authenticate("rackspace-us", gophercloud.AuthOptions{
Username: "sfalvo",
Password: "my-pass-here",
TenantId: "blah",
})
As can be seen, the latter requires much less physical typing (assuming one doesn't rename the gophercloud package to just 'g' in the import statement), and thus less chance for error. That's why I decided to use an options structure instead. It also impacts the design of the callee as well; with option (1), I'd have to manually loop through all the parameters, using a type-case statement to decode the supplied parameters and fill in variables as they're discovered, while in (2) I just inspect the options structure directly. Less code means fewer bugs.
Since the method of authentication remains the same across all providers, assuming universal use of V2 APIs, I associate an AuthEndpoint field with each Provider instance. That's the only per-provider piece of information defined at the moment.
Most other SDKs hard-wire their providers; however, this is grossly inconvenient for unit-testing purposes. Therefore, I wrap what would otherwise be global state into a Context structure. TestContext exists to create a blank context, which can be used by unit tests at will. You'll notice that the init() function (in the api.go file) uses it to create the one, true, global context, and pre-populates it with the otherwise statically defined list of providers. Through this mechanism, users of the library needn't concern themselves with contexts and their proper initialization. Instead, they can just use the package-global functions, and they should "just work."
Note that the result of Authenticate() is a structure instance, allowing the client access to the service catalog, tenant ID information, and user information. As we flesh out additional APIs for the Go SDK, we will add methods to this Access structure, allowing more convenient access to various APIs. For example, one hypothetical approach to working with Cloud Compute services would involve using Access as a factory:
compute, err := accRackspace.CloudComputeApi()
This conforms to the first two levels of the desired <organization.service.entity.method> SDK organization, and provides the appropriate propegation of state that allows for token re-auth in a fully transparent manner, if necessary.