Plugins: Add /meta and /metas APIs to plugins app (#113775)
* add /meta and /metas APIs * wrapped storage route * format file * fix switch statement lint issue * fix plugininstaller test --------- Co-authored-by: Todd Treece <todd.treece@grafana.com>
This commit is contained in:
co-authored by
Todd Treece
parent
335111e783
commit
f1dbbcbe00
@@ -0,0 +1,131 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCloudTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
// CloudProvider retrieves plugin metadata from the grafana.com API.
|
||||
type CloudProvider struct {
|
||||
httpClient *http.Client
|
||||
grafanaComAPIURL string
|
||||
log logging.Logger
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewCloudProvider creates a new CloudProvider that fetches metadata from grafana.com.
|
||||
func NewCloudProvider(grafanaComAPIURL string) *CloudProvider {
|
||||
return NewCloudProviderWithTTL(grafanaComAPIURL, defaultCloudTTL)
|
||||
}
|
||||
|
||||
// NewCloudProviderWithTTL creates a new CloudProvider with a custom TTL.
|
||||
func NewCloudProviderWithTTL(grafanaComAPIURL string, ttl time.Duration) *CloudProvider {
|
||||
if grafanaComAPIURL == "" {
|
||||
grafanaComAPIURL = "https://grafana.com/api/plugins"
|
||||
}
|
||||
|
||||
return &CloudProvider{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
grafanaComAPIURL: grafanaComAPIURL,
|
||||
log: logging.DefaultLogger,
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMeta fetches plugin metadata from grafana.com API endpoint:
|
||||
// GET /api/plugins/{pluginId}/versions/{version}
|
||||
func (p *CloudProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
u, err := url.Parse(p.grafanaComAPIURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid grafana.com API URL: %w", err)
|
||||
}
|
||||
|
||||
u.Path = path.Join(u.Path, pluginID, "versions", version)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "grafana-plugins-app")
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch plugin metadata: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
p.log.Warn("Failed to close response body", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, ErrMetaNotFound
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code %d from grafana.com API", resp.StatusCode)
|
||||
}
|
||||
|
||||
var gcomMeta grafanaComPluginVersionMeta
|
||||
if err = json.NewDecoder(resp.Body).Decode(&gcomMeta); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Meta: gcomMeta.JSON,
|
||||
TTL: p.ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// grafanaComPluginVersionMeta represents the response from grafana.com API
|
||||
// GET /api/plugins/{pluginId}/versions/{version}
|
||||
type grafanaComPluginVersionMeta struct {
|
||||
PluginID string `json:"pluginSlug"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
Commit string `json:"commit"`
|
||||
Description string `json:"description"`
|
||||
Keywords []string `json:"keywords"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
JSON pluginsv0alpha1.PluginMetaJSONData `json:"json"`
|
||||
Readme string `json:"readme"`
|
||||
Downloads int `json:"downloads"`
|
||||
Verified bool `json:"verified"`
|
||||
Status string `json:"status"`
|
||||
StatusContext string `json:"statusContext"`
|
||||
DownloadSlug string `json:"downloadSlug"`
|
||||
SignatureType string `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
Packages struct {
|
||||
Any struct {
|
||||
Md5 string `json:"md5"`
|
||||
Sha256 string `json:"sha256"`
|
||||
PackageName string `json:"packageName"`
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
} `json:"any"`
|
||||
} `json:"packages"`
|
||||
Links []struct {
|
||||
Rel string `json:"rel"`
|
||||
Href string `json:"href"`
|
||||
} `json:"links"`
|
||||
AngularDetected bool `json:"angularDetected"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
Reference in New Issue
Block a user