apiserver: serve controller-only endpoint from /api #5988

Merged
merged 1 commit into from Aug 15, 2016
Jump to file or symbol
Failed to load files and symbols.
+198 −169
Split
View
@@ -68,13 +68,12 @@ type state struct {
// will be associated with (specifically macaroon auth cookies).
cookieURL *url.URL
- // modelTag holds the model tag once we're connected
- modelTag string
+ // modelTag holds the model tag.
+ // It is empty if there is no model tag associated with the connection.
+ modelTag names.ModelTag
- // controllerTag holds the controller tag once we're connected.
- // This is only set with newer apiservers where they are using
- // the v1 login mechansim.
- controllerTag string
+ // controllerTag holds the controller model's tag once we're connected.
+ controllerTag names.ModelTag
// serverVersion holds the version of the API server that we are
// connected to. It is possible that this version is 0 if the
@@ -230,6 +229,7 @@ func open(
nonce: info.Nonce,
tlsConfig: tlsConfig,
bakeryClient: bakeryClient,
+ modelTag: info.ModelTag,
}
if !info.SkipLogin {
if err := loginFunc(st, info.Tag, info.Password, info.Nonce, info.Macaroons); err != nil {
@@ -302,9 +302,9 @@ func connectWebsocket(info *Info, opts DialOpts) (*websocket.Conn, *tls.Config,
}
tlsConfig.RootCAs = certPool
}
- path := "/"
- if info.ModelTag.Id() != "" {
- path = apiPath(info.ModelTag, "/api")
+ path, err := apiPath(info.ModelTag, "/api")
+ if err != nil {
+ return nil, nil, errors.Trace(err)
}
conn, err := dialWebSocket(info.Addrs, path, tlsConfig, opts)
if err != nil {
@@ -343,7 +343,7 @@ func dialWebSocket(addrs []string, path string, tlsConfig *tls.Config, opts Dial
return result.(*websocket.Conn), nil
}
-// ConnectStream implements Connection.ConnectStream, whatever that is..
+// ConnectStream implements StreamConnector.ConnectStream.
func (st *state) ConnectStream(path string, attrs url.Values) (base.Stream, error) {
if !st.isLoggedIn() {
return nil, errors.New("cannot use ConnectStream without logging in")
@@ -376,18 +376,9 @@ func (st *state) ConnectStream(path string, attrs url.Values) (base.Stream, erro
// ConnectStream only in that it will not retry the connection if it encounters
// discharge-required error.
func (st *state) connectStream(path string, attrs url.Values) (base.Stream, error) {
- if !strings.HasPrefix(path, "/") {
- return nil, errors.New(`path must start with "/"`)
- }
- if _, ok := st.ServerVersion(); ok {
- // If the server version is set, then we know the server is capable of
- // serving streams at the model path. We also fully expect
- // that the server has returned a valid model tag.
- modelTag, err := st.ModelTag()
- if err != nil {
- return nil, errors.Annotate(err, "cannot get model tag, perhaps connected to system not model")
- }
- path = apiPath(modelTag, path)
+ path, err := apiPath(st.modelTag, path)
+ if err != nil {
+ return nil, errors.Trace(err)
}
target := url.URL{
Scheme: "wss",
@@ -459,17 +450,11 @@ func (st *state) addCookiesToHeader(h http.Header) {
}
// apiEndpoint returns a URL that refers to the given API slash-prefixed
-// endpoint path and query parameters. Note that the caller
-// is responsible for ensuring that the path *is* prefixed with a slash.
+// endpoint path and query parameters.
func (st *state) apiEndpoint(path, query string) (*url.URL, error) {
- if _, err := st.ControllerTag(); err == nil {
- // The controller tag is set, so the agent version is >= 1.23,
- // so we can use the model endpoint.
- modelTag, err := st.ModelTag()
- if err != nil {
- return nil, errors.Annotate(err, "cannot get API endpoint address")
- }
- path = apiPath(modelTag, path)
+ path, err := apiPath(st.modelTag, path)
+ if err != nil {
+ return nil, errors.Trace(err)
}
return &url.URL{
Scheme: st.serverScheme,
@@ -480,20 +465,16 @@ func (st *state) apiEndpoint(path, query string) (*url.URL, error) {
}
// apiPath returns the given API endpoint path relative
-// to the given model tag. The caller is responsible
-// for ensuring that the model tag is valid and
-// that the path is slash-prefixed.
-func apiPath(modelTag names.ModelTag, path string) string {
+// to the given model tag.
+func apiPath(modelTag names.ModelTag, path string) (string, error) {
if !strings.HasPrefix(path, "/") {
- panic(fmt.Sprintf("apiPath called with non-slash-prefixed path %q", path))
- }
- if modelTag.Id() == "" {
- panic("apiPath called with empty model tag")
+ return "", errors.Errorf("cannot make API path from non-slash-prefixed path %q", path)
}
- if modelUUID := modelTag.Id(); modelUUID != "" {
- return "/model/" + modelUUID + path
+ modelUUID := modelTag.Id()
+ if modelUUID == "" {
+ return path, nil
}
- return path
+ return "/model/" + modelUUID + path, nil
}
// tagToString returns the value of a tag's String method, or "" if the tag is nil.
@@ -668,14 +649,14 @@ func (s *state) Addr() string {
return s.addr
}
-// ModelTag returns the tag of the model we are connected to.
-func (s *state) ModelTag() (names.ModelTag, error) {
- return names.ParseModelTag(s.modelTag)
+// ModelTag implements base.APICaller.ModelTag.
+func (s *state) ModelTag() (names.ModelTag, bool) {
+ return s.modelTag, s.modelTag.Id() != ""
}
-// ControllerTag returns the tag of the server we are connected to.
-func (s *state) ControllerTag() (names.ModelTag, error) {
- return names.ParseModelTag(s.controllerTag)
+// ControllerTag implements base.APICaller.ControllerTag.
+func (s *state) ControllerTag() names.ModelTag {
+ return s.controllerTag
}
// APIHostPorts returns addresses that may be used to connect
View
@@ -106,8 +106,8 @@ func (s *apiclientSuite) TestOpen(c *gc.C) {
defer st.Close()
c.Assert(st.Addr(), gc.Equals, info.Addrs[0])
- modelTag, err := st.ModelTag()
- c.Assert(err, jc.ErrorIsNil)
+ modelTag, ok := st.ModelTag()
+ c.Assert(ok, jc.IsTrue)
c.Assert(modelTag, gc.Equals, s.State.ModelTag())
remoteVersion, versionSet := st.ServerVersion()
@@ -369,5 +369,5 @@ func assertConnAddrForEnv(c *gc.C, conn *websocket.Conn, addr, modelUUID, tail s
}
func assertConnAddrForRoot(c *gc.C, conn *websocket.Conn, addr string) {
- c.Assert(conn.RemoteAddr(), gc.Matches, "^wss://"+addr+"/$")
+ c.Assert(conn.RemoteAddr(), gc.Matches, "^wss://"+addr+"/api$")
}
@@ -52,10 +52,9 @@ func (c *Client) SetMetricCredentials(service string, credentials []byte) error
// ModelUUID returns the model UUID from the client connection.
func (c *Client) ModelUUID() string {
- tag, err := c.st.ModelTag()
- if err != nil {
- logger.Warningf("model tag not an model: %v", err)
- return ""
+ tag, ok := c.st.ModelTag()
+ if !ok {
+ logger.Warningf("controller-only API connection has no model tag")
}
return tag.Id()
}
View
@@ -26,9 +26,9 @@ type APICaller interface {
// client can use with the current API server.
BestFacadeVersion(facade string) int
- // ModelTag returns the tag of the model the client is
- // connected to.
- ModelTag() (names.ModelTag, error)
+ // ModelTag returns the tag of the model the client is connected
+ // to if there is one. It returns false for a controller-only connection.
+ ModelTag() (names.ModelTag, bool)
// HTTPClient returns an httprequest.Client that can be used
// to make HTTP requests to the API. URLs passed to the client
@@ -32,8 +32,8 @@ func (APICallerFunc) BestFacadeVersion(facade string) int {
return 0
}
-func (APICallerFunc) ModelTag() (names.ModelTag, error) {
- return coretesting.ModelTag, nil
+func (APICallerFunc) ModelTag() (names.ModelTag, bool) {
+ return coretesting.ModelTag, true
}
func (APICallerFunc) Close() error {
View
@@ -185,13 +185,21 @@ func (c *Client) SetModelConstraints(constraints constraints.Value) error {
return c.facade.FacadeCall("SetModelConstraints", params, nil)
}
-// ModelUUID returns the model UUID from the client connection.
-func (c *Client) ModelUUID() (string, error) {
- tag, err := c.st.ModelTag()
- if err != nil {
- return "", errors.Annotate(err, "model tag not an model")
+// ModelUUID returns the model UUID from the client connection
+// and reports whether it is valud.
+func (c *Client) ModelUUID() (string, bool) {
+ tag, ok := c.st.ModelTag()
+ if !ok {
+ return "", false
}
- return tag.Id(), nil
+ return tag.Id(), true
+}
+
+// ModelInfo returns details about the Juju model.
+func (c *Client) ModelInfo() (params.ModelInfo, error) {
+ var info params.ModelInfo
+ err := c.facade.FacadeCall("ModelInfo", nil, &info)
+ return info, err
}
// ModelUserInfo returns information on all users in the model.
View
@@ -306,8 +306,8 @@ func fakeAPIEndpoint(c *gc.C, client *api.Client, address, method string, handle
// envEndpoint returns "/model/<model-uuid>/<destination>"
func envEndpoint(c *gc.C, apiState api.Connection, destination string) string {
- modelTag, err := apiState.ModelTag()
- c.Assert(err, jc.ErrorIsNil)
+ modelTag, ok := apiState.ModelTag()
+ c.Assert(ok, jc.IsTrue)
return path.Join("/model", modelTag.Id(), destination)
}
@@ -316,8 +316,8 @@ func (s *clientSuite) TestClientEnvironmentUUID(c *gc.C) {
c.Assert(err, jc.ErrorIsNil)
client := s.APIState.Client()
- uuid, err := client.ModelUUID()
- c.Assert(err, jc.ErrorIsNil)
+ uuid, ok := client.ModelUUID()
+ c.Assert(ok, jc.IsTrue)
c.Assert(uuid, gc.Equals, environ.Tag().Id())
}
@@ -363,7 +363,7 @@ func (s *clientSuite) TestWatchDebugLogConnected(c *gc.C) {
func (s *clientSuite) TestConnectStreamRequiresSlashPathPrefix(c *gc.C) {
reader, err := s.APIState.ConnectStream("foo", nil)
- c.Assert(err, gc.ErrorMatches, `path must start with "/"`)
+ c.Assert(err, gc.ErrorMatches, `cannot make API path from non-slash-prefixed path "foo"`)
c.Assert(reader, gc.Equals, nil)
}
View
@@ -8,6 +8,7 @@ import (
"github.com/juju/juju/api/base"
"github.com/juju/juju/network"
"github.com/juju/utils/clock"
+ "gopkg.in/juju/names.v2"
)
var (
@@ -53,11 +54,19 @@ type TestingStateParams struct {
// isn't backed onto an actual API server, so actual RPC methods can't be
// called on it. But it can be used for testing general behaviour.
func NewTestingState(params TestingStateParams) Connection {
+ var modelTag names.ModelTag
+ if params.ModelTag != "" {
+ t, err := names.ParseModelTag(params.ModelTag)
+ if err != nil {
+ panic("invalid model tag")
+ }
+ modelTag = t
+ }
st := &state{
client: params.RPCConnection,
clock: params.Clock,
addr: params.Address,
- modelTag: params.ModelTag,
+ modelTag: modelTag,
hostPorts: params.APIHostPorts,
facadeVersions: params.FacadeVersions,
serverScheme: params.ServerScheme,
@@ -41,7 +41,7 @@ func (st *State) BestAPIVersion() int {
}
// ModelTag returns the current model's tag.
-func (st *State) ModelTag() (names.ModelTag, error) {
+func (st *State) ModelTag() (names.ModelTag, bool) {
return st.facade.RawAPICaller().ModelTag()
}
@@ -96,16 +96,15 @@ func (st *State) WatchModelMachines() (watcher.StringsWatcher, error) {
// WatchOpenedPorts returns a StringsWatcher that notifies of
// changes to the opened ports for the current model.
func (st *State) WatchOpenedPorts() (watcher.StringsWatcher, error) {
- modelTag, err := st.ModelTag()
- if err != nil {
- return nil, errors.Annotatef(err, "invalid model tag")
+ modelTag, ok := st.ModelTag()
+ if !ok {
+ return nil, errors.New("API connection is controller-only (should never happen)")
}
var results params.StringsWatchResults
args := params.Entities{
Entities: []params.Entity{{Tag: modelTag.String()}},
}
- err = st.facade.FacadeCall("WatchOpenedPorts", args, &results)
- if err != nil {
+ if err := st.facade.FacadeCall("WatchOpenedPorts", args, &results); err != nil {
return nil, err
}
if len(results.Results) != 1 {
@@ -26,10 +26,7 @@ type Client struct {
// NewClient returns a new HighAvailability client.
func NewClient(caller base.APICallCloser) *Client {
- modelTag, err := caller.ModelTag()
- if err != nil {
- logger.Errorf("ignoring invalid model tag: %v", err)
- }
+ modelTag, _ := caller.ModelTag()
frontend, backend := base.NewClientFacade(caller, "HighAvailability")
return &Client{ClientFacade: frontend, facade: backend, modelTag: modelTag}
}
View
@@ -173,7 +173,7 @@ type Connection interface {
// (as opposed to the model tag of the currently connected
// model inside that controller).
// This could be defined on base.APICaller.
- ControllerTag() (names.ModelTag, error)
+ ControllerTag() names.ModelTag
// All the rest are strange and questionable and deserve extra attention
// and/or discussion.
@@ -29,9 +29,10 @@ var _ MetricsManagerClient = (*Client)(nil)
// NewClient creates a new client for accessing the metricsmanager api
func NewClient(apiCaller base.APICaller) (*Client, error) {
- modelTag, err := apiCaller.ModelTag()
- if err != nil {
- return nil, errors.Trace(err)
+ modelTag, ok := apiCaller.ModelTag()
+ if !ok {
+ return nil, errors.New("metricsmanager client is not appropriate for controller-only API")
+
}
facade := base.NewFacadeCaller(apiCaller, "MetricsManager")
return &Client{
View
@@ -22,9 +22,9 @@ func NewAPI(apiCaller base.APICaller, controllerTag names.MachineTag) (*API, err
if !names.IsValidMachine(controllerId) {
return nil, errors.NotValidf("controller tag")
}
- modelTag, err := apiCaller.ModelTag()
- if err != nil {
- return nil, errors.Trace(err)
+ modelTag, ok := apiCaller.ModelTag()
+ if !ok {
+ return nil, errors.New("cannot use singular API on controller-only connection")
}
facadeCaller := base.NewFacadeCaller(apiCaller, "Singular")
return &API{
@@ -37,10 +37,10 @@ func (s *APISuite) TestBadControllerTag(c *gc.C) {
c.Check(err, gc.ErrorMatches, "controller tag not valid")
}
-func (s *APISuite) TestBadModelTag(c *gc.C) {
+func (s *APISuite) TestControllerOnlyAPI(c *gc.C) {
api, err := singular.NewAPI(mockAPICaller{}, machine123)
c.Check(api, gc.IsNil)
- c.Check(err, gc.ErrorMatches, "no tags for you")
+ c.Check(err, gc.ErrorMatches, `cannot use singular API on controller-only connection`)
}
func (s *APISuite) TestNoCalls(c *gc.C) {
@@ -181,6 +181,6 @@ type mockAPICaller struct {
base.APICaller
}
-func (mockAPICaller) ModelTag() (names.ModelTag, error) {
- return names.ModelTag{}, errors.New("no tags for you")
+func (mockAPICaller) ModelTag() (names.ModelTag, bool) {
+ return names.ModelTag{}, false
}
Oops, something went wrong.