Skip to content

Commit

Permalink
Move uniter resource endpoint to apiserver
Browse files Browse the repository at this point in the history
This is the last component architecture apiserver part to be
moved. Much unnecessary abstraction and dead code has been removed. The
code is now much more straightforward.
  • Loading branch information
Menno Smits committed Mar 20, 2017
1 parent bf6f948 commit 61010bf
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 592 deletions.
20 changes: 20 additions & 0 deletions apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (
"github.com/juju/juju/apiserver/common/apihttp"
"github.com/juju/juju/apiserver/observer"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/resource"
"github.com/juju/juju/resource/resourceadapters"
"github.com/juju/juju/rpc"
"github.com/juju/juju/rpc/jsoncodec"
"github.com/juju/juju/state"
Expand Down Expand Up @@ -471,6 +473,24 @@ func (srv *Server) endpoints() []apihttp.Endpoint {
return rst, closer, entity.Tag(), nil
},
})
add("/model/:modeluuid/units/:unit/resources/:resource", &UnitResourcesHandler{
NewOpener: func(req *http.Request, tagKinds ...string) (resource.Opener, func(), error) {
st, closer, _, err := httpCtxt.stateForRequestAuthenticatedTag(req, tagKinds...)
if err != nil {
return nil, nil, errors.Trace(err)
}
tagStr := req.URL.Query().Get(":unit")
tag, err := names.ParseUnitTag(tagStr)
if err != nil {
return nil, nil, errors.Trace(err)
}
opener, err := resourceadapters.NewResourceOpener(st, tag.Id())
if err != nil {
return nil, nil, errors.Trace(err)
}
return opener, closer, nil
},
})

