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  }