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

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver
     5  
     6  import (
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"strconv"
    11  
    12  	charmresource "github.com/juju/charm/v12/resource"
    13  	"github.com/juju/errors"
    14  
    15  	"github.com/juju/juju/core/resources"
    16  	"github.com/juju/juju/rpc/params"
    17  	"github.com/juju/juju/state"
    18  )
    19  
    20  // resourcesMigrationUploadHandler handles resources uploads for model migrations.
    21  type resourcesMigrationUploadHandler struct {
    22  	ctxt          httpContext
    23  	stateAuthFunc func(*http.Request) (*state.PooledState, error)
    24  }
    25  
    26  func (h *resourcesMigrationUploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    27  	// Validate before authenticate because the authentication is dependent
    28  	// on the state connection that is determined during the validation.
    29  	st, err := h.stateAuthFunc(r)
    30  	if err != nil {
    31  		if err := sendError(w, err); err != nil {
    32  			logger.Errorf("%v", err)
    33  		}
    34  		return
    35  	}
    36  	defer st.Release()
    37  
    38  	switch r.Method {
    39  	case "POST":
    40  		res, err := h.processPost(r, st.State)
    41  		if err != nil {
    42  			if err := sendError(w, err); err != nil {
    43  				logger.Errorf("%v", err)
    44  			}
    45  			return
    46  		}
    47  		if err := sendStatusAndJSON(w, http.StatusOK, &params.ResourceUploadResult{
    48  			ID:        res.ID,
    49  			Timestamp: res.Timestamp,
    50  		}); err != nil {
    51  			logger.Errorf("%v", err)
    52  		}
    53  	default:
    54  		if err := sendError(w, errors.MethodNotAllowedf("unsupported method: %q", r.Method)); err != nil {
    55  			logger.Errorf("%v", err)
    56  		}
    57  	}
    58  }
    59  
    60  // processPost handles resources upload POST request after
    61  // authentication.
    62  func (h *resourcesMigrationUploadHandler) processPost(r *http.Request, st *state.State) (resources.Resource, error) {
    63  	var empty resources.Resource
    64  	query := r.URL.Query()
    65  
    66  	target, isUnit, err := getUploadTarget(query)
    67  	if err != nil {
    68  		return empty, errors.Trace(err)
    69  	}
    70  
    71  	userID := query.Get("user") // Is allowed to be blank
    72  	res, err := queryToResource(query)
    73  	if err != nil {
    74  		return empty, errors.Trace(err)
    75  	}
    76  	rSt := st.Resources()
    77  
    78  	reader := r.Body
    79  
    80  	// Don't associate content with a placeholder resource.
    81  	if isPlaceholder(query) {
    82  		reader = nil
    83  	}
    84  
    85  	outRes, err := setResource(isUnit, target, userID, res, reader, rSt)
    86  	if err != nil {
    87  		return empty, errors.Annotate(err, "resource upload failed")
    88  	}
    89  	return outRes, nil
    90  }
    91  
    92  func setResource(isUnit bool, target, user string, res charmresource.Resource, r io.Reader, rSt state.Resources) (
    93  	resources.Resource, error,
    94  ) {
    95  	if isUnit {
    96  		return rSt.SetUnitResource(target, user, res)
    97  	}
    98  	return rSt.SetResource(target, user, res, r, state.DoNotIncrementCharmModifiedVersion)
    99  }
   100  
   101  func isPlaceholder(query url.Values) bool {
   102  	return query.Get("timestamp") == ""
   103  }
   104  
   105  func getUploadTarget(query url.Values) (string, bool, error) {
   106  	appName := query.Get("application")
   107  	unitName := query.Get("unit")
   108  	switch {
   109  	case appName == "" && unitName == "":
   110  		return "", false, errors.BadRequestf("missing application/unit")
   111  	case appName != "" && unitName != "":
   112  		return "", false, errors.BadRequestf("application and unit can't be set at the same time")
   113  	case appName != "":
   114  		return appName, false, nil
   115  	default:
   116  		return unitName, true, nil
   117  	}
   118  }
   119  
   120  func queryToResource(query url.Values) (charmresource.Resource, error) {
   121  	var err error
   122  	empty := charmresource.Resource{}
   123  
   124  	res := charmresource.Resource{
   125  		Meta: charmresource.Meta{
   126  			Name:        query.Get("name"),
   127  			Path:        query.Get("path"),
   128  			Description: query.Get("description"),
   129  		},
   130  	}
   131  	if res.Name == "" {
   132  		return empty, errors.BadRequestf("missing name")
   133  	}
   134  	res.Type, err = charmresource.ParseType(query.Get("type"))
   135  	if err != nil {
   136  		return empty, errors.BadRequestf("invalid type")
   137  	}
   138  	res.Origin, err = charmresource.ParseOrigin(query.Get("origin"))
   139  	if err != nil {
   140  		return empty, errors.BadRequestf("invalid origin")
   141  	}
   142  	res.Revision, err = strconv.Atoi(query.Get("revision"))
   143  	if err != nil {
   144  		return empty, errors.BadRequestf("invalid revision")
   145  	}
   146  	res.Size, err = strconv.ParseInt(query.Get("size"), 10, 64)
   147  	if err != nil {
   148  		return empty, errors.BadRequestf("invalid size")
   149  	}
   150  	switch res.Type {
   151  	case charmresource.TypeFile:
   152  		if res.Path == "" {
   153  			return empty, errors.BadRequestf("missing path")
   154  		}
   155  		res.Fingerprint, err = charmresource.ParseFingerprint(query.Get("fingerprint"))
   156  		if err != nil {
   157  			return empty, errors.BadRequestf("invalid fingerprint")
   158  		}
   159  	case charmresource.TypeContainerImage:
   160  		res.Fingerprint, err = charmresource.ParseFingerprint(query.Get("fingerprint"))
   161  		if err != nil {
   162  			// Old resources do not have fingerprints.
   163  			res.Fingerprint = charmresource.Fingerprint{}
   164  		}
   165  	}
   166  	return res, nil
   167  }