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, ¶ms.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 }