github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/charmhub/client.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // CharmHub is a client for communication with charmHub. Unlike 5 // the charmHub client within juju, this package does not rely on 6 // wrapping an external package client. Generic client code for this 7 // package has been copied from "github.com/juju/charmrepo/v7/csclient". 8 // 9 // TODO: (hml) 2020-06-17 10 // Implement: 11 // - use of macaroons, at that time consider refactoring the local 12 // charmHub pkg to share macaroonJar. 13 // - user/password ? 14 // - allow for use of the channel pieces 15 16 package charmhub 17 18 import ( 19 "context" 20 "fmt" 21 "io" 22 "net/url" 23 "path" 24 "strings" 25 "time" 26 27 "github.com/juju/charm/v12" 28 "github.com/juju/errors" 29 "github.com/juju/loggo" 30 31 charmhubpath "github.com/juju/juju/charmhub/path" 32 "github.com/juju/juju/charmhub/transport" 33 charmmetrics "github.com/juju/juju/core/charm/metrics" 34 corelogger "github.com/juju/juju/core/logger" 35 ) 36 37 const ( 38 // DefaultServerURL is the default location of the global Charmhub API. 39 // An alternate location can be configured by changing the URL 40 // field in the Config struct. 41 DefaultServerURL = "https://api.charmhub.io" 42 43 // RefreshTimeout is the timout callers should use for Refresh calls. 44 RefreshTimeout = 10 * time.Second 45 ) 46 47 const ( 48 serverVersion = "v2" 49 serverEntity = "charms" 50 ) 51 52 // Logger is the interface to use for logging requests and errors. 53 type Logger interface { 54 IsTraceEnabled() bool 55 56 Errorf(string, ...interface{}) 57 Tracef(string, ...interface{}) 58 59 ChildWithLabels(string, ...string) loggo.Logger 60 } 61 62 // Config holds configuration for creating a new charm hub client. 63 // The zero value is a valid default configuration. 64 type Config struct { 65 // Logger to use during the API requests. This field is required. 66 Logger Logger 67 68 // URL holds the base endpoint URL of the Charmhub API, 69 // with no trailing slash, not including the version. 70 // If empty string, use the default Charmhub API server. 71 URL string 72 73 // HTTPClient represents the HTTP client to use for all API 74 // requests. If nil, use the default HTTP client. 75 HTTPClient HTTPClient 76 77 // FileSystem represents the file system operations for downloading. 78 // If nil, use the real OS file system. 79 FileSystem FileSystem 80 } 81 82 // basePath returns the base configuration path for speaking to the server API. 83 func basePath(configURL string) (charmhubpath.Path, error) { 84 baseURL := strings.TrimRight(configURL, "/") 85 rawURL := fmt.Sprintf("%s/%s", baseURL, path.Join(serverVersion, serverEntity)) 86 url, err := url.Parse(rawURL) 87 if err != nil { 88 return charmhubpath.Path{}, errors.Trace(err) 89 } 90 return charmhubpath.MakePath(url), nil 91 } 92 93 // Client represents the client side of a charm store. 94 type Client struct { 95 url string 96 infoClient *infoClient 97 findClient *findClient 98 downloadClient *downloadClient 99 refreshClient *refreshClient 100 resourcesClient *resourcesClient 101 logger Logger 102 } 103 104 // NewClient creates a new Charmhub client from the supplied configuration. 105 func NewClient(config Config) (*Client, error) { 106 logger := config.Logger 107 if logger == nil { 108 return nil, errors.NotValidf("nil logger") 109 } 110 logger = logger.ChildWithLabels("client", corelogger.CHARMHUB) 111 112 url := config.URL 113 if url == "" { 114 url = DefaultServerURL 115 } 116 117 httpClient := config.HTTPClient 118 if httpClient == nil { 119 httpClient = DefaultHTTPClient(logger) 120 } 121 122 fs := config.FileSystem 123 if fs == nil { 124 fs = fileSystem{} 125 } 126 127 base, err := basePath(url) 128 if err != nil { 129 return nil, errors.Trace(err) 130 } 131 132 infoPath, err := base.Join("info") 133 if err != nil { 134 return nil, errors.Annotate(err, "constructing info path") 135 } 136 137 findPath, err := base.Join("find") 138 if err != nil { 139 return nil, errors.Annotate(err, "constructing find path") 140 } 141 142 refreshPath, err := base.Join("refresh") 143 if err != nil { 144 return nil, errors.Annotate(err, "constructing refresh path") 145 } 146 147 resourcesPath, err := base.Join("resources") 148 if err != nil { 149 return nil, errors.Annotate(err, "constructing resources path") 150 } 151 152 logger.Tracef("NewClient to %q", url) 153 154 apiRequester := newAPIRequester(httpClient, logger) 155 apiRequestLogger := newAPIRequesterLogger(apiRequester, logger) 156 restClient := newHTTPRESTClient(apiRequestLogger) 157 158 return &Client{ 159 url: base.String(), 160 infoClient: newInfoClient(infoPath, restClient, logger), 161 findClient: newFindClient(findPath, restClient, logger), 162 refreshClient: newRefreshClient(refreshPath, restClient, logger), 163 // download client doesn't require a path here, as the download could 164 // be from any server in theory. That information is found from the 165 // refresh response. 166 downloadClient: newDownloadClient(httpClient, fs, logger), 167 resourcesClient: newResourcesClient(resourcesPath, restClient, logger), 168 logger: logger, 169 }, nil 170 } 171 172 // URL returns the underlying store URL. 173 func (c *Client) URL() string { 174 return c.url 175 } 176 177 // Info returns charm info on the provided charm name from CharmHub API. 178 func (c *Client) Info(ctx context.Context, name string, options ...InfoOption) (transport.InfoResponse, error) { 179 return c.infoClient.Info(ctx, name, options...) 180 } 181 182 // Find searches for a given charm for a given name from CharmHub API. 183 func (c *Client) Find(ctx context.Context, name string, options ...FindOption) ([]transport.FindResponse, error) { 184 return c.findClient.Find(ctx, name, options...) 185 } 186 187 // Refresh defines a client for making refresh API calls with different actions. 188 func (c *Client) Refresh(ctx context.Context, config RefreshConfig) ([]transport.RefreshResponse, error) { 189 return c.refreshClient.Refresh(ctx, config) 190 } 191 192 // RefreshWithRequestMetrics defines a client for making refresh API calls. 193 // Specifically to use the refresh action and provide metrics. Intended for 194 // use in the charm revision updater facade only. Otherwise use Refresh. 195 func (c *Client) RefreshWithRequestMetrics(ctx context.Context, config RefreshConfig, metrics map[charmmetrics.MetricKey]map[charmmetrics.MetricKey]string) ([]transport.RefreshResponse, error) { 196 return c.refreshClient.RefreshWithRequestMetrics(ctx, config, metrics) 197 } 198 199 // RefreshWithMetricsOnly defines a client making a refresh API call with no 200 // action, whose purpose is to send metrics data for models without current 201 // units. E.G. the controller model. 202 func (c *Client) RefreshWithMetricsOnly(ctx context.Context, metrics map[charmmetrics.MetricKey]map[charmmetrics.MetricKey]string) error { 203 return c.refreshClient.RefreshWithMetricsOnly(ctx, metrics) 204 } 205 206 // Download defines a client for downloading charms directly. 207 func (c *Client) Download(ctx context.Context, resourceURL *url.URL, archivePath string, options ...DownloadOption) error { 208 return c.downloadClient.Download(ctx, resourceURL, archivePath, options...) 209 } 210 211 // DownloadAndRead defines a client for downloading charms directly. 212 func (c *Client) DownloadAndRead(ctx context.Context, resourceURL *url.URL, archivePath string, options ...DownloadOption) (*charm.CharmArchive, error) { 213 return c.downloadClient.DownloadAndRead(ctx, resourceURL, archivePath, options...) 214 } 215 216 // DownloadAndReadBundle defines a client for downloading bundles directly. 217 func (c *Client) DownloadAndReadBundle(ctx context.Context, resourceURL *url.URL, archivePath string, options ...DownloadOption) (charm.Bundle, error) { 218 return c.downloadClient.DownloadAndReadBundle(ctx, resourceURL, archivePath, options...) 219 } 220 221 // DownloadResource returns an io.ReadCloser to read the Resource from. 222 func (c *Client) DownloadResource(ctx context.Context, resourceURL *url.URL) (r io.ReadCloser, err error) { 223 return c.downloadClient.DownloadResource(ctx, resourceURL) 224 } 225 226 // ListResourceRevisions returns resource revisions for the provided charm and resource. 227 func (c *Client) ListResourceRevisions(ctx context.Context, charm, resource string) ([]transport.ResourceRevision, error) { 228 return c.resourcesClient.ListResourceRevisions(ctx, charm, resource) 229 }