github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/api/client/charms/localcharmputters.go (about) 1 // Copyright 2024 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package charms 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "os" 13 "strconv" 14 15 "github.com/juju/charm/v12" 16 "github.com/juju/errors" 17 "gopkg.in/httprequest.v1" 18 19 "github.com/juju/juju/api/base" 20 "github.com/juju/juju/rpc/params" 21 ) 22 23 // CharmPutter uploads a local charm blob to the controller 24 type CharmPutter interface { 25 PutCharm(ctx context.Context, modelUUID, charmRef, curl string, body io.Reader) (string, error) 26 } 27 28 // httpPutter uploads local charm blobs to the controller via the legacy 29 // "model/:modeluuid/charms" endpoint hosted by the controller 30 type httpPutter struct { 31 httpClient *httprequest.Client 32 } 33 34 // newHTTPPutter create an httpPutter, which uploads local charm blobs 35 // to the controller via the legacy "model/:modeluuid/charms" endpoint 36 // hosted by the controller 37 func newHTTPPutter(api base.APICaller) (CharmPutter, error) { 38 // The returned httpClient sets the base url to /model/:modeluuid if it can. 39 apiHTTPClient, err := api.HTTPClient() 40 if err != nil { 41 return nil, errors.Annotate(err, "cannot retrieve http client from the api connection") 42 } 43 return &httpPutter{ 44 httpClient: apiHTTPClient, 45 }, nil 46 } 47 48 func (h *httpPutter) PutCharm(ctx context.Context, _, _, curlStr string, body io.Reader) (string, error) { 49 curl, err := charm.ParseURL(curlStr) 50 if err != nil { 51 return "", errors.Trace(err) 52 } 53 args := url.Values{} 54 args.Add("series", curl.Series) 55 args.Add("schema", curl.Schema) 56 args.Add("revision", strconv.Itoa(curl.Revision)) 57 apiURI := url.URL{Path: "/charms", RawQuery: args.Encode()} 58 59 var resp params.CharmsResponse 60 req, err := http.NewRequest("POST", apiURI.String(), body) 61 if err != nil { 62 return "", errors.Annotate(err, "cannot create upload request") 63 } 64 req.Header.Set("Content-Type", "application/zip") 65 66 if err := h.httpClient.Do(ctx, req, &resp); err != nil { 67 return "", errors.Trace(err) 68 } 69 return resp.CharmURL, nil 70 } 71 72 // s3Putter uploads local charm blobs to the controller via the 73 // s3-compatible api endpoint "model-:modeluuid/charms/:object" 74 75 // We use a regular httpClient to curl the s3-compatible endpoint 76 type s3Putter struct { 77 httpClient *httprequest.Client 78 } 79 80 // newS3Putter creates an s3Putter, which uploads local charm blobs 81 // to the controller via the s3-compatible api endpoint 82 // "model-:modeluuid/charms/:object" 83 func newS3Putter(api base.APICaller) (CharmPutter, error) { 84 apiHTTPClient, err := api.RootHTTPClient() 85 if err != nil { 86 return nil, errors.Annotate(err, "cannot retrieve http client from the api connection") 87 } 88 return &s3Putter{ 89 httpClient: apiHTTPClient, 90 }, nil 91 } 92 93 func (h *s3Putter) PutCharm(ctx context.Context, modelUUID, charmRef, curl string, body io.Reader) (string, error) { 94 apiURI := url.URL{Path: fmt.Sprintf("/model-%s/charms/%s", modelUUID, charmRef)} 95 96 resp := &http.Response{} 97 req, err := http.NewRequest("PUT", apiURI.String(), body) 98 if err != nil { 99 return "", errors.Trace(err) 100 } 101 req.Header.Set("Content-Type", "application/zip") 102 req.Header.Set("Juju-Curl", curl) 103 104 if err := h.httpClient.Do(ctx, req, &resp); err != nil { 105 return "", errors.Trace(err) 106 } 107 108 return resp.Header.Get("Juju-Curl"), nil 109 } 110 111 // fallbackPutter iterates over a number of sub-putters, attempting to upload a charm 112 // blob until one is successful. If a putter fails, before falling back to the next one 113 // we check is the error is 'fallback-able'. It doesn't make sense to fallback on certain 114 // error types such as unauthorised 115 type fallbackPutter struct { 116 putters []CharmPutter 117 } 118 119 // newFallbackPutter creates a fallbackPutter with at least 2 sub-putters 120 func newFallbackPutter(putters ...CharmPutter) (CharmPutter, error) { 121 if len(putters) == 0 { 122 return nil, errors.Errorf("programming error: fallbackPutter requires at least 1 sub putter") 123 } 124 return &fallbackPutter{ 125 putters: putters, 126 }, nil 127 } 128 129 func (h *fallbackPutter) PutCharm(ctx context.Context, modelUUID, charmRef, curl string, b io.Reader) (string, error) { 130 // body must be a ReadSeeker to use PutCharm on fallbackPutter so we can rewind the body 131 // between requests 132 body, ok := b.(io.ReadSeeker) 133 if !ok { 134 return "", errors.Errorf("Programming error: body must be a seeker to use FallbackPutter") 135 } 136 137 // Wrap our body in a nopCloser to ensure sub-putters do not close our body before passing to the next 138 // putter. As a consequence, we need to defer a close ourselves, if closeable. 139 nopCloserBody := io.NopCloser(body) 140 if closer, ok := body.(io.Closer); ok { 141 defer closer.Close() 142 } 143 144 var err error 145 for _, putter := range h.putters { 146 respCurl, err := putter.PutCharm(ctx, modelUUID, charmRef, curl, nopCloserBody) 147 if err == nil { 148 return respCurl, nil 149 } 150 if !h.fallbackableError(err) { 151 return "", errors.Trace(err) 152 } 153 _, seekErr := body.Seek(0, os.SEEK_SET) 154 if seekErr != nil { 155 return "", errors.Trace(seekErr) 156 } 157 } 158 return "", errors.Annotate(err, "All charm putters failed") 159 } 160 161 func (h *fallbackPutter) fallbackableError(err error) bool { 162 fallbackableTypes := []errors.ConstError{errors.NotFound, errors.MethodNotAllowed} 163 for _, typ := range fallbackableTypes { 164 if errors.Is(errors.Cause(err), typ) { 165 return true 166 } 167 } 168 return false 169 }