diff --git a/internal/jujuapi/api.go b/internal/jujuapi/api.go new file mode 100644 index 000000000..50e39a0c5 --- /dev/null +++ b/internal/jujuapi/api.go @@ -0,0 +1,33 @@ +// Copyright 2016 Canonical Ltd. + +// Package jujuapi implements API endpoints for the juju API. +package jujuapi + +import ( + "net/http" + + "github.com/juju/httprequest" + "github.com/julienschmidt/httprouter" + + "github.com/CanonicalLtd/jem/internal/jem" + "github.com/CanonicalLtd/jem/internal/jemserver" +) + +func NewAPIHandler(jp *jem.Pool, sp jemserver.Params) ([]httprequest.Handler, error) { + return []httprequest.Handler{ + newWebSocketHandler(jp), + }, nil +} + +func newWebSocketHandler(jp *jem.Pool) httprequest.Handler { + return httprequest.Handler{ + Method: "GET", + Path: "/model/:modeluuid/api", + Handle: func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + j := jp.JEM() + defer j.Close() + wsServer := newWSServer(j, p.ByName("modeluuid")) + wsServer.ServeHTTP(w, r) + }, + } +} diff --git a/internal/jujuapi/package_test.go b/internal/jujuapi/package_test.go new file mode 100644 index 000000000..e9f8e3562 --- /dev/null +++ b/internal/jujuapi/package_test.go @@ -0,0 +1,13 @@ +// Copyright 2015 Canonical Ltd. + +package jujuapi_test + +import ( + "testing" + + jujutesting "github.com/juju/juju/testing" +) + +func TestPackage(t *testing.T) { + jujutesting.MgoTestPackage(t) +} diff --git a/internal/jujuapi/websocket.go b/internal/jujuapi/websocket.go new file mode 100644 index 000000000..69209a74d --- /dev/null +++ b/internal/jujuapi/websocket.go @@ -0,0 +1,65 @@ +// Copyright 2016 Canonical Ltd. + +package jujuapi + +import ( + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/observer" + "github.com/juju/juju/rpc" + "github.com/juju/juju/rpc/jsoncodec" + "github.com/juju/juju/rpc/rpcreflect" + "golang.org/x/net/websocket" + + "github.com/CanonicalLtd/jem/internal/jem" +) + +// newWSServer creates a new websocket server suitible for handling the api for modelUUID. +func newWSServer(jem *jem.JEM, modelUUID string) websocket.Server { + hnd := wsHandler{ + jem: jem, + modelUUID: modelUUID, + } + return websocket.Server{ + Handler: hnd.handle, + } +} + +// wsHandler is a handler for a particular websocket connection. +type wsHandler struct { + jem *jem.JEM + modelUUID string +} + +// handle handles the connection. +func (h *wsHandler) handle(wsConn *websocket.Conn) { + codec := jsoncodec.NewWebsocket(wsConn) + conn := rpc.NewConn(codec, observer.None()) + + // TODO(mhilton) serve something useful on this connection. + err := common.UnknownModelError(h.modelUUID) + conn.ServeFinder(&errRoot{err}, serverError) + conn.Start() + select { + case <-conn.Dead(): + } + conn.Close() +} + +func serverError(err error) error { + if err := common.ServerError(err); err != nil { + return err + } + return nil +} + +// errRoot implements the API that a client first sees +// when connecting to the API. It exposes the same API as initialRoot, except +// it returns the requested error when the client makes any request. +type errRoot struct { + err error +} + +// FindMethod conforms to the same API as initialRoot, but we'll always return (nil, err) +func (r *errRoot) FindMethod(rootName string, version int, methodName string) (rpcreflect.MethodCaller, error) { + return nil, r.err +} diff --git a/internal/jujuapi/websocket_test.go b/internal/jujuapi/websocket_test.go new file mode 100644 index 000000000..d3580d99b --- /dev/null +++ b/internal/jujuapi/websocket_test.go @@ -0,0 +1,66 @@ +// Copyright 2016 Canonical Ltd. + +package jujuapi_test + +import ( + "bytes" + "encoding/pem" + "net/http/httptest" + "net/url" + + "github.com/juju/juju/api" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + + "github.com/CanonicalLtd/jem/internal/apitest" +) + +type websocketSuite struct { + apitest.Suite + wsServer *httptest.Server + connection api.Connection +} + +var _ = gc.Suite(&websocketSuite{}) + +func (s *websocketSuite) SetUpTest(c *gc.C) { + s.Suite.SetUpTest(c) + s.wsServer = httptest.NewTLSServer(s.JEMSrv) +} + +func (s *websocketSuite) TearDownTest(c *gc.C) { + s.wsServer.Close() + s.Suite.TearDownTest(c) +} + +func (s *websocketSuite) TestUnknownModel(c *gc.C) { + conn := s.open(c, &api.Info{ + ModelTag: names.NewModelTag("00000000-0000-0000-0000-000000000000"), + SkipLogin: true, + }) + defer conn.Close() + err := conn.Login(names.NewUserTag("test-user"), "", "", nil) + c.Assert(err, gc.ErrorMatches, `unknown model: "00000000-0000-0000-0000-000000000000" \(not found\)`) +} + +func (s *websocketSuite) open(c *gc.C, info *api.Info) api.Connection { + inf := *info + u, err := url.Parse(s.wsServer.URL) + c.Assert(err, jc.ErrorIsNil) + inf.Addrs = []string{ + u.Host, + } + w := new(bytes.Buffer) + err = pem.Encode(w, &pem.Block{ + Type: "CERTIFICATE", + Bytes: s.wsServer.TLS.Certificates[0].Certificate[0], + }) + c.Assert(err, jc.ErrorIsNil) + inf.CACert = w.String() + conn, err := api.Open(&inf, api.DialOpts{ + InsecureSkipVerify: true, + }) + c.Assert(err, jc.ErrorIsNil) + return conn +} diff --git a/server.go b/server.go index 55fef920b..8e1448bd2 100644 --- a/server.go +++ b/server.go @@ -12,6 +12,7 @@ import ( "github.com/CanonicalLtd/jem/internal/debugapi" "github.com/CanonicalLtd/jem/internal/jemserver" + "github.com/CanonicalLtd/jem/internal/jujuapi" "github.com/CanonicalLtd/jem/internal/v2" "github.com/CanonicalLtd/jem/params" ) @@ -19,6 +20,7 @@ import ( var versions = map[string]jemserver.NewAPIHandlerFunc{ "v2": v2.NewAPIHandler, "debug": debugapi.NewAPIHandler, + "juju": jujuapi.NewAPIHandler, } // ServerParams holds configuration for a new API server.