Plugins: Refactor plugin repository API (#69063)

* support grafana wildcard version

* undo go.mod changes

* tidy

* flesh out tests

* refactor

* add tests

* tidy naming

* undo some changes

* split interfaces

* separation

* update new signature

* simplify

* update var namings

* unexport types

* introduce opts pattern

* reorder test

* fix compat checks

* middle ground

* unexport client

* move back

* fix tests

* inline logger

* make client usable

* use fake logger

* tidy errors

* remove unused types

* fix test

* review fixes

* rework compatibility

* adjust installer

* fix tests

* opts => cfg

* remove unused var

* fix var name
This commit is contained in:
Will Browne
2023-05-30 11:48:52 +02:00
committed by GitHub
parent e7e70dbac6
commit 12dc56ad0c
18 changed files with 724 additions and 358 deletions

View File

@ -26,7 +26,7 @@ type Client struct {
log log.PrettyLogger
}
func newClient(skipTLSVerify bool, logger log.PrettyLogger) *Client {
func NewClient(skipTLSVerify bool, logger log.PrettyLogger) *Client {
return &Client{
httpClient: makeHttpClient(skipTLSVerify, 10*time.Second),
httpClientNoTimeout: makeHttpClient(skipTLSVerify, 0),
@ -34,7 +34,7 @@ func newClient(skipTLSVerify bool, logger log.PrettyLogger) *Client {
}
}
func (c *Client) download(_ context.Context, pluginZipURL, checksum string, compatOpts CompatOpts) (*PluginArchive, error) {
func (c *Client) Download(_ context.Context, pluginZipURL, checksum string, compatOpts CompatOpts) (*PluginArchive, error) {
// Create temp file for downloading zip file
tmpFile, err := os.CreateTemp("", "*.zip")
if err != nil {
@ -53,7 +53,7 @@ func (c *Client) download(_ context.Context, pluginZipURL, checksum string, comp
if err := tmpFile.Close(); err != nil {
c.log.Warn("Failed to close file", "err", err)
}
return nil, fmt.Errorf("%w: failed to download plugin archive (%s)", err, pluginZipURL)
return nil, fmt.Errorf("failed to download plugin archive: %w", err)
}
rc, err := zip.OpenReader(tmpFile.Name())
@ -61,9 +61,29 @@ func (c *Client) download(_ context.Context, pluginZipURL, checksum string, comp
return nil, err
}
return &PluginArchive{
File: rc,
}, nil
return &PluginArchive{File: rc}, nil
}
func (c *Client) SendReq(url *url.URL, compatOpts CompatOpts) ([]byte, error) {
req, err := c.createReq(url, compatOpts)
if err != nil {
return nil, err
}
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
bodyReader, err := c.handleResp(res, compatOpts)
if err != nil {
return nil, err
}
defer func() {
if err = bodyReader.Close(); err != nil {
c.log.Warn("Failed to close stream", "err", err)
}
}()
return io.ReadAll(bodyReader)
}
func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, compatOpts CompatOpts) (err error) {
@ -122,8 +142,8 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp
return err
}
// Using no timeout here as some plugins can be bigger and smaller timeout would prevent to download a plugin on
// slow network. As this is CLI operation hanging is not a big of an issue as user can just abort.
// Using no timeout as some plugin archives make take longer to fetch due to size, network performance, etc.
// Note: This is also used as part of the grafana plugin install CLI operation
bodyReader, err := c.sendReqNoTimeout(u, compatOpts)
if err != nil {
return err
@ -139,37 +159,15 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp
if _, err = io.Copy(w, io.TeeReader(bodyReader, h)); err != nil {
return fmt.Errorf("%v: %w", "failed to compute SHA256 checksum", err)
}
if err := w.Flush(); err != nil {
if err = w.Flush(); err != nil {
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
}
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
return fmt.Errorf("expected SHA256 checksum does not match the downloaded archive (%s) - please contact security@grafana.com", pluginURL)
return ErrChecksumMismatch{archiveURL: pluginURL}
}
return nil
}
func (c *Client) sendReq(url *url.URL, compatOpts CompatOpts) ([]byte, error) {
req, err := c.createReq(url, compatOpts)
if err != nil {
return nil, err
}
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
bodyReader, err := c.handleResp(res, compatOpts)
if err != nil {
return nil, err
}
defer func() {
if err := bodyReader.Close(); err != nil {
c.log.Warn("Failed to close stream", "err", err)
}
}()
return io.ReadAll(bodyReader)
}
func (c *Client) sendReqNoTimeout(url *url.URL, compatOpts CompatOpts) (io.ReadCloser, error) {
req, err := c.createReq(url, compatOpts)
if err != nil {
@ -189,10 +187,18 @@ func (c *Client) createReq(url *url.URL, compatOpts CompatOpts) (*http.Request,
return nil, err
}
req.Header.Set("grafana-version", compatOpts.GrafanaVersion)
req.Header.Set("grafana-os", compatOpts.OS)
req.Header.Set("grafana-arch", compatOpts.Arch)
req.Header.Set("User-Agent", "grafana "+compatOpts.GrafanaVersion)
if gVer, exists := compatOpts.GrafanaVersion(); exists {
req.Header.Set("grafana-version", gVer)
req.Header.Set("User-Agent", "grafana "+gVer)
}
if sysOS, exists := compatOpts.system.OS(); exists {
req.Header.Set("grafana-os", sysOS)
}
if sysArch, exists := compatOpts.system.Arch(); exists {
req.Header.Set("grafana-arch", sysArch)
}
return req, err
}
@ -206,7 +212,7 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC
}
}()
if err != nil || len(body) == 0 {
return nil, Response4xxError{StatusCode: res.StatusCode}
return nil, newErrResponse4xx(res.StatusCode)
}
var message string
var jsonBody map[string]string
@ -216,7 +222,8 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC
} else {
message = jsonBody["message"]
}
return nil, Response4xxError{StatusCode: res.StatusCode, Message: message, SystemInfo: compatOpts.String()}
return nil, newErrResponse4xx(res.StatusCode).withMessage(message).withCompatibilityInfo(compatOpts)
}
if res.StatusCode/100 != 2 {
@ -227,23 +234,21 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC
}
func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client {
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: skipTLSVerify,
return http.Client{
Timeout: timeout,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: skipTLSVerify,
},
},
}
return http.Client{
Timeout: timeout,
Transport: tr,
}
}