Skip to content

Commit

Permalink
Merge pull request juju#16696 from SimonRichardson/objects-get-request
Browse files Browse the repository at this point in the history
juju#16696

Adds the ability to get an object of any type just by the path of the object. Currently, this is the storage path to the metadata, but conceivably it is the UUID of the object.

This piggybacks off the current charm objects implementation. This is a raw endpoint and doesn't require checking of the underlying entity state. It's either in the object store or it isn't.

This is required by the file object store to move into HA. The concept is simple; if you're in HA and you don't have the file locally, it's reasonable to ask another controller for that file. Upon receiving the file, it will store it locally, and send it back in the request.

---

Interestingly, I now better understand how to fix the API server and remove a lot of complexity. The API Server should offer an endpoint register interface from the API server worker. Each handler can then register and unregister at will. It will provide resiliency for each handler. Failure at a handler doesn't bring down the whole API server. In addition, we can then move a lot of this mess out of the apiserver package into individual worker handlers.

I'll look into creating a prototype for this in the new year.

<!-- Why this change is needed and what it does. -->

## Checklist

<!-- If an item is not applicable, use `~strikethrough~`. -->

- [x] Code style: imports ordered, good names, simple structure, etc
- [x] Comments saying why design decisions were made
- [x] Go unit tests, with comments saying what you're testing

## QA steps

```sh
$ go test -v ./apiserver -check.v -check.f=objects
```


## Links


**Jira card:** JUJU-5139
  • Loading branch information
jujubot committed Dec 14, 2023
2 parents 07de6c1 + 2d619a2 commit 24c3c2e
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 64 deletions.
26 changes: 20 additions & 6 deletions apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,10 +648,13 @@ func (srv *Server) loop(ready chan struct{}) error {
return tomb.ErrDying
}

func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
const modelRoutePrefix = "/model/:modeluuid"
const charmsObjectsRoutePrefix = "/:bucket/charms/:object"
const (
modelRoutePrefix = "/model/:modeluuid"
charmsObjectsRoutePrefix = "/:bucket/charms/:object"
objectsRoutePrefix = "/:bucket/objects/:object"
)

func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
type handler struct {
pattern string
methods []string
Expand All @@ -661,6 +664,7 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
tracked bool
noModelUUID bool
}

