github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/objects.go (about)

     1  // Copyright 2023 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"strings"
    12  
    13  	"github.com/juju/charm/v12"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/utils/v3"
    16  
    17  	"github.com/juju/juju/rpc/params"
    18  	"github.com/juju/juju/state"
    19  )
    20  
    21  type objectsCharmHTTPHandler struct {
    22  	GetHandler          FailableHandlerFunc
    23  	PutHandler          FailableHandlerFunc
    24  	LegacyCharmsHandler http.Handler
    25  }
    26  
    27  func (h *objectsCharmHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    28  	var err error
    29  	switch r.Method {
    30  	case "GET":
    31  		err = errors.Annotate(h.GetHandler(w, r), "cannot retrieve charm")
    32  		if err == nil {
    33  			// Chain call to legacy (REST API) charms handler
    34  			h.LegacyCharmsHandler.ServeHTTP(w, r)
    35  		}
    36  	case "PUT":
    37  		err = errors.Annotate(h.PutHandler(w, r), "cannot upload charm")
    38  	default:
    39  		http.Error(w, fmt.Sprintf("http method %s not implemented", r.Method), http.StatusNotImplemented)
    40  		return
    41  	}
    42  
    43  	if err != nil {
    44  		if err := sendJSONError(w, r, errors.Trace(err)); err != nil {
    45  			logger.Errorf("%v", errors.Annotate(err, "cannot return error to user"))
    46  		}
    47  	}
    48  
    49  }
    50  
    51  // objectsCharmHandler handles charm upload through S3-compatible HTTPS in the
    52  // API server.
    53  type objectsCharmHandler struct {
    54  	ctxt          httpContext
    55  	stateAuthFunc func(*http.Request) (*state.PooledState, error)
    56  }
    57  
    58  func (h *objectsCharmHandler) ServeUnsupported(w http.ResponseWriter, r *http.Request) error {
    59  	return errors.Trace(emitUnsupportedMethodErr(r.Method))
    60  }
    61  
    62  // ServeGet serves the GET method for the S3 API. This is the equivalent of the
    63  // `GetObject` method in the AWS S3 API.
    64  // Since juju's objects (S3) API only acts as a shim, this method will only
    65  // rewrite the http request for it to be correctly processed by the legacy
    66  // '/charms' handler.
    67  func (h *objectsCharmHandler) ServeGet(w http.ResponseWriter, r *http.Request) error {
    68  	st, _, err := h.ctxt.stateForRequestAuthenticated(r)
    69  	if err != nil {
    70  		return errors.Trace(err)
    71  	}
    72  	defer st.Release()
    73  
    74  	query := r.URL.Query()
    75  
    76  	_, charmSha256, err := splitNameAndSHAFromQuery(query)
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	// Retrieve charm from state.
    82  	ch, err := st.CharmFromSha256(charmSha256)
    83  	if err != nil {
    84  		return errors.Annotate(err, "cannot get charm from state")
    85  	}
    86  
    87  	query.Add("url", ch.URL())
    88  	query.Add("file", "*")
    89  	r.URL.RawQuery = query.Encode()
    90  
    91  	return nil
    92  }
    93  
    94  // ServePut serves the PUT method for the S3 API. This is the equivalent of the
    95  // `PutObject` method in the AWS S3 API.
    96  // Since juju's objects (S3) API only acts as a shim, this method will only
    97  // rewrite the http request for it to be correctly processed by the legacy
    98  // '/charms' handler.
    99  func (h *objectsCharmHandler) ServePut(w http.ResponseWriter, r *http.Request) error {
   100  	// Make sure the content type is zip.
   101  	contentType := r.Header.Get("Content-Type")
   102  	if contentType != "application/zip" {
   103  		return errors.BadRequestf("expected Content-Type: application/zip, got: %v", contentType)
   104  	}
   105  
   106  	st, err := h.stateAuthFunc(r)
   107  	if err != nil {
   108  		return errors.Trace(err)
   109  	}
   110  	defer st.Release()
   111  
   112  	// Add a charm to the store provider.
   113  	charmURL, err := h.processPut(r, st.State)
   114  	if err != nil {
   115  		return errors.NewBadRequest(err, "")
   116  	}
   117  	return errors.Trace(sendStatusAndHeadersAndJSON(w, http.StatusOK, map[string]string{"Juju-Curl": charmURL.String()}, &params.CharmsResponse{CharmURL: charmURL.String()}))
   118  }
   119  
   120  func (h *objectsCharmHandler) processPut(r *http.Request, st *state.State) (*charm.URL, error) {
   121  	query := r.URL.Query()
   122  	name, shaFromQuery, err := splitNameAndSHAFromQuery(query)
   123  	if err != nil {
   124  		return nil, errors.Trace(err)
   125  	}
   126  
   127  	curlStr := r.Header.Get("Juju-Curl")
   128  	curl, err := charm.ParseURL(curlStr)
   129  	if err != nil {
   130  		return nil, errors.BadRequestf("%q is not a valid charm url", curlStr)
   131  	}
   132  	curl.Name = name
   133  
   134  	schema := curl.Schema
   135  	if schema != "local" {
   136  		// charmhub charms may only be uploaded into models
   137  		// which are being imported during model migrations.
   138  		// There's currently no other time where it makes sense
   139  		// to accept repository charms through this endpoint.
   140  		if isImporting, err := modelIsImporting(st); err != nil {
   141  			return nil, errors.Trace(err)
   142  		} else if !isImporting {
   143  			return nil, errors.New("non-local charms may only be uploaded during model migration import")
   144  		}
   145  	}
   146  
   147  	charmFileName, err := writeCharmToTempFile(r.Body)
   148  	if err != nil {
   149  		return nil, errors.Trace(err)
   150  	}
   151  	defer os.Remove(charmFileName)
   152  
   153  	charmSHA, _, err := utils.ReadFileSHA256(charmFileName)
   154  	if err != nil {
   155  		return nil, errors.Trace(err)
   156  	}
   157  
   158  	// ReadFileSHA256 returns a full 64 char SHA256. However, charm refs
   159  	// only use the first 7 chars. So truncate the sha to match
   160  	charmSHA = charmSHA[0:7]
   161  	if charmSHA != shaFromQuery {
   162  		return nil, errors.BadRequestf("Uploaded charm sha256 (%v) does not match sha in url (%v)", charmSHA, shaFromQuery)
   163  	}
   164  
   165  	archive, err := charm.ReadCharmArchive(charmFileName)
   166  	if err != nil {
   167  		return nil, errors.BadRequestf("invalid charm archive: %v", err)
   168  	}
   169  
   170  	if curl.Revision == -1 {
   171  		curl.Revision = archive.Revision()
   172  	}
   173  
   174  	switch charm.Schema(schema) {
   175  	case charm.Local:
   176  		curl, err = st.PrepareLocalCharmUpload(curl.String())
   177  		if err != nil {
   178  			return nil, errors.Trace(err)
   179  		}
   180  
   181  	case charm.CharmHub:
   182  		if _, err := st.PrepareCharmUpload(curl.String()); err != nil {
   183  			return nil, errors.Trace(err)
   184  		}
   185  
   186  	default:
   187  		return nil, errors.Errorf("unsupported schema %q", schema)
   188  	}
   189  
   190  	return curl, errors.Trace(RepackageAndUploadCharm(st, archive, curl.String(), curl.Revision))
   191  }
   192  
   193  func splitNameAndSHAFromQuery(query url.Values) (string, string, error) {
   194  	charmObjectID := query.Get(":object")
   195  
   196  	// Path param is {charmName}-{charmSha256[0:7]} so we need to split it.
   197  	// NOTE: charmName can contain "-", so we cannot simply strings.Split
   198  	splitIndex := strings.LastIndex(charmObjectID, "-")
   199  	if splitIndex == -1 {
   200  		return "", "", errors.BadRequestf("%q is not a valid charm object path", charmObjectID)
   201  	}
   202  	name, sha := charmObjectID[:splitIndex], charmObjectID[splitIndex+1:]
   203  	return name, sha, nil
   204  }