migrateCharmsHandler := &charmsHandler{
ctxt: httpCtxt,
Expand Down
6 changes: 5 additions & 1 deletion apiserver/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ func (s *ResourcesHandlerSuite) TestPutSetResourceFailure(c *gc.C) {
}

func (s *ResourcesHandlerSuite) checkResp(c *gc.C, status int, ctype, body string) {
resp := s.recorder.Result()
checkHTTPResp(c, s.recorder, status, ctype, body)
}

func checkHTTPResp(c *gc.C, recorder *httptest.ResponseRecorder, status int, ctype, body string) {
resp := recorder.Result()
c.Assert(resp.StatusCode, gc.Equals, status)
c.Check(resp.Header.Get("Content-Type"), gc.Equals, ctype)
c.Check(resp.Header.Get("Content-Length"), gc.Equals, strconv.Itoa(len(body)))
Expand Down
60 changes: 60 additions & 0 deletions apiserver/resources_unit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2017 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package apiserver

import (
"fmt"
"io"
"net/http"

"github.com/juju/errors"
"gopkg.in/juju/names.v2"

"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/resource"
"github.com/juju/juju/resource/api"
)

// ResourcesHandler is the HTTP handler for unit agent downloads of
// resources.
type UnitResourcesHandler struct {
NewOpener func(*http.Request, ...string) (resource.Opener, func(), error)
}

// ServeHTTP implements http.Handler.
func (h *UnitResourcesHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
switch req.Method {
case "GET":
opener, closer, err := h.NewOpener(req, names.UnitTagKind)
if err != nil {
api.SendHTTPError(resp, err)
return
}
defer closer()

name := req.URL.Query().Get(":resource")
opened, err := opener.OpenResource(name)
if err != nil {
logger.Errorf("cannot fetch resource reader: %v", err)
api.SendHTTPError(resp, err)
return
}
defer opened.Close()

hdr := resp.Header()
hdr.Set("Content-Type", params.ContentTypeRaw)
hdr.Set("Content-Length", fmt.Sprint(opened.Size))
hdr.Set("Content-Sha384", opened.Fingerprint.String())

resp.WriteHeader(http.StatusOK)
if _, err := io.Copy(resp, opened); err != nil {
// We cannot use SendHTTPError here, so we log the error
// and move on.
logger.Errorf("unable to complete stream for resource: %v", err)
return
}
default:
api.SendHTTPError(resp, errors.MethodNotAllowedf("unsupported method: %q", req.Method))
}
}
148 changes: 148 additions & 0 deletions apiserver/resources_unit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2017 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package apiserver_test

import (
"net/http"
"net/http/httptest"
"net/url"

"github.com/juju/names"
"github.com/juju/testing"
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"

"github.com/juju/juju/apiserver"
"github.com/juju/juju/resource"
"github.com/juju/juju/resource/resourcetesting"
)

type UnitResourcesHandlerSuite struct {
testing.IsolationSuite

stub *testing.Stub
urlStr string
recorder *httptest.ResponseRecorder
}

var _ = gc.Suite(&UnitResourcesHandlerSuite{})

func (s *UnitResourcesHandlerSuite) SetUpTest(c *gc.C) {
s.IsolationSuite.SetUpTest(c)

s.stub = new(testing.Stub)

args := url.Values{}
args.Add(":unit", "foo/0")
args.Add(":resource", "blob")
s.urlStr = "https://api:17017/?" + args.Encode()

s.recorder = httptest.NewRecorder()
}

func (s *UnitResourcesHandlerSuite) closer() {
s.stub.AddCall("Close")
}

func (s *UnitResourcesHandlerSuite) TestWrongMethod(c *gc.C) {
handler := &apiserver.UnitResourcesHandler{}

req, err := http.NewRequest("POST", s.urlStr, nil)
c.Assert(err, jc.ErrorIsNil)

handler.ServeHTTP(s.recorder, req)

resp := s.recorder.Result()
c.Assert(resp.StatusCode, gc.Equals, http.StatusMethodNotAllowed)
s.stub.CheckNoCalls(c)
}

func (s *UnitResourcesHandlerSuite) TestOpenerCreationError(c *gc.C) {
failure, expectedBody := apiFailure("boom", "")
handler := &apiserver.UnitResourcesHandler{
NewOpener: func(_ *http.Request, kinds ...string) (resource.Opener, func(), error) {
return nil, nil, failure
},
}

req, err := http.NewRequest("GET", s.urlStr, nil)
c.Assert(err, jc.ErrorIsNil)

handler.ServeHTTP(s.recorder, req)

s.checkResp(c,
http.StatusInternalServerError,
"application/json",
expectedBody,
)
}

func (s *UnitResourcesHandlerSuite) TestOpenResourceError(c *gc.C) {
opener := &stubResourceOpener{
Stub: s.stub,
}
failure, expectedBody := apiFailure("boom", "")
s.stub.SetErrors(failure)
handler := &apiserver.UnitResourcesHandler{
NewOpener: func(_ *http.Request, kinds ...string) (resource.Opener, func(), error) {
s.stub.AddCall("NewOpener", kinds)
return opener, s.closer, nil
},
}

req, err := http.NewRequest("GET", s.urlStr, nil)
c.Assert(err, jc.ErrorIsNil)

handler.ServeHTTP(s.recorder, req)

s.checkResp(c, http.StatusInternalServerError, "application/json", expectedBody)
s.stub.CheckCalls(c, []testing.StubCall{
{"NewOpener", []interface{}{[]string{names.UnitTagKind}}},
{"OpenResource", []interface{}{"blob"}},
{"Close", nil},
})
}

func (s *UnitResourcesHandlerSuite) TestSuccess(c *gc.C) {
const body = "some data"
opened := resourcetesting.NewResource(c, new(testing.Stub), "blob", "app", body)
opener := &stubResourceOpener{
Stub: s.stub,
ReturnOpenResource: opened,
}
handler := &apiserver.UnitResourcesHandler{
NewOpener: func(_ *http.Request, kinds ...string) (resource.Opener, func(), error) {
s.stub.AddCall("NewOpener", kinds)
return opener, s.closer, nil
},
}

req, err := http.NewRequest("GET", s.urlStr, nil)
c.Assert(err, jc.ErrorIsNil)

handler.ServeHTTP(s.recorder, req)

s.checkResp(c, http.StatusOK, "application/octet-stream", body)
s.stub.CheckCalls(c, []testing.StubCall{
{"NewOpener", []interface{}{[]string{names.UnitTagKind}}},
{"OpenResource", []interface{}{"blob"}},
{"Close", nil},
})
}
func (s *UnitResourcesHandlerSuite) checkResp(c *gc.C, status int, ctype, body string) {
checkHTTPResp(c, s.recorder, status, ctype, body)
}

type stubResourceOpener struct {
*testing.Stub
ReturnOpenResource resource.Opened
}

func (s *stubResourceOpener) OpenResource(name string) (resource.Opened, error) {
s.AddCall("OpenResource", name)
if err := s.NextErr(); err != nil {
return resource.Opened{}, err
}
return s.ReturnOpenResource, nil
}
16 changes: 0 additions & 16 deletions component/all/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@ import (

"github.com/juju/juju/api/base"
"github.com/juju/juju/apiserver/charmrevisionupdater"
"github.com/juju/juju/apiserver/common"
"github.com/juju/juju/apiserver/common/apihttp"
"github.com/juju/juju/cmd/juju/charmcmd"
"github.com/juju/juju/cmd/juju/commands"
"github.com/juju/juju/cmd/modelcmd"
"github.com/juju/juju/resource"
"github.com/juju/juju/resource/api/client"
privateapi "github.com/juju/juju/resource/api/private"
internalclient "github.com/juju/juju/resource/api/private/client"
"github.com/juju/juju/resource/cmd"
"github.com/juju/juju/resource/context"
Expand Down Expand Up @@ -141,7 +138,6 @@ func (r resources) registerHookContext() {
)

r.registerHookContextCommands()
r.registerUnitDownloadEndpoint()
}

func (r resources) registerHookContextCommands() {
Expand All @@ -165,18 +161,6 @@ func (r resources) registerHookContextCommands() {
)
}

// XXX
func (r resources) registerUnitDownloadEndpoint() {
common.RegisterAPIModelEndpoint(privateapi.HTTPEndpointPattern, apihttp.HandlerSpec{
Constraints: apihttp.HandlerConstraints{
AuthKinds: []string{names.UnitTagKind},
StrictValidation: true,
ControllerModelOnly: false,
},
NewHandler: resourceadapters.NewDownloadHandler,
})
}

func (r resources) newUnitFacadeClient(unitName string, caller base.APICaller) (context.APIClient, error) {

facadeCaller := base.NewFacadeCallerForVersion(caller, context.HookContextFacade, 1)
Expand Down
38 changes: 1 addition & 37 deletions resource/api/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@

package api

import (
"fmt"
"net/http"

"github.com/juju/errors"
charmresource "gopkg.in/juju/charm.v6-unstable/resource"

"github.com/juju/juju/resource"
)
import "net/http"

// NewHTTPDownloadRequest creates a new HTTP download request
// for the given resource.
Expand All @@ -20,31 +12,3 @@ import (
func NewHTTPDownloadRequest(resourceName string) (*http.Request, error) {
return http.NewRequest("GET", "/resources/"+resourceName, nil)
}

// ExtractDownloadRequest pulls the download request info out of the
// given HTTP request.
//
// Intended for use on the server side.
func ExtractDownloadRequest(req *http.Request) string {
return req.URL.Query().Get(":resource")
}

// UpdateDownloadResponse sets the appropriate headers in the response
// to an HTTP download request.
//
// Intended for use on the server side.
func UpdateDownloadResponse(resp http.ResponseWriter, resource resource.Resource) {
resp.Header().Set("Content-Type", ContentTypeRaw)
resp.Header().Set("Content-Length", fmt.Sprint(resource.Size))
resp.Header().Set("Content-Sha384", resource.Fingerprint.String())
}

// ExtractDownloadResponse pulls the download size and checksum
// from the HTTP response.
func ExtractDownloadResponse(resp *http.Response) (int64, charmresource.Fingerprint, error) {
var fp charmresource.Fingerprint

// TODO(ericsnow) Finish!
// See UpdateDownloadResponse for the data to extract.
return 0, fp, errors.New("not finished")
}
Loading

0 comments on commit 61010bf

Please sign in to comment.