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