diff --git a/client.go b/client.go
index f736c19..3b346c4 100644
--- a/client.go
+++ b/client.go
@@ -274,6 +274,21 @@ func AddAPIVersionToURL(BaseURL, apiVersion string) string {
return fmt.Sprintf("%sapi/%s/", baseurl, apiVersion)
}
+var apiVersionPattern = regexp.MustCompile(`^(?P.*/)api/(?P\d+\.\d+)/?$`)
+
+//splitVersionedURL splits a versioned API URL (like
+//http://maas.server/MAAS/api/2.0/) into a base URL
+//(http://maas.server/MAAS/) and API version (2.0). If the URL doesn't
+//include a version component the bool return value will be false.
+func splitVersionedURL(url string) (string, string, bool) {
+ if !apiVersionPattern.MatchString(url) {
+ return url, "", false
+ }
+ version := apiVersionPattern.ReplaceAllString(url, "$version")
+ baseURL := apiVersionPattern.ReplaceAllString(url, "$base")
+ return baseURL, version, true
+}
+
// NewAnonymousClient creates a client that issues anonymous requests.
// BaseURL should refer to the root of the MAAS server path, e.g.
// http://my.maas.server.example.com/MAAS/
diff --git a/client_test.go b/client_test.go
index 6b81da3..49a3805 100644
--- a/client_test.go
+++ b/client_test.go
@@ -311,7 +311,20 @@ func (suite *ClientSuite) TestNewAuthenticatedClientFailsIfInvalidKey(c *gc.C) {
}
func (suite *ClientSuite) TestAddAPIVersionToURL(c *gc.C) {
- apiurl := AddAPIVersionToURL("http://example.com/MAAS", "1.0")
- expectedURL := "http://example.com/MAAS/api/1.0/"
- c.Assert(expectedURL, jc.DeepEquals, apiurl)
+ addVersion := AddAPIVersionToURL
+ c.Assert(addVersion("http://example.com/MAAS", "1.0"), gc.Equals, "http://example.com/MAAS/api/1.0/")
+ c.Assert(addVersion("http://example.com/MAAS/", "2.0"), gc.Equals, "http://example.com/MAAS/api/2.0/")
+}
+
+func (suite *ClientSuite) TestSplitVersionedURL(c *gc.C) {
+ check := func(url, expectedBase, expectedVersion string, expectedResult bool) {
+ base, version, ok := splitVersionedURL(url)
+ c.Check(ok, gc.Equals, expectedResult)
+ c.Check(base, gc.Equals, expectedBase)
+ c.Check(version, gc.Equals, expectedVersion)
+ }
+ check("http://maas.server/MAAS", "http://maas.server/MAAS", "", false)
+ check("http://maas.server/MAAS/api/3.0", "http://maas.server/MAAS/", "3.0", true)
+ check("http://maas.server/MAAS/api/3.0/", "http://maas.server/MAAS/", "3.0", true)
+ check("http://maas.server/MAAS/api/maas", "http://maas.server/MAAS/api/maas", "", false)
}
diff --git a/controller.go b/controller.go
index 5a9b846..dceb568 100644
--- a/controller.go
+++ b/controller.go
@@ -44,47 +44,85 @@ type ControllerArgs struct {
APIKey string
}
-// NewController creates an authenticated client to the MAAS API, and checks
-// the capabilities of the server.
+// NewController creates an authenticated client to the MAAS API, and
+// checks the capabilities of the server. If the BaseURL specified
+// includes the API version, that version of the API will be used,
+// otherwise the controller will use the highest supported version
+// available.
//
// If the APIKey is not valid, a NotValid error is returned.
// If the credentials are incorrect, a PermissionError is returned.
func NewController(args ControllerArgs) (Controller, error) {
+ base, apiVersion, includesVersion := splitVersionedURL(args.BaseURL)
+ if includesVersion {
+ if !supportedVersion(apiVersion) {
+ return nil, NewUnsupportedVersionError("version %s", apiVersion)
+ }
+ return newControllerWithVersion(base, apiVersion, args.APIKey)
+ }
+ return newControllerUnknownVersion(args)
+}
+
+func supportedVersion(value string) bool {
+ for _, version := range supportedAPIVersions {
+ if value == version {
+ return true
+ }
+ }
+ return false
+}
+
+func newControllerWithVersion(baseURL, apiVersion, apiKey string) (Controller, error) {
+ major, minor, err := version.ParseMajorMinor(apiVersion)
+ // We should not get an error here. See the test.
+ if err != nil {
+ return nil, errors.Errorf("bad version defined in supported versions: %q", apiVersion)
+ }
+ client, err := NewAuthenticatedClient(AddAPIVersionToURL(baseURL, apiVersion), apiKey)
+ if err != nil {
+ // If the credentials aren't valid, return now.
+ if errors.IsNotValid(err) {
+ return nil, errors.Trace(err)
+ }
+ // Any other error attempting to create the authenticated client
+ // is an unexpected error and return now.
+ return nil, NewUnexpectedError(err)
+ }
+ controllerVersion := version.Number{
+ Major: major,
+ Minor: minor,
+ }
+ controller := &controller{client: client, apiVersion: controllerVersion}
+ // The controllerVersion returned from the function will include any patch version.
+ controller.capabilities, err = controller.readAPIVersionInfo()
+ if err != nil {
+ logger.Debugf("read version failed: %#v", err)
+ return nil, NewBadVersionInfoError(err)
+ }
+
+ if err := controller.checkCreds(); err != nil {
+ return nil, errors.Trace(err)
+ }
+ return controller, nil
+}
+
+func newControllerUnknownVersion(args ControllerArgs) (Controller, error) {
// For now we don't need to test multiple versions. It is expected that at
// some time in the future, we will try the most up to date version and then
// work our way backwards.
for _, apiVersion := range supportedAPIVersions {
- major, minor, err := version.ParseMajorMinor(apiVersion)
- // We should not get an error here. See the test.
- if err != nil {
- return nil, errors.Errorf("bad version defined in supported versions: %q", apiVersion)
- }
- client, err := NewAuthenticatedClient(AddAPIVersionToURL(args.BaseURL, apiVersion), args.APIKey)
- if err != nil {
- // If the credentials aren't valid, return now.
- if errors.IsNotValid(err) {
- return nil, errors.Trace(err)
- }
- // Any other error attempting to create the authenticated client
- // is an unexpected error and return now.
- return nil, NewUnexpectedError(err)
- }
- controllerVersion := version.Number{
- Major: major,
- Minor: minor,
- }
- controller := &controller{client: client, apiVersion: controllerVersion}
- // The controllerVersion returned from the function will include any patch version.
- controller.capabilities, err = controller.readAPIVersionInfo()
- if err != nil {
- logger.Debugf("read version failed: %#v", err)
+ controller, err := newControllerWithVersion(args.BaseURL, apiVersion, args.APIKey)
+ switch {
+ case err == nil:
+ return controller, nil
+ case IsBadVersionInfoError(err):
+ // TODO(babbageclunk): this is bad - it treats transient
+ // network errors the same as version mismatches. See
+ // lp:1667095
continue
- }
-
- if err := controller.checkCreds(); err != nil {
+ default:
return nil, errors.Trace(err)
}
- return controller, nil
}
return nil, NewUnsupportedVersionError("controller at %s does not support any of %s", args.BaseURL, supportedAPIVersions)
diff --git a/controller_test.go b/controller_test.go
index f834e3d..3928e28 100644
--- a/controller_test.go
+++ b/controller_test.go
@@ -132,6 +132,36 @@ func (s *controllerSuite) TestNewControllerUnexpected(c *gc.C) {
c.Assert(err, jc.Satisfies, IsUnexpectedError)
}
+func (s *controllerSuite) TestNewControllerKnownVersion(c *gc.C) {
+ // Using a server URL including the version should work.
+ officialController, err := NewController(ControllerArgs{
+ BaseURL: s.server.URL + "/api/2.0/",
+ APIKey: "fake:as:key",
+ })
+ c.Assert(err, jc.ErrorIsNil)
+ rawController, ok := officialController.(*controller)
+ c.Assert(ok, jc.IsTrue)
+ c.Assert(rawController.apiVersion, gc.Equals, version.Number{
+ Major: 2,
+ Minor: 0,
+ })
+}
+
+func (s *controllerSuite) TestNewControllerUnsupportedVersionSpecified(c *gc.C) {
+ // Ensure the server would actually respond to the version if it
+ // was asked.
+ s.server.AddGetResponse("/api/3.0/users/?op=whoami", http.StatusOK, `"captain awesome"`)
+ s.server.AddGetResponse("/api/3.0/version/", http.StatusOK, versionResponse)
+ // Using a server URL including a version that isn't in the known
+ // set should be denied.
+ controller, err := NewController(ControllerArgs{
+ BaseURL: s.server.URL + "/api/3.0/",
+ APIKey: "fake:as:key",
+ })
+ c.Assert(controller, gc.IsNil)
+ c.Assert(err, jc.Satisfies, IsUnsupportedVersionError)
+}
+
func (s *controllerSuite) TestBootResources(c *gc.C) {
controller := s.getController(c)
resources, err := controller.BootResources()
diff --git a/errors.go b/errors.go
index 8931d56..ff83004 100644
--- a/errors.go
+++ b/errors.go
@@ -65,6 +65,27 @@ func IsUnsupportedVersionError(err error) bool {
return ok
}
+// BadVersionInfoError is more specific than UnsupportedVersionError -
+// it means that we couldn't read the version info endpoint, so the
+// API version is probably wrong. Used in version selection when
+// creating a controller.
+type BadVersionInfoError struct {
+ errors.Err
+}
+
+// NewBadVersionInfoError constructs a new BadVersionInfoError and sets the location.
+func NewBadVersionInfoError(err error) error {
+ uerr := &BadVersionInfoError{Err: errors.NewErr("bad version info: %v", err)}
+ uerr.SetLocation(1)
+ return errors.Wrap(err, uerr)
+}
+
+// IsBadVersionInfoError returns true if err is an BadVersionInfoError.
+func IsBadVersionInfoError(err error) bool {
+ _, ok := errors.Cause(err).(*BadVersionInfoError)
+ return ok
+}
+
// DeserializationError types are returned when the returned JSON data from
// the controller doesn't match the code's expectations.
type DeserializationError struct {
diff --git a/errors_test.go b/errors_test.go
index a625577..1174c46 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -29,6 +29,14 @@ func (*errorTypesSuite) TestUnexpectedError(c *gc.C) {
c.Assert(err.Error(), gc.Equals, "unexpected: wat")
}
+func (*errorTypesSuite) TestNewBadVersionInfoError(c *gc.C) {
+ err := errors.New("wat")
+ err = NewBadVersionInfoError(err)
+ c.Assert(err, gc.NotNil)
+ c.Assert(err, jc.Satisfies, IsBadVersionInfoError)
+ c.Assert(err.Error(), gc.Equals, "bad version info: wat")
+}
+
func (*errorTypesSuite) TestUnsupportedVersionError(c *gc.C) {
err := NewUnsupportedVersionError("foo %d", 42)
c.Assert(err, gc.NotNil)