var endpoints []apihttp.Endpoint
systemState, err := srv.shared.statePool.SystemState()
if err != nil {
Expand Down Expand Up @@ -697,6 +701,11 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
Handler: h,
Query: ":bucket",
}
} else if strings.HasPrefix(handler.pattern, objectsRoutePrefix) {
h = &httpcontext.BucketModelHandler{
Handler: h,
Query: ":bucket",
}
} else {
h = &httpcontext.ImpliedModelHandler{
Handler: h,
Expand Down Expand Up @@ -768,12 +777,13 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
}
modelCharmsUploadAuthorizer := tagKindAuthorizer{names.UserTagKind}

modelObjectsCharmsHandler := &objectsCharmHandler{
modelObjectsCharmsHTTPHandler := &objectsCharmHTTPHandler{
ctxt: httpCtxt,
objectStoreGetter: srv.shared.objectStoreGetter,
}
modelObjectsCharmsHTTPHandler := &objectsCharmHTTPHandler{
GetHandler: modelObjectsCharmsHandler.ServeGet,
modelObjectsHTTPHandler := &objectsHTTPHandler{
ctxt: httpCtxt,
objectStoreGetter: srv.shared.objectStoreGetter,
}

modelToolsUploadHandler := &toolsUploadHandler{
Expand Down Expand Up @@ -993,6 +1003,10 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
pattern: charmsObjectsRoutePrefix,
methods: []string{"GET"},
handler: modelObjectsCharmsHTTPHandler,
}, {
pattern: objectsRoutePrefix,
methods: []string{"GET"},
handler: modelObjectsHTTPHandler,
}}
if srv.registerIntrospectionHandlers != nil {
add := func(subpath string, h http.Handler) {
Expand Down
85 changes: 74 additions & 11 deletions apiserver/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package apiserver

import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
Expand All @@ -22,14 +23,15 @@ type ObjectStoreGetter interface {
}

type objectsCharmHTTPHandler struct {
GetHandler FailableHandlerFunc
ctxt httpContext
objectStoreGetter ObjectStoreGetter
}

func (h *objectsCharmHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var err error
switch r.Method {
case "GET":
err = errors.Annotate(h.GetHandler(w, r), "cannot retrieve charm")
err = errors.Annotate(h.ServeGet(w, r), "cannot retrieve charm")
default:
http.Error(w, fmt.Sprintf("http method %s not implemented", r.Method), http.StatusNotImplemented)
return
Expand All @@ -40,19 +42,11 @@ func (h *objectsCharmHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
logger.Errorf("%v", errors.Annotate(err, "cannot return error to user"))
}
}

}

// objectsCharmHandler handles charm upload through S3-compatible HTTPS in the
// API server.
type objectsCharmHandler struct {
ctxt httpContext
objectStoreGetter ObjectStoreGetter
}

// ServeGet serves the GET method for the S3 API. This is the equivalent of the
// `GetObject` method in the AWS S3 API.
func (h *objectsCharmHandler) ServeGet(w http.ResponseWriter, r *http.Request) error {
func (h *objectsCharmHTTPHandler) ServeGet(w http.ResponseWriter, r *http.Request) error {
st, _, err := h.ctxt.stateForRequestAuthenticated(r)
if err != nil {
return errors.Trace(err)
Expand Down Expand Up @@ -102,3 +96,72 @@ func (h *objectsCharmHandler) ServeGet(w http.ResponseWriter, r *http.Request) e

return nil
}

type objectsHTTPHandler struct {
ctxt httpContext
objectStoreGetter ObjectStoreGetter
}

func (h *objectsHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var err error
switch r.Method {
case "GET":
err = errors.Annotate(h.ServeGet(w, r), "cannot retrieve object")
default:
http.Error(w, fmt.Sprintf("http method %s not implemented", r.Method), http.StatusNotImplemented)
return
}

if err != nil {
if err := sendJSONError(w, r, errors.Trace(err)); err != nil {
logger.Errorf("%v", errors.Annotate(err, "cannot return error to user"))
}
}
}

// ServeGet serves the GET method for the S3 API. This is the equivalent of the
// `GetObject` method in the AWS S3 API.
func (h *objectsHTTPHandler) ServeGet(w http.ResponseWriter, r *http.Request) error {
st, _, err := h.ctxt.stateForRequestAuthenticated(r)
if err != nil {
return errors.Trace(err)
}
defer st.Release()

query := r.URL.Query()
objectID := query.Get(":object")
if objectID == "" {
return errors.NewBadRequest(nil, "missing object id")
}

// The object ID is base64 encoded, so we need to decode it.
rawObjectID, err := base64.URLEncoding.DecodeString(objectID)
if err != nil {
return errors.NewBadRequest(nil, "cannot decode object id")
}

// Get the underlying object store for the model UUID, which we can then
// retrieve the blob from.
store, err := h.objectStoreGetter.GetObjectStore(r.Context(), st.ModelUUID())
if err != nil {
return errors.Annotate(err, "cannot get object store")
}

// Use the storage to retrieve the charm archive.
reader, size, err := store.Get(r.Context(), string(rawObjectID))
if err != nil {
return errors.Annotate(err, "cannot get object at path %s")
}
defer reader.Close()

written, err := io.Copy(w, reader)
if err != nil {
return errors.Annotate(err, "error processing charm archive download")
}

if written != size {
return errors.Errorf("expected to write %d bytes, but wrote %d", size, written)
}

return nil
}
Loading

0 comments on commit 24c3c2e

Please sign in to comment.