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:
@@ -2,9 +2,6 @@ package v0alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -100,24 +97,3 @@ func (c *PluginClient) UpdateStatus(ctx context.Context, identifier resource.Ide
|
||||
func (c *PluginClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
|
||||
return c.client.Delete(ctx, identifier, opts)
|
||||
}
|
||||
|
||||
type GetMetaRequest struct {
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
func (c *PluginClient) GetMeta(ctx context.Context, identifier resource.Identifier, request GetMetaRequest) (*GetMeta, error) {
|
||||
resp, err := c.client.SubresourceRequest(ctx, identifier, resource.CustomRouteRequestOptions{
|
||||
Path: "/meta",
|
||||
Verb: "GET",
|
||||
Headers: request.Headers,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cast := GetMeta{}
|
||||
err = json.Unmarshal(resp, &cast)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal response bytes into GetMeta: %w", err)
|
||||
}
|
||||
return &cast, nil
|
||||
}
|
||||
|
||||
-463
@@ -1,463 +0,0 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
time "time"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type Info struct {
|
||||
// Required fields
|
||||
// +listType=set
|
||||
Keywords []string `json:"keywords"`
|
||||
Logos V0alpha1InfoLogos `json:"logos"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Version string `json:"version"`
|
||||
// Optional fields
|
||||
Author *V0alpha1InfoAuthor `json:"author,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
// +listType=atomic
|
||||
Links []V0alpha1InfoLinks `json:"links,omitempty"`
|
||||
// +listType=atomic
|
||||
Screenshots []V0alpha1InfoScreenshots `json:"screenshots,omitempty"`
|
||||
}
|
||||
|
||||
// NewInfo creates a new Info object.
|
||||
func NewInfo() *Info {
|
||||
return &Info{
|
||||
Keywords: []string{},
|
||||
Logos: *NewV0alpha1InfoLogos(),
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type Dependencies struct {
|
||||
// Required field
|
||||
GrafanaDependency string `json:"grafanaDependency"`
|
||||
// Optional fields
|
||||
GrafanaVersion *string `json:"grafanaVersion,omitempty"`
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
Plugins []V0alpha1DependenciesPlugins `json:"plugins,omitempty"`
|
||||
Extensions *V0alpha1DependenciesExtensions `json:"extensions,omitempty"`
|
||||
}
|
||||
|
||||
// NewDependencies creates a new Dependencies object.
|
||||
func NewDependencies() *Dependencies {
|
||||
return &Dependencies{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type EnterpriseFeatures struct {
|
||||
// Allow additional properties
|
||||
HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"`
|
||||
}
|
||||
|
||||
// NewEnterpriseFeatures creates a new EnterpriseFeatures object.
|
||||
func NewEnterpriseFeatures() *EnterpriseFeatures {
|
||||
return &EnterpriseFeatures{
|
||||
HealthDiagnosticsErrors: (func(input bool) *bool { return &input })(false),
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type Include struct {
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
Type *IncludeType `json:"type,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Component *string `json:"component,omitempty"`
|
||||
Role *IncludeRole `json:"role,omitempty"`
|
||||
Action *string `json:"action,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
AddToNav *bool `json:"addToNav,omitempty"`
|
||||
DefaultNav *bool `json:"defaultNav,omitempty"`
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
// NewInclude creates a new Include object.
|
||||
func NewInclude() *Include {
|
||||
return &Include{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type QueryOptions struct {
|
||||
MaxDataPoints *bool `json:"maxDataPoints,omitempty"`
|
||||
MinInterval *bool `json:"minInterval,omitempty"`
|
||||
CacheTimeout *bool `json:"cacheTimeout,omitempty"`
|
||||
}
|
||||
|
||||
// NewQueryOptions creates a new QueryOptions object.
|
||||
func NewQueryOptions() *QueryOptions {
|
||||
return &QueryOptions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type Route struct {
|
||||
Path *string `json:"path,omitempty"`
|
||||
Method *string `json:"method,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
ReqSignedIn *bool `json:"reqSignedIn,omitempty"`
|
||||
ReqRole *string `json:"reqRole,omitempty"`
|
||||
ReqAction *string `json:"reqAction,omitempty"`
|
||||
// +listType=atomic
|
||||
Headers []string `json:"headers,omitempty"`
|
||||
Body map[string]interface{} `json:"body,omitempty"`
|
||||
TokenAuth *V0alpha1RouteTokenAuth `json:"tokenAuth,omitempty"`
|
||||
JwtTokenAuth *V0alpha1RouteJwtTokenAuth `json:"jwtTokenAuth,omitempty"`
|
||||
// +listType=atomic
|
||||
UrlParams []V0alpha1RouteUrlParams `json:"urlParams,omitempty"`
|
||||
}
|
||||
|
||||
// NewRoute creates a new Route object.
|
||||
func NewRoute() *Route {
|
||||
return &Route{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type IAM struct {
|
||||
// +listType=atomic
|
||||
Permissions []V0alpha1IAMPermissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
// NewIAM creates a new IAM object.
|
||||
func NewIAM() *IAM {
|
||||
return &IAM{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type Role struct {
|
||||
Role *V0alpha1RoleRole `json:"role,omitempty"`
|
||||
// +listType=set
|
||||
Grants []string `json:"grants,omitempty"`
|
||||
}
|
||||
|
||||
// NewRole creates a new Role object.
|
||||
func NewRole() *Role {
|
||||
return &Role{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type Extensions struct {
|
||||
// +listType=atomic
|
||||
AddedComponents []V0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"`
|
||||
// +listType=atomic
|
||||
AddedLinks []V0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"`
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
ExposedComponents []V0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"`
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
ExtensionPoints []V0alpha1ExtensionsExtensionPoints `json:"extensionPoints,omitempty"`
|
||||
}
|
||||
|
||||
// NewExtensions creates a new Extensions object.
|
||||
func NewExtensions() *Extensions {
|
||||
return &Extensions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetMeta struct {
|
||||
// Unique name of the plugin
|
||||
Id string `json:"id"`
|
||||
// Plugin type
|
||||
Type GetMetaType `json:"type"`
|
||||
// Human-readable name of the plugin
|
||||
Name string `json:"name"`
|
||||
// Metadata for the plugin
|
||||
Info Info `json:"info"`
|
||||
// Dependency information
|
||||
Dependencies Dependencies `json:"dependencies"`
|
||||
// Optional fields
|
||||
Alerting *bool `json:"alerting,omitempty"`
|
||||
Annotations *bool `json:"annotations,omitempty"`
|
||||
AutoEnabled *bool `json:"autoEnabled,omitempty"`
|
||||
Backend *bool `json:"backend,omitempty"`
|
||||
BuildMode *string `json:"buildMode,omitempty"`
|
||||
BuiltIn *bool `json:"builtIn,omitempty"`
|
||||
Category *GetMetaCategory `json:"category,omitempty"`
|
||||
EnterpriseFeatures *EnterpriseFeatures `json:"enterpriseFeatures,omitempty"`
|
||||
Executable *string `json:"executable,omitempty"`
|
||||
HideFromList *bool `json:"hideFromList,omitempty"`
|
||||
// +listType=atomic
|
||||
Includes []Include `json:"includes,omitempty"`
|
||||
Logs *bool `json:"logs,omitempty"`
|
||||
Metrics *bool `json:"metrics,omitempty"`
|
||||
MultiValueFilterOperators *bool `json:"multiValueFilterOperators,omitempty"`
|
||||
PascalName *string `json:"pascalName,omitempty"`
|
||||
Preload *bool `json:"preload,omitempty"`
|
||||
QueryOptions *QueryOptions `json:"queryOptions,omitempty"`
|
||||
// +listType=atomic
|
||||
Routes []Route `json:"routes,omitempty"`
|
||||
SkipDataQuery *bool `json:"skipDataQuery,omitempty"`
|
||||
State *GetMetaState `json:"state,omitempty"`
|
||||
Streaming *bool `json:"streaming,omitempty"`
|
||||
Tracing *bool `json:"tracing,omitempty"`
|
||||
Iam *IAM `json:"iam,omitempty"`
|
||||
// +listType=atomic
|
||||
Roles []Role `json:"roles,omitempty"`
|
||||
Extensions *Extensions `json:"extensions,omitempty"`
|
||||
}
|
||||
|
||||
// NewGetMeta creates a new GetMeta object.
|
||||
func NewGetMeta() *GetMeta {
|
||||
return &GetMeta{
|
||||
Info: *NewInfo(),
|
||||
Dependencies: *NewDependencies(),
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1InfoLogos struct {
|
||||
Small string `json:"small"`
|
||||
Large string `json:"large"`
|
||||
}
|
||||
|
||||
// NewV0alpha1InfoLogos creates a new V0alpha1InfoLogos object.
|
||||
func NewV0alpha1InfoLogos() *V0alpha1InfoLogos {
|
||||
return &V0alpha1InfoLogos{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1InfoAuthor struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1InfoAuthor creates a new V0alpha1InfoAuthor object.
|
||||
func NewV0alpha1InfoAuthor() *V0alpha1InfoAuthor {
|
||||
return &V0alpha1InfoAuthor{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1InfoLinks struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1InfoLinks creates a new V0alpha1InfoLinks object.
|
||||
func NewV0alpha1InfoLinks() *V0alpha1InfoLinks {
|
||||
return &V0alpha1InfoLinks{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1InfoScreenshots struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1InfoScreenshots creates a new V0alpha1InfoScreenshots object.
|
||||
func NewV0alpha1InfoScreenshots() *V0alpha1InfoScreenshots {
|
||||
return &V0alpha1InfoScreenshots{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1DependenciesPlugins struct {
|
||||
Id string `json:"id"`
|
||||
Type V0alpha1DependenciesPluginsType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// NewV0alpha1DependenciesPlugins creates a new V0alpha1DependenciesPlugins object.
|
||||
func NewV0alpha1DependenciesPlugins() *V0alpha1DependenciesPlugins {
|
||||
return &V0alpha1DependenciesPlugins{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1DependenciesExtensions struct {
|
||||
// +listType=set
|
||||
ExposedComponents []string `json:"exposedComponents,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1DependenciesExtensions creates a new V0alpha1DependenciesExtensions object.
|
||||
func NewV0alpha1DependenciesExtensions() *V0alpha1DependenciesExtensions {
|
||||
return &V0alpha1DependenciesExtensions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1RouteTokenAuth struct {
|
||||
Url *string `json:"url,omitempty"`
|
||||
// +listType=set
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1RouteTokenAuth creates a new V0alpha1RouteTokenAuth object.
|
||||
func NewV0alpha1RouteTokenAuth() *V0alpha1RouteTokenAuth {
|
||||
return &V0alpha1RouteTokenAuth{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1RouteJwtTokenAuth struct {
|
||||
Url *string `json:"url,omitempty"`
|
||||
// +listType=set
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1RouteJwtTokenAuth creates a new V0alpha1RouteJwtTokenAuth object.
|
||||
func NewV0alpha1RouteJwtTokenAuth() *V0alpha1RouteJwtTokenAuth {
|
||||
return &V0alpha1RouteJwtTokenAuth{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1RouteUrlParams struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Content *string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1RouteUrlParams creates a new V0alpha1RouteUrlParams object.
|
||||
func NewV0alpha1RouteUrlParams() *V0alpha1RouteUrlParams {
|
||||
return &V0alpha1RouteUrlParams{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1IAMPermissions struct {
|
||||
Action *string `json:"action,omitempty"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1IAMPermissions creates a new V0alpha1IAMPermissions object.
|
||||
func NewV0alpha1IAMPermissions() *V0alpha1IAMPermissions {
|
||||
return &V0alpha1IAMPermissions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1RoleRolePermissions struct {
|
||||
Action *string `json:"action,omitempty"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1RoleRolePermissions creates a new V0alpha1RoleRolePermissions object.
|
||||
func NewV0alpha1RoleRolePermissions() *V0alpha1RoleRolePermissions {
|
||||
return &V0alpha1RoleRolePermissions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1RoleRole struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
// +listType=atomic
|
||||
Permissions []V0alpha1RoleRolePermissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1RoleRole creates a new V0alpha1RoleRole object.
|
||||
func NewV0alpha1RoleRole() *V0alpha1RoleRole {
|
||||
return &V0alpha1RoleRole{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1ExtensionsAddedComponents struct {
|
||||
// +listType=set
|
||||
Targets []string `json:"targets"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1ExtensionsAddedComponents creates a new V0alpha1ExtensionsAddedComponents object.
|
||||
func NewV0alpha1ExtensionsAddedComponents() *V0alpha1ExtensionsAddedComponents {
|
||||
return &V0alpha1ExtensionsAddedComponents{
|
||||
Targets: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1ExtensionsAddedLinks struct {
|
||||
// +listType=set
|
||||
Targets []string `json:"targets"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1ExtensionsAddedLinks creates a new V0alpha1ExtensionsAddedLinks object.
|
||||
func NewV0alpha1ExtensionsAddedLinks() *V0alpha1ExtensionsAddedLinks {
|
||||
return &V0alpha1ExtensionsAddedLinks{
|
||||
Targets: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1ExtensionsExposedComponents struct {
|
||||
Id string `json:"id"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1ExtensionsExposedComponents creates a new V0alpha1ExtensionsExposedComponents object.
|
||||
func NewV0alpha1ExtensionsExposedComponents() *V0alpha1ExtensionsExposedComponents {
|
||||
return &V0alpha1ExtensionsExposedComponents{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1ExtensionsExtensionPoints struct {
|
||||
Id string `json:"id"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// NewV0alpha1ExtensionsExtensionPoints creates a new V0alpha1ExtensionsExtensionPoints object.
|
||||
func NewV0alpha1ExtensionsExtensionPoints() *V0alpha1ExtensionsExtensionPoints {
|
||||
return &V0alpha1ExtensionsExtensionPoints{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type IncludeType string
|
||||
|
||||
const (
|
||||
IncludeTypeDashboard IncludeType = "dashboard"
|
||||
IncludeTypePage IncludeType = "page"
|
||||
IncludeTypePanel IncludeType = "panel"
|
||||
IncludeTypeDatasource IncludeType = "datasource"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type IncludeRole string
|
||||
|
||||
const (
|
||||
IncludeRoleAdmin IncludeRole = "Admin"
|
||||
IncludeRoleEditor IncludeRole = "Editor"
|
||||
IncludeRoleViewer IncludeRole = "Viewer"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetMetaType string
|
||||
|
||||
const (
|
||||
GetMetaTypeApp GetMetaType = "app"
|
||||
GetMetaTypeDatasource GetMetaType = "datasource"
|
||||
GetMetaTypePanel GetMetaType = "panel"
|
||||
GetMetaTypeRenderer GetMetaType = "renderer"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetMetaCategory string
|
||||
|
||||
const (
|
||||
GetMetaCategoryTsdb GetMetaCategory = "tsdb"
|
||||
GetMetaCategoryLogging GetMetaCategory = "logging"
|
||||
GetMetaCategoryCloud GetMetaCategory = "cloud"
|
||||
GetMetaCategoryTracing GetMetaCategory = "tracing"
|
||||
GetMetaCategoryProfiling GetMetaCategory = "profiling"
|
||||
GetMetaCategorySql GetMetaCategory = "sql"
|
||||
GetMetaCategoryEnterprise GetMetaCategory = "enterprise"
|
||||
GetMetaCategoryIot GetMetaCategory = "iot"
|
||||
GetMetaCategoryOther GetMetaCategory = "other"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetMetaState string
|
||||
|
||||
const (
|
||||
GetMetaStateAlpha GetMetaState = "alpha"
|
||||
GetMetaStateBeta GetMetaState = "beta"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1DependenciesPluginsType string
|
||||
|
||||
const (
|
||||
V0alpha1DependenciesPluginsTypeApp V0alpha1DependenciesPluginsType = "app"
|
||||
V0alpha1DependenciesPluginsTypeDatasource V0alpha1DependenciesPluginsType = "datasource"
|
||||
V0alpha1DependenciesPluginsTypePanel V0alpha1DependenciesPluginsType = "panel"
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type PluginMetaClient struct {
|
||||
client *resource.TypedClient[*PluginMeta, *PluginMetaList]
|
||||
}
|
||||
|
||||
func NewPluginMetaClient(client resource.Client) *PluginMetaClient {
|
||||
return &PluginMetaClient{
|
||||
client: resource.NewTypedClient[*PluginMeta, *PluginMetaList](client, PluginMetaKind()),
|
||||
}
|
||||
}
|
||||
|
||||
func NewPluginMetaClientFromGenerator(generator resource.ClientGenerator) (*PluginMetaClient, error) {
|
||||
c, err := generator.ClientFor(PluginMetaKind())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewPluginMetaClient(c), nil
|
||||
}
|
||||
|
||||
func (c *PluginMetaClient) Get(ctx context.Context, identifier resource.Identifier) (*PluginMeta, error) {
|
||||
return c.client.Get(ctx, identifier)
|
||||
}
|
||||
|
||||
func (c *PluginMetaClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) {
|
||||
return c.client.List(ctx, namespace, opts)
|
||||
}
|
||||
|
||||
func (c *PluginMetaClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) {
|
||||
resp, err := c.client.List(ctx, namespace, resource.ListOptions{
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
Limit: opts.Limit,
|
||||
LabelFilters: opts.LabelFilters,
|
||||
FieldSelectors: opts.FieldSelectors,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for resp.GetContinue() != "" {
|
||||
page, err := c.client.List(ctx, namespace, resource.ListOptions{
|
||||
Continue: resp.GetContinue(),
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
Limit: opts.Limit,
|
||||
LabelFilters: opts.LabelFilters,
|
||||
FieldSelectors: opts.FieldSelectors,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.SetContinue(page.GetContinue())
|
||||
resp.SetResourceVersion(page.GetResourceVersion())
|
||||
resp.SetItems(append(resp.GetItems(), page.GetItems()...))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *PluginMetaClient) Create(ctx context.Context, obj *PluginMeta, opts resource.CreateOptions) (*PluginMeta, error) {
|
||||
// Make sure apiVersion and kind are set
|
||||
obj.APIVersion = GroupVersion.Identifier()
|
||||
obj.Kind = PluginMetaKind().Kind()
|
||||
return c.client.Create(ctx, obj, opts)
|
||||
}
|
||||
|
||||
func (c *PluginMetaClient) Update(ctx context.Context, obj *PluginMeta, opts resource.UpdateOptions) (*PluginMeta, error) {
|
||||
return c.client.Update(ctx, obj, opts)
|
||||
}
|
||||
|
||||
func (c *PluginMetaClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*PluginMeta, error) {
|
||||
return c.client.Patch(ctx, identifier, req, opts)
|
||||
}
|
||||
|
||||
func (c *PluginMetaClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus PluginMetaStatus, opts resource.UpdateOptions) (*PluginMeta, error) {
|
||||
return c.client.Update(ctx, &PluginMeta{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: PluginMetaKind().Kind(),
|
||||
APIVersion: GroupVersion.Identifier(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
Namespace: identifier.Namespace,
|
||||
Name: identifier.Name,
|
||||
},
|
||||
Status: newStatus,
|
||||
}, resource.UpdateOptions{
|
||||
Subresource: "status",
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *PluginMetaClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
|
||||
return c.client.Delete(ctx, identifier, opts)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// PluginMetaJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
|
||||
type PluginMetaJSONCodec struct{}
|
||||
|
||||
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
|
||||
func (*PluginMetaJSONCodec) Read(reader io.Reader, into resource.Object) error {
|
||||
return json.NewDecoder(reader).Decode(into)
|
||||
}
|
||||
|
||||
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
|
||||
func (*PluginMetaJSONCodec) Write(writer io.Writer, from resource.Object) error {
|
||||
return json.NewEncoder(writer).Encode(from)
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Codec = &PluginMetaJSONCodec{}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
time "time"
|
||||
)
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
type PluginMetaMetadata struct {
|
||||
UpdateTimestamp time.Time `json:"updateTimestamp"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Uid string `json:"uid"`
|
||||
CreationTimestamp time.Time `json:"creationTimestamp"`
|
||||
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
|
||||
Finalizers []string `json:"finalizers"`
|
||||
ResourceVersion string `json:"resourceVersion"`
|
||||
Generation int64 `json:"generation"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// NewPluginMetaMetadata creates a new PluginMetaMetadata object.
|
||||
func NewPluginMetaMetadata() *PluginMetaMetadata {
|
||||
return &PluginMetaMetadata{
|
||||
Finalizers: []string{},
|
||||
Labels: map[string]string{},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMeta struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
|
||||
|
||||
// Spec is the spec of the PluginMeta
|
||||
Spec PluginMetaSpec `json:"spec" yaml:"spec"`
|
||||
|
||||
Status PluginMetaStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
func (o *PluginMeta) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
func (o *PluginMeta) SetSpec(spec any) error {
|
||||
cast, ok := spec.(PluginMetaSpec)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
|
||||
}
|
||||
o.Spec = cast
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *PluginMeta) GetSubresources() map[string]any {
|
||||
return map[string]any{
|
||||
"status": o.Status,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PluginMeta) GetSubresource(name string) (any, bool) {
|
||||
switch name {
|
||||
case "status":
|
||||
return o.Status, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PluginMeta) SetSubresource(name string, value any) error {
|
||||
switch name {
|
||||
case "status":
|
||||
cast, ok := value.(PluginMetaStatus)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot set status type %#v, not of type PluginMetaStatus", value)
|
||||
}
|
||||
o.Status = cast
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("subresource '%s' does not exist", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PluginMeta) GetStaticMetadata() resource.StaticMetadata {
|
||||
gvk := o.GroupVersionKind()
|
||||
return resource.StaticMetadata{
|
||||
Name: o.ObjectMeta.Name,
|
||||
Namespace: o.ObjectMeta.Namespace,
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: gvk.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PluginMeta) SetStaticMetadata(metadata resource.StaticMetadata) {
|
||||
o.Name = metadata.Name
|
||||
o.Namespace = metadata.Namespace
|
||||
o.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: metadata.Group,
|
||||
Version: metadata.Version,
|
||||
Kind: metadata.Kind,
|
||||
})
|
||||
}
|
||||
|
||||
func (o *PluginMeta) GetCommonMetadata() resource.CommonMetadata {
|
||||
dt := o.DeletionTimestamp
|
||||
var deletionTimestamp *time.Time
|
||||
if dt != nil {
|
||||
deletionTimestamp = &dt.Time
|
||||
}
|
||||
// Legacy ExtraFields support
|
||||
extraFields := make(map[string]any)
|
||||
if o.Annotations != nil {
|
||||
extraFields["annotations"] = o.Annotations
|
||||
}
|
||||
if o.ManagedFields != nil {
|
||||
extraFields["managedFields"] = o.ManagedFields
|
||||
}
|
||||
if o.OwnerReferences != nil {
|
||||
extraFields["ownerReferences"] = o.OwnerReferences
|
||||
}
|
||||
return resource.CommonMetadata{
|
||||
UID: string(o.UID),
|
||||
ResourceVersion: o.ResourceVersion,
|
||||
Generation: o.Generation,
|
||||
Labels: o.Labels,
|
||||
CreationTimestamp: o.CreationTimestamp.Time,
|
||||
DeletionTimestamp: deletionTimestamp,
|
||||
Finalizers: o.Finalizers,
|
||||
UpdateTimestamp: o.GetUpdateTimestamp(),
|
||||
CreatedBy: o.GetCreatedBy(),
|
||||
UpdatedBy: o.GetUpdatedBy(),
|
||||
ExtraFields: extraFields,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PluginMeta) SetCommonMetadata(metadata resource.CommonMetadata) {
|
||||
o.UID = types.UID(metadata.UID)
|
||||
o.ResourceVersion = metadata.ResourceVersion
|
||||
o.Generation = metadata.Generation
|
||||
o.Labels = metadata.Labels
|
||||
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
|
||||
if metadata.DeletionTimestamp != nil {
|
||||
dt := metav1.NewTime(*metadata.DeletionTimestamp)
|
||||
o.DeletionTimestamp = &dt
|
||||
} else {
|
||||
o.DeletionTimestamp = nil
|
||||
}
|
||||
o.Finalizers = metadata.Finalizers
|
||||
if o.Annotations == nil {
|
||||
o.Annotations = make(map[string]string)
|
||||
}
|
||||
if !metadata.UpdateTimestamp.IsZero() {
|
||||
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
|
||||
}
|
||||
if metadata.CreatedBy != "" {
|
||||
o.SetCreatedBy(metadata.CreatedBy)
|
||||
}
|
||||
if metadata.UpdatedBy != "" {
|
||||
o.SetUpdatedBy(metadata.UpdatedBy)
|
||||
}
|
||||
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
|
||||
if metadata.ExtraFields != nil {
|
||||
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
|
||||
if cast, ok := annotations.(map[string]string); ok {
|
||||
o.Annotations = cast
|
||||
}
|
||||
}
|
||||
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
|
||||
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
|
||||
o.ManagedFields = cast
|
||||
}
|
||||
}
|
||||
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
|
||||
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
|
||||
o.OwnerReferences = cast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PluginMeta) GetCreatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
|
||||
}
|
||||
|
||||
func (o *PluginMeta) SetCreatedBy(createdBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
|
||||
}
|
||||
|
||||
func (o *PluginMeta) GetUpdateTimestamp() time.Time {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (o *PluginMeta) SetUpdateTimestamp(updateTimestamp time.Time) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (o *PluginMeta) GetUpdatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
|
||||
}
|
||||
|
||||
func (o *PluginMeta) SetUpdatedBy(updatedBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
|
||||
}
|
||||
|
||||
func (o *PluginMeta) Copy() resource.Object {
|
||||
return resource.CopyObject(o)
|
||||
}
|
||||
|
||||
func (o *PluginMeta) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *PluginMeta) DeepCopy() *PluginMeta {
|
||||
cpy := &PluginMeta{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *PluginMeta) DeepCopyInto(dst *PluginMeta) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
|
||||
o.Spec.DeepCopyInto(&dst.Spec)
|
||||
o.Status.DeepCopyInto(&dst.Status)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.Object = &PluginMeta{}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaList struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ListMeta `json:"metadata" yaml:"metadata"`
|
||||
Items []PluginMeta `json:"items" yaml:"items"`
|
||||
}
|
||||
|
||||
func (o *PluginMetaList) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *PluginMetaList) Copy() resource.ListObject {
|
||||
cpy := &PluginMetaList{
|
||||
TypeMeta: o.TypeMeta,
|
||||
Items: make([]PluginMeta, len(o.Items)),
|
||||
}
|
||||
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
if item, ok := o.Items[i].Copy().(*PluginMeta); ok {
|
||||
cpy.Items[i] = *item
|
||||
}
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *PluginMetaList) GetItems() []resource.Object {
|
||||
items := make([]resource.Object, len(o.Items))
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
items[i] = &o.Items[i]
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (o *PluginMetaList) SetItems(items []resource.Object) {
|
||||
o.Items = make([]PluginMeta, len(items))
|
||||
for i := 0; i < len(items); i++ {
|
||||
o.Items[i] = *items[i].(*PluginMeta)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PluginMetaList) DeepCopy() *PluginMetaList {
|
||||
cpy := &PluginMetaList{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *PluginMetaList) DeepCopyInto(dst *PluginMetaList) {
|
||||
resource.CopyObjectInto(dst, o)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.ListObject = &PluginMetaList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
// DeepCopy creates a full deep copy of Spec
|
||||
func (s *PluginMetaSpec) DeepCopy() *PluginMetaSpec {
|
||||
cpy := &PluginMetaSpec{}
|
||||
s.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// DeepCopyInto deep copies Spec into another Spec object
|
||||
func (s *PluginMetaSpec) DeepCopyInto(dst *PluginMetaSpec) {
|
||||
resource.CopyObjectInto(dst, s)
|
||||
}
|
||||
|
||||
// DeepCopy creates a full deep copy of PluginMetaStatus
|
||||
func (s *PluginMetaStatus) DeepCopy() *PluginMetaStatus {
|
||||
cpy := &PluginMetaStatus{}
|
||||
s.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// DeepCopyInto deep copies PluginMetaStatus into another PluginMetaStatus object
|
||||
func (s *PluginMetaStatus) DeepCopyInto(dst *PluginMetaStatus) {
|
||||
resource.CopyObjectInto(dst, s)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaPluginMeta = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", &PluginMeta{}, &PluginMetaList{}, resource.WithKind("PluginMeta"),
|
||||
resource.WithPlural("pluginmetas"), resource.WithScope(resource.NamespacedScope))
|
||||
kindPluginMeta = resource.Kind{
|
||||
Schema: schemaPluginMeta,
|
||||
Codecs: map[resource.KindEncoding]resource.Codec{
|
||||
resource.KindEncodingJSON: &PluginMetaJSONCodec{},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Kind returns a resource.Kind for this Schema with a JSON codec
|
||||
func PluginMetaKind() resource.Kind {
|
||||
return kindPluginMeta
|
||||
}
|
||||
|
||||
// Schema returns a resource.SimpleSchema representation of PluginMeta
|
||||
func PluginMetaSchema() *resource.SimpleSchema {
|
||||
return schemaPluginMeta
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Schema = kindPluginMeta
|
||||
@@ -0,0 +1,473 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
// JSON configuration schema for Grafana plugins
|
||||
// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaJSONData struct {
|
||||
// Unique name of the plugin
|
||||
Id string `json:"id"`
|
||||
// Plugin type
|
||||
Type PluginMetaJSONDataType `json:"type"`
|
||||
// Human-readable name of the plugin
|
||||
Name string `json:"name"`
|
||||
// Metadata for the plugin
|
||||
Info PluginMetaInfo `json:"info"`
|
||||
// Dependency information
|
||||
Dependencies PluginMetaDependencies `json:"dependencies"`
|
||||
// Optional fields
|
||||
Alerting *bool `json:"alerting,omitempty"`
|
||||
Annotations *bool `json:"annotations,omitempty"`
|
||||
AutoEnabled *bool `json:"autoEnabled,omitempty"`
|
||||
Backend *bool `json:"backend,omitempty"`
|
||||
BuildMode *string `json:"buildMode,omitempty"`
|
||||
BuiltIn *bool `json:"builtIn,omitempty"`
|
||||
Category *PluginMetaJSONDataCategory `json:"category,omitempty"`
|
||||
EnterpriseFeatures *PluginMetaEnterpriseFeatures `json:"enterpriseFeatures,omitempty"`
|
||||
Executable *string `json:"executable,omitempty"`
|
||||
HideFromList *bool `json:"hideFromList,omitempty"`
|
||||
// +listType=atomic
|
||||
Includes []PluginMetaInclude `json:"includes,omitempty"`
|
||||
Logs *bool `json:"logs,omitempty"`
|
||||
Metrics *bool `json:"metrics,omitempty"`
|
||||
MultiValueFilterOperators *bool `json:"multiValueFilterOperators,omitempty"`
|
||||
PascalName *string `json:"pascalName,omitempty"`
|
||||
Preload *bool `json:"preload,omitempty"`
|
||||
QueryOptions *PluginMetaQueryOptions `json:"queryOptions,omitempty"`
|
||||
// +listType=atomic
|
||||
Routes []PluginMetaRoute `json:"routes,omitempty"`
|
||||
SkipDataQuery *bool `json:"skipDataQuery,omitempty"`
|
||||
State *PluginMetaJSONDataState `json:"state,omitempty"`
|
||||
Streaming *bool `json:"streaming,omitempty"`
|
||||
Tracing *bool `json:"tracing,omitempty"`
|
||||
Iam *PluginMetaIAM `json:"iam,omitempty"`
|
||||
// +listType=atomic
|
||||
Roles []PluginMetaRole `json:"roles,omitempty"`
|
||||
Extensions *PluginMetaExtensions `json:"extensions,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaJSONData creates a new PluginMetaJSONData object.
|
||||
func NewPluginMetaJSONData() *PluginMetaJSONData {
|
||||
return &PluginMetaJSONData{
|
||||
Info: *NewPluginMetaInfo(),
|
||||
Dependencies: *NewPluginMetaDependencies(),
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaInfo struct {
|
||||
// Required fields
|
||||
// +listType=set
|
||||
Keywords []string `json:"keywords"`
|
||||
Logos PluginMetaV0alpha1InfoLogos `json:"logos"`
|
||||
Updated string `json:"updated"`
|
||||
Version string `json:"version"`
|
||||
// Optional fields
|
||||
Author *PluginMetaV0alpha1InfoAuthor `json:"author,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
// +listType=atomic
|
||||
Links []PluginMetaV0alpha1InfoLinks `json:"links,omitempty"`
|
||||
// +listType=atomic
|
||||
Screenshots []PluginMetaV0alpha1InfoScreenshots `json:"screenshots,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaInfo creates a new PluginMetaInfo object.
|
||||
func NewPluginMetaInfo() *PluginMetaInfo {
|
||||
return &PluginMetaInfo{
|
||||
Keywords: []string{},
|
||||
Logos: *NewPluginMetaV0alpha1InfoLogos(),
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaDependencies struct {
|
||||
// Required field
|
||||
GrafanaDependency string `json:"grafanaDependency"`
|
||||
// Optional fields
|
||||
GrafanaVersion *string `json:"grafanaVersion,omitempty"`
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
Plugins []PluginMetaV0alpha1DependenciesPlugins `json:"plugins,omitempty"`
|
||||
Extensions *PluginMetaV0alpha1DependenciesExtensions `json:"extensions,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaDependencies creates a new PluginMetaDependencies object.
|
||||
func NewPluginMetaDependencies() *PluginMetaDependencies {
|
||||
return &PluginMetaDependencies{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaEnterpriseFeatures struct {
|
||||
// Allow additional properties
|
||||
HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaEnterpriseFeatures creates a new PluginMetaEnterpriseFeatures object.
|
||||
func NewPluginMetaEnterpriseFeatures() *PluginMetaEnterpriseFeatures {
|
||||
return &PluginMetaEnterpriseFeatures{
|
||||
HealthDiagnosticsErrors: (func(input bool) *bool { return &input })(false),
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaInclude struct {
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
Type *PluginMetaIncludeType `json:"type,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Component *string `json:"component,omitempty"`
|
||||
Role *PluginMetaIncludeRole `json:"role,omitempty"`
|
||||
Action *string `json:"action,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
AddToNav *bool `json:"addToNav,omitempty"`
|
||||
DefaultNav *bool `json:"defaultNav,omitempty"`
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaInclude creates a new PluginMetaInclude object.
|
||||
func NewPluginMetaInclude() *PluginMetaInclude {
|
||||
return &PluginMetaInclude{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaQueryOptions struct {
|
||||
MaxDataPoints *bool `json:"maxDataPoints,omitempty"`
|
||||
MinInterval *bool `json:"minInterval,omitempty"`
|
||||
CacheTimeout *bool `json:"cacheTimeout,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaQueryOptions creates a new PluginMetaQueryOptions object.
|
||||
func NewPluginMetaQueryOptions() *PluginMetaQueryOptions {
|
||||
return &PluginMetaQueryOptions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaRoute struct {
|
||||
Path *string `json:"path,omitempty"`
|
||||
Method *string `json:"method,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
ReqSignedIn *bool `json:"reqSignedIn,omitempty"`
|
||||
ReqRole *string `json:"reqRole,omitempty"`
|
||||
ReqAction *string `json:"reqAction,omitempty"`
|
||||
// +listType=atomic
|
||||
Headers []string `json:"headers,omitempty"`
|
||||
Body map[string]interface{} `json:"body,omitempty"`
|
||||
TokenAuth *PluginMetaV0alpha1RouteTokenAuth `json:"tokenAuth,omitempty"`
|
||||
JwtTokenAuth *PluginMetaV0alpha1RouteJwtTokenAuth `json:"jwtTokenAuth,omitempty"`
|
||||
// +listType=atomic
|
||||
UrlParams []PluginMetaV0alpha1RouteUrlParams `json:"urlParams,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaRoute creates a new PluginMetaRoute object.
|
||||
func NewPluginMetaRoute() *PluginMetaRoute {
|
||||
return &PluginMetaRoute{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaIAM struct {
|
||||
// +listType=atomic
|
||||
Permissions []PluginMetaV0alpha1IAMPermissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaIAM creates a new PluginMetaIAM object.
|
||||
func NewPluginMetaIAM() *PluginMetaIAM {
|
||||
return &PluginMetaIAM{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaRole struct {
|
||||
Role *PluginMetaV0alpha1RoleRole `json:"role,omitempty"`
|
||||
// +listType=set
|
||||
Grants []string `json:"grants,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaRole creates a new PluginMetaRole object.
|
||||
func NewPluginMetaRole() *PluginMetaRole {
|
||||
return &PluginMetaRole{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaExtensions struct {
|
||||
// +listType=atomic
|
||||
AddedComponents []PluginMetaV0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"`
|
||||
// +listType=atomic
|
||||
AddedLinks []PluginMetaV0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"`
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
ExposedComponents []PluginMetaV0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"`
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
ExtensionPoints []PluginMetaV0alpha1ExtensionsExtensionPoints `json:"extensionPoints,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaExtensions creates a new PluginMetaExtensions object.
|
||||
func NewPluginMetaExtensions() *PluginMetaExtensions {
|
||||
return &PluginMetaExtensions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaSpec struct {
|
||||
PluginJSON PluginMetaJSONData `json:"pluginJSON"`
|
||||
}
|
||||
|
||||
// NewPluginMetaSpec creates a new PluginMetaSpec object.
|
||||
func NewPluginMetaSpec() *PluginMetaSpec {
|
||||
return &PluginMetaSpec{
|
||||
PluginJSON: *NewPluginMetaJSONData(),
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1InfoLogos struct {
|
||||
Small string `json:"small"`
|
||||
Large string `json:"large"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1InfoLogos creates a new PluginMetaV0alpha1InfoLogos object.
|
||||
func NewPluginMetaV0alpha1InfoLogos() *PluginMetaV0alpha1InfoLogos {
|
||||
return &PluginMetaV0alpha1InfoLogos{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1InfoAuthor struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1InfoAuthor creates a new PluginMetaV0alpha1InfoAuthor object.
|
||||
func NewPluginMetaV0alpha1InfoAuthor() *PluginMetaV0alpha1InfoAuthor {
|
||||
return &PluginMetaV0alpha1InfoAuthor{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1InfoLinks struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1InfoLinks creates a new PluginMetaV0alpha1InfoLinks object.
|
||||
func NewPluginMetaV0alpha1InfoLinks() *PluginMetaV0alpha1InfoLinks {
|
||||
return &PluginMetaV0alpha1InfoLinks{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1InfoScreenshots struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1InfoScreenshots creates a new PluginMetaV0alpha1InfoScreenshots object.
|
||||
func NewPluginMetaV0alpha1InfoScreenshots() *PluginMetaV0alpha1InfoScreenshots {
|
||||
return &PluginMetaV0alpha1InfoScreenshots{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1DependenciesPlugins struct {
|
||||
Id string `json:"id"`
|
||||
Type PluginMetaV0alpha1DependenciesPluginsType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1DependenciesPlugins creates a new PluginMetaV0alpha1DependenciesPlugins object.
|
||||
func NewPluginMetaV0alpha1DependenciesPlugins() *PluginMetaV0alpha1DependenciesPlugins {
|
||||
return &PluginMetaV0alpha1DependenciesPlugins{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1DependenciesExtensions struct {
|
||||
// +listType=set
|
||||
ExposedComponents []string `json:"exposedComponents,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1DependenciesExtensions creates a new PluginMetaV0alpha1DependenciesExtensions object.
|
||||
func NewPluginMetaV0alpha1DependenciesExtensions() *PluginMetaV0alpha1DependenciesExtensions {
|
||||
return &PluginMetaV0alpha1DependenciesExtensions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1RouteTokenAuth struct {
|
||||
Url *string `json:"url,omitempty"`
|
||||
// +listType=set
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1RouteTokenAuth creates a new PluginMetaV0alpha1RouteTokenAuth object.
|
||||
func NewPluginMetaV0alpha1RouteTokenAuth() *PluginMetaV0alpha1RouteTokenAuth {
|
||||
return &PluginMetaV0alpha1RouteTokenAuth{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1RouteJwtTokenAuth struct {
|
||||
Url *string `json:"url,omitempty"`
|
||||
// +listType=set
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1RouteJwtTokenAuth creates a new PluginMetaV0alpha1RouteJwtTokenAuth object.
|
||||
func NewPluginMetaV0alpha1RouteJwtTokenAuth() *PluginMetaV0alpha1RouteJwtTokenAuth {
|
||||
return &PluginMetaV0alpha1RouteJwtTokenAuth{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1RouteUrlParams struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Content *string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1RouteUrlParams creates a new PluginMetaV0alpha1RouteUrlParams object.
|
||||
func NewPluginMetaV0alpha1RouteUrlParams() *PluginMetaV0alpha1RouteUrlParams {
|
||||
return &PluginMetaV0alpha1RouteUrlParams{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1IAMPermissions struct {
|
||||
Action *string `json:"action,omitempty"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1IAMPermissions creates a new PluginMetaV0alpha1IAMPermissions object.
|
||||
func NewPluginMetaV0alpha1IAMPermissions() *PluginMetaV0alpha1IAMPermissions {
|
||||
return &PluginMetaV0alpha1IAMPermissions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1RoleRolePermissions struct {
|
||||
Action *string `json:"action,omitempty"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1RoleRolePermissions creates a new PluginMetaV0alpha1RoleRolePermissions object.
|
||||
func NewPluginMetaV0alpha1RoleRolePermissions() *PluginMetaV0alpha1RoleRolePermissions {
|
||||
return &PluginMetaV0alpha1RoleRolePermissions{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1RoleRole struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
// +listType=atomic
|
||||
Permissions []PluginMetaV0alpha1RoleRolePermissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1RoleRole creates a new PluginMetaV0alpha1RoleRole object.
|
||||
func NewPluginMetaV0alpha1RoleRole() *PluginMetaV0alpha1RoleRole {
|
||||
return &PluginMetaV0alpha1RoleRole{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1ExtensionsAddedComponents struct {
|
||||
// +listType=set
|
||||
Targets []string `json:"targets"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1ExtensionsAddedComponents creates a new PluginMetaV0alpha1ExtensionsAddedComponents object.
|
||||
func NewPluginMetaV0alpha1ExtensionsAddedComponents() *PluginMetaV0alpha1ExtensionsAddedComponents {
|
||||
return &PluginMetaV0alpha1ExtensionsAddedComponents{
|
||||
Targets: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1ExtensionsAddedLinks struct {
|
||||
// +listType=set
|
||||
Targets []string `json:"targets"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1ExtensionsAddedLinks creates a new PluginMetaV0alpha1ExtensionsAddedLinks object.
|
||||
func NewPluginMetaV0alpha1ExtensionsAddedLinks() *PluginMetaV0alpha1ExtensionsAddedLinks {
|
||||
return &PluginMetaV0alpha1ExtensionsAddedLinks{
|
||||
Targets: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1ExtensionsExposedComponents struct {
|
||||
Id string `json:"id"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1ExtensionsExposedComponents creates a new PluginMetaV0alpha1ExtensionsExposedComponents object.
|
||||
func NewPluginMetaV0alpha1ExtensionsExposedComponents() *PluginMetaV0alpha1ExtensionsExposedComponents {
|
||||
return &PluginMetaV0alpha1ExtensionsExposedComponents{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1ExtensionsExtensionPoints struct {
|
||||
Id string `json:"id"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1ExtensionsExtensionPoints creates a new PluginMetaV0alpha1ExtensionsExtensionPoints object.
|
||||
func NewPluginMetaV0alpha1ExtensionsExtensionPoints() *PluginMetaV0alpha1ExtensionsExtensionPoints {
|
||||
return &PluginMetaV0alpha1ExtensionsExtensionPoints{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaJSONDataType string
|
||||
|
||||
const (
|
||||
PluginMetaJSONDataTypeApp PluginMetaJSONDataType = "app"
|
||||
PluginMetaJSONDataTypeDatasource PluginMetaJSONDataType = "datasource"
|
||||
PluginMetaJSONDataTypePanel PluginMetaJSONDataType = "panel"
|
||||
PluginMetaJSONDataTypeRenderer PluginMetaJSONDataType = "renderer"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaJSONDataCategory string
|
||||
|
||||
const (
|
||||
PluginMetaJSONDataCategoryTsdb PluginMetaJSONDataCategory = "tsdb"
|
||||
PluginMetaJSONDataCategoryLogging PluginMetaJSONDataCategory = "logging"
|
||||
PluginMetaJSONDataCategoryCloud PluginMetaJSONDataCategory = "cloud"
|
||||
PluginMetaJSONDataCategoryTracing PluginMetaJSONDataCategory = "tracing"
|
||||
PluginMetaJSONDataCategoryProfiling PluginMetaJSONDataCategory = "profiling"
|
||||
PluginMetaJSONDataCategorySql PluginMetaJSONDataCategory = "sql"
|
||||
PluginMetaJSONDataCategoryEnterprise PluginMetaJSONDataCategory = "enterprise"
|
||||
PluginMetaJSONDataCategoryIot PluginMetaJSONDataCategory = "iot"
|
||||
PluginMetaJSONDataCategoryOther PluginMetaJSONDataCategory = "other"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaJSONDataState string
|
||||
|
||||
const (
|
||||
PluginMetaJSONDataStateAlpha PluginMetaJSONDataState = "alpha"
|
||||
PluginMetaJSONDataStateBeta PluginMetaJSONDataState = "beta"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaIncludeType string
|
||||
|
||||
const (
|
||||
PluginMetaIncludeTypeDashboard PluginMetaIncludeType = "dashboard"
|
||||
PluginMetaIncludeTypePage PluginMetaIncludeType = "page"
|
||||
PluginMetaIncludeTypePanel PluginMetaIncludeType = "panel"
|
||||
PluginMetaIncludeTypeDatasource PluginMetaIncludeType = "datasource"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaIncludeRole string
|
||||
|
||||
const (
|
||||
PluginMetaIncludeRoleAdmin PluginMetaIncludeRole = "Admin"
|
||||
PluginMetaIncludeRoleEditor PluginMetaIncludeRole = "Editor"
|
||||
PluginMetaIncludeRoleViewer PluginMetaIncludeRole = "Viewer"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1DependenciesPluginsType string
|
||||
|
||||
const (
|
||||
PluginMetaV0alpha1DependenciesPluginsTypeApp PluginMetaV0alpha1DependenciesPluginsType = "app"
|
||||
PluginMetaV0alpha1DependenciesPluginsTypeDatasource PluginMetaV0alpha1DependenciesPluginsType = "datasource"
|
||||
PluginMetaV0alpha1DependenciesPluginsTypePanel PluginMetaV0alpha1DependenciesPluginsType = "panel"
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetastatusOperatorState struct {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
LastEvaluation string `json:"lastEvaluation"`
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
State PluginMetaStatusOperatorStateState `json:"state"`
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
DescriptiveState *string `json:"descriptiveState,omitempty"`
|
||||
// details contains any extra information that is operator-specific
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetastatusOperatorState creates a new PluginMetastatusOperatorState object.
|
||||
func NewPluginMetastatusOperatorState() *PluginMetastatusOperatorState {
|
||||
return &PluginMetastatusOperatorState{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaStatus struct {
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
OperatorStates map[string]PluginMetastatusOperatorState `json:"operatorStates,omitempty"`
|
||||
// additionalFields is reserved for future use
|
||||
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaStatus creates a new PluginMetaStatus object.
|
||||
func NewPluginMetaStatus() *PluginMetaStatus {
|
||||
return &PluginMetaStatus{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaStatusOperatorStateState string
|
||||
|
||||
const (
|
||||
PluginMetaStatusOperatorStateStateSuccess PluginMetaStatusOperatorStateState = "success"
|
||||
PluginMetaStatusOperatorStateStateInProgress PluginMetaStatusOperatorStateState = "in_progress"
|
||||
PluginMetaStatusOperatorStateStateFailed PluginMetaStatusOperatorStateState = "failed"
|
||||
)
|
||||
+16
-229
File diff suppressed because one or more lines are too long
+14
-47
@@ -2,27 +2,23 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
"github.com/grafana/grafana-app-sdk/k8s"
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana-app-sdk/operator"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/grafana/grafana-app-sdk/simple"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
pluginsapi "github.com/grafana/grafana/apps/plugins/pkg/apis"
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/plugins/pkg/app/meta"
|
||||
)
|
||||
|
||||
func New(cfg app.Config) (app.App, error) {
|
||||
cfg.KubeConfig.APIPath = "apis"
|
||||
clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig())
|
||||
client, err := pluginsv0alpha1.NewPluginClientFromGenerator(clientGenerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
specificConfig, ok := cfg.SpecificConfig.(*PluginAppConfig)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid config type")
|
||||
}
|
||||
|
||||
simpleConfig := simple.AppConfig{
|
||||
@@ -38,27 +34,9 @@ func New(cfg app.Config) (app.App, error) {
|
||||
ManagedKinds: []simple.AppManagedKind{
|
||||
{
|
||||
Kind: pluginsv0alpha1.PluginKind(),
|
||||
CustomRoutes: simple.AppCustomRouteHandlers{
|
||||
simple.AppCustomRoute{
|
||||
Method: http.MethodGet,
|
||||
Path: "meta",
|
||||
}: func(ctx context.Context, w app.CustomRouteResponseWriter, req *app.CustomRouteRequest) error {
|
||||
plugin, err := client.Get(ctx, resource.Identifier{
|
||||
Namespace: req.ResourceIdentifier.Namespace,
|
||||
Name: req.ResourceIdentifier.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logging.DefaultLogger.Debug("fetched plugin", "plugin", plugin)
|
||||
// TODO: Implement this in future PR
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
if _, err := w.Write([]byte("Not implemented")); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: pluginsv0alpha1.PluginMetaKind(),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -73,23 +51,12 @@ func New(cfg app.Config) (app.App, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Register MetaProviderManager as a runnable so its cleanup goroutine is managed by the app lifecycle
|
||||
a.AddRunnable(specificConfig.MetaProviderManager)
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func GetKinds() map[schema.GroupVersion][]resource.Kind {
|
||||
kinds := make(map[schema.GroupVersion][]resource.Kind)
|
||||
manifest := pluginsapi.LocalManifest()
|
||||
for _, v := range manifest.ManifestData.Versions {
|
||||
gv := schema.GroupVersion{
|
||||
Group: manifest.ManifestData.Group,
|
||||
Version: v.Name,
|
||||
}
|
||||
for _, k := range v.Kinds {
|
||||
kind, ok := pluginsapi.ManifestGoTypeAssociator(k.Kind, v.Name)
|
||||
if ok {
|
||||
kinds[gv] = append(kinds[gv], kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
return kinds
|
||||
type PluginAppConfig struct {
|
||||
MetaProviderManager *meta.ProviderManager
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
)
|
||||
|
||||
func TestCloudProvider_GetMeta(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("successfully fetches plugin metadata", func(t *testing.T) {
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodGet, r.Method)
|
||||
assert.Equal(t, "/api/plugins/test-plugin/versions/1.0.0", r.URL.Path)
|
||||
assert.Equal(t, "application/json", r.Header.Get("Accept"))
|
||||
assert.Equal(t, "grafana-plugins-app", r.Header.Get("User-Agent"))
|
||||
|
||||
response := grafanaComPluginVersionMeta{
|
||||
PluginID: "test-plugin",
|
||||
Version: "1.0.0",
|
||||
JSON: expectedMeta,
|
||||
Description: "Test description",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewCloudProvider(server.URL + "/api/plugins")
|
||||
result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
assert.Equal(t, defaultCloudTTL, result.TTL)
|
||||
})
|
||||
|
||||
t.Run("returns ErrMetaNotFound for 404 status", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewCloudProvider(server.URL + "/api/plugins")
|
||||
result, err := provider.GetMeta(ctx, "nonexistent-plugin", "1.0.0")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrMetaNotFound))
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns error for non-200 status codes", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewCloudProvider(server.URL + "/api/plugins")
|
||||
result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unexpected status code 500")
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns error for invalid JSON response", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("invalid json"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewCloudProvider(server.URL + "/api/plugins")
|
||||
result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to decode response")
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns error for invalid API URL", func(t *testing.T) {
|
||||
provider := NewCloudProvider("://invalid-url")
|
||||
result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid grafana.com API URL")
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("uses custom TTL when provided", func(t *testing.T) {
|
||||
customTTL := 2 * time.Hour
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response := grafanaComPluginVersionMeta{
|
||||
PluginID: "test-plugin",
|
||||
Version: "1.0.0",
|
||||
JSON: expectedMeta,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewCloudProviderWithTTL(server.URL+"/api/plugins", customTTL)
|
||||
result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, customTTL, result.TTL)
|
||||
})
|
||||
|
||||
t.Run("handles context cancellation", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
provider := NewCloudProvider(server.URL + "/api/plugins")
|
||||
result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewCloudProvider(t *testing.T) {
|
||||
t.Run("creates provider with default TTL", func(t *testing.T) {
|
||||
provider := NewCloudProvider("https://grafana.com/api/plugins")
|
||||
assert.Equal(t, defaultCloudTTL, provider.ttl)
|
||||
assert.NotNil(t, provider.httpClient)
|
||||
assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL)
|
||||
})
|
||||
|
||||
t.Run("uses default URL when empty", func(t *testing.T) {
|
||||
provider := NewCloudProvider("")
|
||||
assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewCloudProviderWithTTL(t *testing.T) {
|
||||
t.Run("creates provider with custom TTL", func(t *testing.T) {
|
||||
customTTL := 2 * time.Hour
|
||||
provider := NewCloudProviderWithTTL("https://grafana.com/api/plugins", customTTL)
|
||||
assert.Equal(t, customTTL, provider.ttl)
|
||||
})
|
||||
|
||||
t.Run("accepts zero TTL", func(t *testing.T) {
|
||||
provider := NewCloudProviderWithTTL("https://grafana.com/api/plugins", 0)
|
||||
assert.Equal(t, time.Duration(0), provider.ttl)
|
||||
})
|
||||
|
||||
t.Run("uses default URL when empty", func(t *testing.T) {
|
||||
provider := NewCloudProviderWithTTL("", defaultCloudTTL)
|
||||
assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCoreTTL = 24 * time.Hour
|
||||
)
|
||||
|
||||
// CoreProvider retrieves plugin metadata for core plugins.
|
||||
type CoreProvider struct {
|
||||
mu sync.RWMutex
|
||||
loadedPlugins map[string]pluginsv0alpha1.PluginMetaJSONData
|
||||
initialized bool
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewCoreProvider creates a new CoreProvider for core plugins.
|
||||
func NewCoreProvider() *CoreProvider {
|
||||
return NewCoreProviderWithTTL(defaultCoreTTL)
|
||||
}
|
||||
|
||||
// NewCoreProviderWithTTL creates a new CoreProvider with a custom TTL.
|
||||
func NewCoreProviderWithTTL(ttl time.Duration) *CoreProvider {
|
||||
return &CoreProvider{
|
||||
loadedPlugins: make(map[string]pluginsv0alpha1.PluginMetaJSONData),
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMeta retrieves plugin metadata for core plugins.
|
||||
func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result, error) {
|
||||
// Check cache first
|
||||
p.mu.RLock()
|
||||
if meta, found := p.loadedPlugins[pluginID]; found {
|
||||
p.mu.RUnlock()
|
||||
return &Result{
|
||||
Meta: meta,
|
||||
TTL: p.ttl,
|
||||
}, nil
|
||||
}
|
||||
p.mu.RUnlock()
|
||||
|
||||
// Initialize cache if not already done
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Double-check after acquiring write lock
|
||||
if meta, found := p.loadedPlugins[pluginID]; found {
|
||||
return &Result{
|
||||
Meta: meta,
|
||||
TTL: p.ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !p.initialized {
|
||||
if err := p.loadPlugins(ctx); err != nil {
|
||||
logging.DefaultLogger.Warn("CoreProvider: could not load core plugins, will return ErrMetaNotFound for all lookups", "error", err)
|
||||
// Mark as initialized even on failure so we don't keep trying
|
||||
p.initialized = true
|
||||
return nil, ErrMetaNotFound
|
||||
}
|
||||
p.initialized = true
|
||||
}
|
||||
|
||||
if meta, found := p.loadedPlugins[pluginID]; found {
|
||||
return &Result{
|
||||
Meta: meta,
|
||||
TTL: p.ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, ErrMetaNotFound
|
||||
}
|
||||
|
||||
// loadPlugins discovers and caches all core plugins.
|
||||
// Returns an error if the static root path cannot be found or if plugin discovery fails.
|
||||
// This error will be handled gracefully by GetMeta, which will return ErrMetaNotFound
|
||||
// to allow other providers to handle the request.
|
||||
func (p *CoreProvider) loadPlugins(ctx context.Context) error {
|
||||
var staticRootPath string
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
// Check if we're in the Grafana root
|
||||
publicPath := filepath.Join(wd, "public", "app", "plugins")
|
||||
if _, err = os.Stat(publicPath); err == nil {
|
||||
staticRootPath = filepath.Join(wd, "public")
|
||||
}
|
||||
}
|
||||
|
||||
if staticRootPath == "" {
|
||||
return errors.New("could not find Grafana static root path")
|
||||
}
|
||||
|
||||
datasourcePath := filepath.Join(staticRootPath, "app", "plugins", "datasource")
|
||||
panelPath := filepath.Join(staticRootPath, "app", "plugins", "panel")
|
||||
|
||||
src := sources.NewLocalSource(plugins.ClassCore, []string{datasourcePath, panelPath})
|
||||
ps, err := src.Discover(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(ps) == 0 {
|
||||
logging.DefaultLogger.Warn("CoreProvider: no core plugins found during discovery")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, bundle := range ps {
|
||||
meta := jsonDataToPluginMetaJSONData(bundle.Primary.JSONData)
|
||||
p.loadedPlugins[bundle.Primary.JSONData.ID] = meta
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// jsonDataToPluginMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.PluginMetaJSONData.
|
||||
// nolint:gocyclo
|
||||
func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.PluginMetaJSONData {
|
||||
meta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: jsonData.ID,
|
||||
Name: jsonData.Name,
|
||||
}
|
||||
|
||||
// Map plugin type
|
||||
switch jsonData.Type {
|
||||
case plugins.TypeApp:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeApp
|
||||
case plugins.TypeDataSource:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeDatasource
|
||||
case plugins.TypePanel:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypePanel
|
||||
case plugins.TypeRenderer:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeRenderer
|
||||
}
|
||||
|
||||
// Map Info
|
||||
meta.Info = pluginsv0alpha1.PluginMetaInfo{
|
||||
Keywords: jsonData.Info.Keywords,
|
||||
Logos: pluginsv0alpha1.PluginMetaV0alpha1InfoLogos{
|
||||
Small: jsonData.Info.Logos.Small,
|
||||
Large: jsonData.Info.Logos.Large,
|
||||
},
|
||||
Updated: jsonData.Info.Updated,
|
||||
Version: jsonData.Info.Version,
|
||||
}
|
||||
|
||||
if jsonData.Info.Description != "" {
|
||||
meta.Info.Description = &jsonData.Info.Description
|
||||
}
|
||||
|
||||
if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" {
|
||||
author := &pluginsv0alpha1.PluginMetaV0alpha1InfoAuthor{}
|
||||
if jsonData.Info.Author.Name != "" {
|
||||
author.Name = &jsonData.Info.Author.Name
|
||||
}
|
||||
if jsonData.Info.Author.URL != "" {
|
||||
author.Url = &jsonData.Info.Author.URL
|
||||
}
|
||||
meta.Info.Author = author
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Links) > 0 {
|
||||
meta.Info.Links = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links))
|
||||
for _, link := range jsonData.Info.Links {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1InfoLinks{}
|
||||
if link.Name != "" {
|
||||
v0Link.Name = &link.Name
|
||||
}
|
||||
if link.URL != "" {
|
||||
v0Link.Url = &link.URL
|
||||
}
|
||||
meta.Info.Links = append(meta.Info.Links, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Screenshots) > 0 {
|
||||
meta.Info.Screenshots = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots))
|
||||
for _, screenshot := range jsonData.Info.Screenshots {
|
||||
v0Screenshot := pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots{}
|
||||
if screenshot.Name != "" {
|
||||
v0Screenshot.Name = &screenshot.Name
|
||||
}
|
||||
if screenshot.Path != "" {
|
||||
v0Screenshot.Path = &screenshot.Path
|
||||
}
|
||||
meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Dependencies
|
||||
meta.Dependencies = pluginsv0alpha1.PluginMetaDependencies{
|
||||
GrafanaDependency: jsonData.Dependencies.GrafanaDependency,
|
||||
}
|
||||
|
||||
if jsonData.Dependencies.GrafanaVersion != "" {
|
||||
meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Plugins) > 0 {
|
||||
meta.Dependencies.Plugins = make([]pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins))
|
||||
for _, dep := range jsonData.Dependencies.Plugins {
|
||||
var depType pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsType
|
||||
switch dep.Type {
|
||||
case "app":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeApp
|
||||
case "datasource":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeDatasource
|
||||
case "panel":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypePanel
|
||||
}
|
||||
meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins{
|
||||
Id: dep.ID,
|
||||
Type: depType,
|
||||
Name: dep.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 {
|
||||
meta.Dependencies.Extensions = &pluginsv0alpha1.PluginMetaV0alpha1DependenciesExtensions{
|
||||
ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents,
|
||||
}
|
||||
}
|
||||
|
||||
// Map optional boolean fields
|
||||
if jsonData.Alerting {
|
||||
meta.Alerting = &jsonData.Alerting
|
||||
}
|
||||
if jsonData.Annotations {
|
||||
meta.Annotations = &jsonData.Annotations
|
||||
}
|
||||
if jsonData.AutoEnabled {
|
||||
meta.AutoEnabled = &jsonData.AutoEnabled
|
||||
}
|
||||
if jsonData.Backend {
|
||||
meta.Backend = &jsonData.Backend
|
||||
}
|
||||
if jsonData.BuiltIn {
|
||||
meta.BuiltIn = &jsonData.BuiltIn
|
||||
}
|
||||
if jsonData.HideFromList {
|
||||
meta.HideFromList = &jsonData.HideFromList
|
||||
}
|
||||
if jsonData.Logs {
|
||||
meta.Logs = &jsonData.Logs
|
||||
}
|
||||
if jsonData.Metrics {
|
||||
meta.Metrics = &jsonData.Metrics
|
||||
}
|
||||
if jsonData.MultiValueFilterOperators {
|
||||
meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators
|
||||
}
|
||||
if jsonData.Preload {
|
||||
meta.Preload = &jsonData.Preload
|
||||
}
|
||||
if jsonData.SkipDataQuery {
|
||||
meta.SkipDataQuery = &jsonData.SkipDataQuery
|
||||
}
|
||||
if jsonData.Streaming {
|
||||
meta.Streaming = &jsonData.Streaming
|
||||
}
|
||||
if jsonData.Tracing {
|
||||
meta.Tracing = &jsonData.Tracing
|
||||
}
|
||||
|
||||
// Map category
|
||||
if jsonData.Category != "" {
|
||||
var category pluginsv0alpha1.PluginMetaJSONDataCategory
|
||||
switch jsonData.Category {
|
||||
case "tsdb":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTsdb
|
||||
case "logging":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryLogging
|
||||
case "cloud":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryCloud
|
||||
case "tracing":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTracing
|
||||
case "profiling":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryProfiling
|
||||
case "sql":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategorySql
|
||||
case "enterprise":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryEnterprise
|
||||
case "iot":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryIot
|
||||
case "other":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
default:
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
}
|
||||
meta.Category = &category
|
||||
}
|
||||
|
||||
// Map state
|
||||
if jsonData.State != "" {
|
||||
var state pluginsv0alpha1.PluginMetaJSONDataState
|
||||
switch jsonData.State {
|
||||
case plugins.ReleaseStateAlpha:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateAlpha
|
||||
case plugins.ReleaseStateBeta:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateBeta
|
||||
default:
|
||||
}
|
||||
if state != "" {
|
||||
meta.State = &state
|
||||
}
|
||||
}
|
||||
|
||||
// Map executable
|
||||
if jsonData.Executable != "" {
|
||||
meta.Executable = &jsonData.Executable
|
||||
}
|
||||
|
||||
// Map QueryOptions
|
||||
if len(jsonData.QueryOptions) > 0 {
|
||||
queryOptions := &pluginsv0alpha1.PluginMetaQueryOptions{}
|
||||
if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok {
|
||||
queryOptions.MaxDataPoints = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["minInterval"]; ok {
|
||||
queryOptions.MinInterval = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok {
|
||||
queryOptions.CacheTimeout = &val
|
||||
}
|
||||
meta.QueryOptions = queryOptions
|
||||
}
|
||||
|
||||
// Map Includes
|
||||
if len(jsonData.Includes) > 0 {
|
||||
meta.Includes = make([]pluginsv0alpha1.PluginMetaInclude, 0, len(jsonData.Includes))
|
||||
for _, include := range jsonData.Includes {
|
||||
v0Include := pluginsv0alpha1.PluginMetaInclude{}
|
||||
if include.UID != "" {
|
||||
v0Include.Uid = &include.UID
|
||||
}
|
||||
if include.Type != "" {
|
||||
var includeType pluginsv0alpha1.PluginMetaIncludeType
|
||||
switch include.Type {
|
||||
case "dashboard":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDashboard
|
||||
case "page":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePage
|
||||
case "panel":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePanel
|
||||
case "datasource":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDatasource
|
||||
}
|
||||
v0Include.Type = &includeType
|
||||
}
|
||||
if include.Name != "" {
|
||||
v0Include.Name = &include.Name
|
||||
}
|
||||
if include.Component != "" {
|
||||
v0Include.Component = &include.Component
|
||||
}
|
||||
if include.Role != "" {
|
||||
var role pluginsv0alpha1.PluginMetaIncludeRole
|
||||
switch include.Role {
|
||||
case "Admin":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleAdmin
|
||||
case "Editor":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleEditor
|
||||
case "Viewer":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleViewer
|
||||
}
|
||||
v0Include.Role = &role
|
||||
}
|
||||
if include.Action != "" {
|
||||
v0Include.Action = &include.Action
|
||||
}
|
||||
if include.Path != "" {
|
||||
v0Include.Path = &include.Path
|
||||
}
|
||||
if include.AddToNav {
|
||||
v0Include.AddToNav = &include.AddToNav
|
||||
}
|
||||
if include.DefaultNav {
|
||||
v0Include.DefaultNav = &include.DefaultNav
|
||||
}
|
||||
if include.Icon != "" {
|
||||
v0Include.Icon = &include.Icon
|
||||
}
|
||||
meta.Includes = append(meta.Includes, v0Include)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Routes
|
||||
if len(jsonData.Routes) > 0 {
|
||||
meta.Routes = make([]pluginsv0alpha1.PluginMetaRoute, 0, len(jsonData.Routes))
|
||||
for _, route := range jsonData.Routes {
|
||||
v0Route := pluginsv0alpha1.PluginMetaRoute{}
|
||||
if route.Path != "" {
|
||||
v0Route.Path = &route.Path
|
||||
}
|
||||
if route.Method != "" {
|
||||
v0Route.Method = &route.Method
|
||||
}
|
||||
if route.URL != "" {
|
||||
v0Route.Url = &route.URL
|
||||
}
|
||||
if route.ReqRole != "" {
|
||||
reqRole := string(route.ReqRole)
|
||||
v0Route.ReqRole = &reqRole
|
||||
}
|
||||
if route.ReqAction != "" {
|
||||
v0Route.ReqAction = &route.ReqAction
|
||||
}
|
||||
if len(route.Headers) > 0 {
|
||||
headers := make([]string, 0, len(route.Headers))
|
||||
for _, header := range route.Headers {
|
||||
headers = append(headers, header.Name+": "+header.Content)
|
||||
}
|
||||
v0Route.Headers = headers
|
||||
}
|
||||
if len(route.URLParams) > 0 {
|
||||
v0Route.UrlParams = make([]pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams, 0, len(route.URLParams))
|
||||
for _, param := range route.URLParams {
|
||||
v0Param := pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams{}
|
||||
if param.Name != "" {
|
||||
v0Param.Name = ¶m.Name
|
||||
}
|
||||
if param.Content != "" {
|
||||
v0Param.Content = ¶m.Content
|
||||
}
|
||||
v0Route.UrlParams = append(v0Route.UrlParams, v0Param)
|
||||
}
|
||||
}
|
||||
if route.TokenAuth != nil {
|
||||
v0Route.TokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteTokenAuth{}
|
||||
if route.TokenAuth.Url != "" {
|
||||
v0Route.TokenAuth.Url = &route.TokenAuth.Url
|
||||
}
|
||||
if len(route.TokenAuth.Scopes) > 0 {
|
||||
v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes
|
||||
}
|
||||
if len(route.TokenAuth.Params) > 0 {
|
||||
v0Route.TokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.TokenAuth.Params {
|
||||
v0Route.TokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if route.JwtTokenAuth != nil {
|
||||
v0Route.JwtTokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteJwtTokenAuth{}
|
||||
if route.JwtTokenAuth.Url != "" {
|
||||
v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url
|
||||
}
|
||||
if len(route.JwtTokenAuth.Scopes) > 0 {
|
||||
v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes
|
||||
}
|
||||
if len(route.JwtTokenAuth.Params) > 0 {
|
||||
v0Route.JwtTokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.JwtTokenAuth.Params {
|
||||
v0Route.JwtTokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(route.Body) > 0 {
|
||||
var bodyMap map[string]interface{}
|
||||
if err := json.Unmarshal(route.Body, &bodyMap); err == nil {
|
||||
v0Route.Body = bodyMap
|
||||
}
|
||||
}
|
||||
meta.Routes = append(meta.Routes, v0Route)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Extensions
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 ||
|
||||
len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions := &pluginsv0alpha1.PluginMetaExtensions{}
|
||||
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 {
|
||||
extensions.AddedLinks = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks))
|
||||
for _, link := range jsonData.Extensions.AddedLinks {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks{
|
||||
Targets: link.Targets,
|
||||
Title: link.Title,
|
||||
}
|
||||
if link.Description != "" {
|
||||
v0Link.Description = &link.Description
|
||||
}
|
||||
extensions.AddedLinks = append(extensions.AddedLinks, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.AddedComponents) > 0 {
|
||||
extensions.AddedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents))
|
||||
for _, comp := range jsonData.Extensions.AddedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents{
|
||||
Targets: comp.Targets,
|
||||
Title: comp.Title,
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.AddedComponents = append(extensions.AddedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExposedComponents) > 0 {
|
||||
extensions.ExposedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents))
|
||||
for _, comp := range jsonData.Extensions.ExposedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents{
|
||||
Id: comp.Id,
|
||||
}
|
||||
if comp.Title != "" {
|
||||
v0Comp.Title = &comp.Title
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions.ExtensionPoints = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints))
|
||||
for _, point := range jsonData.Extensions.ExtensionPoints {
|
||||
v0Point := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints{
|
||||
Id: point.Id,
|
||||
}
|
||||
if point.Title != "" {
|
||||
v0Point.Title = &point.Title
|
||||
}
|
||||
if point.Description != "" {
|
||||
v0Point.Description = &point.Description
|
||||
}
|
||||
extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point)
|
||||
}
|
||||
}
|
||||
|
||||
meta.Extensions = extensions
|
||||
}
|
||||
|
||||
// Map Roles
|
||||
if len(jsonData.Roles) > 0 {
|
||||
meta.Roles = make([]pluginsv0alpha1.PluginMetaRole, 0, len(jsonData.Roles))
|
||||
for _, role := range jsonData.Roles {
|
||||
v0Role := pluginsv0alpha1.PluginMetaRole{
|
||||
Grants: role.Grants,
|
||||
}
|
||||
if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole := &pluginsv0alpha1.PluginMetaV0alpha1RoleRole{}
|
||||
if role.Role.Name != "" {
|
||||
v0RoleRole.Name = &role.Role.Name
|
||||
}
|
||||
if role.Role.Description != "" {
|
||||
v0RoleRole.Description = &role.Role.Description
|
||||
}
|
||||
if len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole.Permissions = make([]pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions))
|
||||
for _, perm := range role.Role.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm)
|
||||
}
|
||||
}
|
||||
v0Role.Role = v0RoleRole
|
||||
}
|
||||
meta.Roles = append(meta.Roles, v0Role)
|
||||
}
|
||||
}
|
||||
|
||||
// Map IAM
|
||||
if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 {
|
||||
iam := &pluginsv0alpha1.PluginMetaIAM{
|
||||
Permissions: make([]pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)),
|
||||
}
|
||||
for _, perm := range jsonData.IAM.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
iam.Permissions = append(iam.Permissions, v0Perm)
|
||||
}
|
||||
meta.Iam = iam
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
func TestCoreProvider_GetMeta(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns cached plugin when available", func(t *testing.T) {
|
||||
provider := NewCoreProvider()
|
||||
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.loadedPlugins["test-plugin"] = expectedMeta
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
assert.Equal(t, defaultCoreTTL, result.TTL)
|
||||
})
|
||||
|
||||
t.Run("returns ErrMetaNotFound for non-existent plugin", func(t *testing.T) {
|
||||
provider := NewCoreProvider()
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
result, err := provider.GetMeta(ctx, "nonexistent-plugin", "1.0.0")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrMetaNotFound))
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("ignores version parameter", func(t *testing.T) {
|
||||
provider := NewCoreProvider()
|
||||
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.loadedPlugins["test-plugin"] = expectedMeta
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
result1, err1 := provider.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
result2, err2 := provider.GetMeta(ctx, "test-plugin", "2.0.0")
|
||||
|
||||
require.NoError(t, err1)
|
||||
require.NoError(t, err2)
|
||||
assert.Equal(t, result1.Meta, result2.Meta)
|
||||
})
|
||||
|
||||
t.Run("uses custom TTL when provided", func(t *testing.T) {
|
||||
customTTL := 2 * time.Hour
|
||||
provider := NewCoreProviderWithTTL(customTTL)
|
||||
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.loadedPlugins["test-plugin"] = expectedMeta
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, customTTL, result.TTL)
|
||||
})
|
||||
|
||||
t.Run("gracefully handles initialization failure and returns ErrMetaNotFound", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
oldWd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = os.Chdir(oldWd)
|
||||
}()
|
||||
|
||||
require.NoError(t, os.Chdir(tempDir))
|
||||
|
||||
provider := NewCoreProvider()
|
||||
result, err := provider.GetMeta(ctx, "any-plugin", "1.0.0")
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrMetaNotFound))
|
||||
assert.Nil(t, result)
|
||||
|
||||
initialized := provider.initialized
|
||||
assert.True(t, initialized, "provider should be marked as initialized even after failure")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCoreProvider_loadPlugins(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("loads all core plugins", func(t *testing.T) {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
testDir := filepath.Dir(filename)
|
||||
grafanaRoot := filepath.Join(testDir, "..", "..", "..", "..", "..")
|
||||
grafanaRoot, err := filepath.Abs(grafanaRoot)
|
||||
require.NoError(t, err)
|
||||
|
||||
publicPath := filepath.Join(grafanaRoot, "public", "app", "plugins")
|
||||
if _, err = os.Stat(publicPath); err != nil {
|
||||
t.Skipf("Grafana root not found at %s, skipping integration test: %v", publicPath, err)
|
||||
}
|
||||
|
||||
require.NoError(t, os.Chdir(grafanaRoot))
|
||||
|
||||
provider := NewCoreProvider()
|
||||
err = provider.loadPlugins(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, provider.loadedPlugins, 53)
|
||||
})
|
||||
|
||||
t.Run("returns error when static root path not found", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
oldWd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = os.Chdir(oldWd)
|
||||
}()
|
||||
|
||||
require.NoError(t, os.Chdir(tempDir))
|
||||
|
||||
provider := NewCoreProvider()
|
||||
err = provider.loadPlugins(ctx)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "could not find Grafana static root path")
|
||||
})
|
||||
|
||||
t.Run("returns no error when no plugins found", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
publicPath := filepath.Join(tempDir, "public", "app", "plugins")
|
||||
require.NoError(t, os.MkdirAll(publicPath, 0750))
|
||||
|
||||
oldWd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = os.Chdir(oldWd)
|
||||
}()
|
||||
|
||||
require.NoError(t, os.Chdir(tempDir))
|
||||
|
||||
provider := NewCoreProvider()
|
||||
err = provider.loadPlugins(ctx)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("successfully loads plugins when structure exists", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
publicPath := filepath.Join(tempDir, "public", "app", "plugins")
|
||||
datasourcePath := filepath.Join(publicPath, "datasource")
|
||||
panelPath := filepath.Join(publicPath, "panel")
|
||||
|
||||
require.NoError(t, os.MkdirAll(datasourcePath, 0750))
|
||||
require.NoError(t, os.MkdirAll(panelPath, 0750))
|
||||
|
||||
pluginDir := filepath.Join(datasourcePath, "test-datasource")
|
||||
require.NoError(t, os.MkdirAll(pluginDir, 0750))
|
||||
|
||||
pluginJSON := `{
|
||||
"id": "test-datasource",
|
||||
"name": "Test Datasource",
|
||||
"type": "datasource",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"description": "Test description"
|
||||
}
|
||||
}`
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(pluginJSON), 0644))
|
||||
|
||||
oldWd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = os.Chdir(oldWd)
|
||||
}()
|
||||
|
||||
require.NoError(t, os.Chdir(tempDir))
|
||||
|
||||
provider := NewCoreProvider()
|
||||
err = provider.loadPlugins(ctx)
|
||||
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
provider.mu.RLock()
|
||||
loaded := len(provider.loadedPlugins) > 0
|
||||
provider.mu.RUnlock()
|
||||
|
||||
if loaded {
|
||||
result, err := provider.GetMeta(ctx, "test-datasource", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-datasource", result.Meta.Id)
|
||||
assert.Equal(t, "Test Datasource", result.Meta.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewCoreProvider(t *testing.T) {
|
||||
t.Run("creates provider with default TTL", func(t *testing.T) {
|
||||
provider := NewCoreProvider()
|
||||
assert.Equal(t, defaultCoreTTL, provider.ttl)
|
||||
assert.NotNil(t, provider.loadedPlugins)
|
||||
assert.False(t, provider.initialized)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewCoreProviderWithTTL(t *testing.T) {
|
||||
t.Run("creates provider with custom TTL", func(t *testing.T) {
|
||||
customTTL := 2 * time.Hour
|
||||
provider := NewCoreProviderWithTTL(customTTL)
|
||||
assert.Equal(t, customTTL, provider.ttl)
|
||||
})
|
||||
|
||||
t.Run("accepts zero TTL", func(t *testing.T) {
|
||||
provider := NewCoreProviderWithTTL(0)
|
||||
assert.Equal(t, time.Duration(0), provider.ttl)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJsonDataToMeta(t *testing.T) {
|
||||
t.Run("converts basic plugin JSON data", func(t *testing.T) {
|
||||
jsonData := plugins.JSONData{
|
||||
ID: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: plugins.TypeDataSource,
|
||||
Info: plugins.Info{
|
||||
Version: "1.0.0",
|
||||
Description: "Test description",
|
||||
Keywords: []string{"test", "plugin"},
|
||||
Logos: plugins.Logos{
|
||||
Small: "small.png",
|
||||
Large: "large.png",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
meta := jsonDataToPluginMetaJSONData(jsonData)
|
||||
|
||||
assert.Equal(t, "test-plugin", meta.Id)
|
||||
assert.Equal(t, "Test Plugin", meta.Name)
|
||||
assert.Equal(t, pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, meta.Type)
|
||||
assert.Equal(t, "1.0.0", meta.Info.Version)
|
||||
assert.Equal(t, "Test description", *meta.Info.Description)
|
||||
assert.Equal(t, []string{"test", "plugin"}, meta.Info.Keywords)
|
||||
assert.Equal(t, "small.png", meta.Info.Logos.Small)
|
||||
assert.Equal(t, "large.png", meta.Info.Logos.Large)
|
||||
})
|
||||
|
||||
t.Run("handles optional fields", func(t *testing.T) {
|
||||
jsonData := plugins.JSONData{
|
||||
ID: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: plugins.TypePanel,
|
||||
Info: plugins.Info{
|
||||
Version: "1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
meta := jsonDataToPluginMetaJSONData(jsonData)
|
||||
|
||||
assert.Nil(t, meta.Info.Description)
|
||||
assert.Nil(t, meta.Info.Author)
|
||||
assert.Empty(t, meta.Info.Keywords)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCleanupInterval = 10 * time.Minute
|
||||
)
|
||||
|
||||
// cachedMeta represents a cached metadata entry with expiration time
|
||||
type cachedMeta struct {
|
||||
meta pluginsv0alpha1.PluginMetaJSONData
|
||||
ttl time.Duration
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// ProviderManager searches multiple providers for Plugin Meta in order until one succeeds, and caches
|
||||
// results with per-provider TTLs.
|
||||
// It implements app.Runnable to manage the cleanup goroutine lifecycle.
|
||||
type ProviderManager struct {
|
||||
providers []Provider
|
||||
cache map[string]*cachedMeta
|
||||
cacheMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewProviderManager creates a new ProviderManager that chains the given providers
|
||||
// and caches results with per-provider TTLs.
|
||||
func NewProviderManager(providers ...Provider) *ProviderManager {
|
||||
if len(providers) == 0 {
|
||||
panic("ProviderManager requires at least one provider")
|
||||
}
|
||||
|
||||
return &ProviderManager{
|
||||
providers: providers,
|
||||
cache: make(map[string]*cachedMeta),
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements app.Runnable. It runs the cleanup loop until the context is cancelled.
|
||||
// This method blocks until the context is cancelled (when the app shuts down).
|
||||
func (pm *ProviderManager) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(defaultCleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
pm.cleanupExpired()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetMeta tries each provider in order until one succeeds, using cache when available.
|
||||
// Returns ErrMetaNotFound only if all providers return ErrMetaNotFound.
|
||||
// Otherwise, returns the last non-ErrMetaNotFound error if all providers fail.
|
||||
func (pm *ProviderManager) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
cacheKey := pm.cacheKey(pluginID, version)
|
||||
|
||||
// Check cache first
|
||||
pm.cacheMu.RLock()
|
||||
cached, exists := pm.cache[cacheKey]
|
||||
pm.cacheMu.RUnlock()
|
||||
|
||||
if exists && time.Now().Before(cached.expiresAt) {
|
||||
return &Result{
|
||||
Meta: cached.meta,
|
||||
TTL: cached.ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Try each provider in order until one succeeds
|
||||
var lastErr error
|
||||
for _, provider := range pm.providers {
|
||||
result, err := provider.GetMeta(ctx, pluginID, version)
|
||||
if err == nil {
|
||||
// Don't cache results with a zero TTL
|
||||
if result.TTL == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pm.cacheMu.Lock()
|
||||
pm.cache[cacheKey] = &cachedMeta{
|
||||
meta: result.Meta,
|
||||
ttl: result.TTL,
|
||||
expiresAt: time.Now().Add(result.TTL),
|
||||
}
|
||||
pm.cacheMu.Unlock()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// If not found, try next provider
|
||||
if errors.Is(err, ErrMetaNotFound) {
|
||||
continue
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return nil, fmt.Errorf("failed to fetch plugin metadata from any provider: %w", lastErr)
|
||||
}
|
||||
|
||||
return nil, ErrMetaNotFound
|
||||
}
|
||||
|
||||
// cleanupExpired removes expired entries from the cache.
|
||||
func (pm *ProviderManager) cleanupExpired() {
|
||||
now := time.Now()
|
||||
|
||||
pm.cacheMu.Lock()
|
||||
defer pm.cacheMu.Unlock()
|
||||
for key, entry := range pm.cache {
|
||||
if now.After(entry.expiresAt) {
|
||||
delete(pm.cache, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *ProviderManager) cacheKey(pluginID, version string) string {
|
||||
return fmt.Sprintf("%s:%s", pluginID, version)
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
)
|
||||
|
||||
func TestNewProviderManager(t *testing.T) {
|
||||
t.Run("panics with no providers", func(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
NewProviderManager()
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("creates manager with providers", func(t *testing.T) {
|
||||
provider1 := &mockProvider{}
|
||||
provider2 := &mockProvider{}
|
||||
|
||||
pm := NewProviderManager(provider1, provider2)
|
||||
|
||||
require.NotNil(t, pm)
|
||||
assert.Len(t, pm.providers, 2)
|
||||
assert.NotNil(t, pm.cache)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProviderManager_GetMeta(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns cached result when available and not expired", func(t *testing.T) {
|
||||
cachedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: cachedMeta,
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider)
|
||||
|
||||
result1, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result1)
|
||||
assert.Equal(t, cachedMeta, result1.Meta)
|
||||
assert.Equal(t, time.Hour, result1.TTL)
|
||||
|
||||
provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: pluginsv0alpha1.PluginMetaJSONData{Id: "different"},
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
}
|
||||
|
||||
result2, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result2)
|
||||
assert.Equal(t, cachedMeta, result2.Meta)
|
||||
assert.Equal(t, time.Hour, result2.TTL)
|
||||
})
|
||||
|
||||
t.Run("fetches from provider when not cached", func(t *testing.T) {
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
expectedTTL := 2 * time.Hour
|
||||
|
||||
provider := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: expectedMeta,
|
||||
TTL: expectedTTL,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider)
|
||||
|
||||
result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
assert.Equal(t, expectedTTL, result.TTL)
|
||||
|
||||
pm.cacheMu.RLock()
|
||||
cached, exists := pm.cache["test-plugin:1.0.0"]
|
||||
pm.cacheMu.RUnlock()
|
||||
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, expectedMeta, cached.meta)
|
||||
assert.Equal(t, expectedTTL, cached.ttl)
|
||||
})
|
||||
|
||||
t.Run("does not cache result with zero TTL and tries next provider", func(t *testing.T) {
|
||||
zeroTTLMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Zero TTL Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider1 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: zeroTTLMeta,
|
||||
TTL: 0,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
provider2 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: expectedMeta,
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider1, provider2)
|
||||
|
||||
result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
|
||||
pm.cacheMu.RLock()
|
||||
cached, exists := pm.cache["test-plugin:1.0.0"]
|
||||
pm.cacheMu.RUnlock()
|
||||
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, expectedMeta, cached.meta)
|
||||
assert.Equal(t, time.Hour, cached.ttl)
|
||||
})
|
||||
|
||||
t.Run("tries next provider when first returns ErrMetaNotFound", func(t *testing.T) {
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider1 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return nil, ErrMetaNotFound
|
||||
},
|
||||
}
|
||||
provider2 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: expectedMeta,
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider1, provider2)
|
||||
|
||||
result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
})
|
||||
|
||||
t.Run("returns ErrMetaNotFound when all providers return ErrMetaNotFound", func(t *testing.T) {
|
||||
provider1 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return nil, ErrMetaNotFound
|
||||
},
|
||||
}
|
||||
provider2 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return nil, ErrMetaNotFound
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider1, provider2)
|
||||
|
||||
result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrMetaNotFound))
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns error when provider returns non-ErrMetaNotFound error", func(t *testing.T) {
|
||||
expectedErr := errors.New("network error")
|
||||
|
||||
provider1 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return nil, expectedErr
|
||||
},
|
||||
}
|
||||
provider2 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return nil, ErrMetaNotFound
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider1, provider2)
|
||||
|
||||
result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to fetch plugin metadata from any provider")
|
||||
assert.ErrorIs(t, err, expectedErr)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips expired cache entries", func(t *testing.T) {
|
||||
expiredMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Expired Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
provider := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
callCount++
|
||||
if callCount == 1 {
|
||||
return &Result{
|
||||
Meta: expiredMeta,
|
||||
TTL: time.Nanosecond,
|
||||
}, nil
|
||||
}
|
||||
return &Result{
|
||||
Meta: expectedMeta,
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider)
|
||||
|
||||
result1, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expiredMeta, result1.Meta)
|
||||
|
||||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
result2, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedMeta, result2.Meta)
|
||||
assert.Equal(t, 2, callCount)
|
||||
})
|
||||
|
||||
t.Run("uses first successful provider", func(t *testing.T) {
|
||||
expectedMeta1 := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 1 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
expectedMeta2 := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 2 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider1 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: expectedMeta1,
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
provider2 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: expectedMeta2,
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider1, provider2)
|
||||
|
||||
result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta1, result.Meta)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProviderManager_Run(t *testing.T) {
|
||||
t.Run("runs cleanup loop until context cancelled", func(t *testing.T) {
|
||||
pm := NewProviderManager(&mockProvider{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- pm.Run(ctx)
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
|
||||
err := <-done
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProviderManager_cleanupExpired(t *testing.T) {
|
||||
t.Run("removes expired entries", func(t *testing.T) {
|
||||
validMeta := pluginsv0alpha1.PluginMetaJSONData{Id: "valid"}
|
||||
expiredMeta1 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired1"}
|
||||
expiredMeta2 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired2"}
|
||||
|
||||
provider := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
switch pluginID {
|
||||
case "valid":
|
||||
return &Result{Meta: validMeta, TTL: time.Hour}, nil
|
||||
case "expired1":
|
||||
return &Result{Meta: expiredMeta1, TTL: time.Nanosecond}, nil
|
||||
case "expired2":
|
||||
return &Result{Meta: expiredMeta2, TTL: time.Nanosecond}, nil
|
||||
}
|
||||
return nil, ErrMetaNotFound
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := pm.GetMeta(ctx, "expired1", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
_, err = pm.GetMeta(ctx, "expired2", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
_, err = pm.GetMeta(ctx, "valid", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
pm.cleanupExpired()
|
||||
|
||||
provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
if pluginID == "valid" {
|
||||
return &Result{Meta: validMeta, TTL: time.Hour}, nil
|
||||
}
|
||||
return nil, ErrMetaNotFound
|
||||
}
|
||||
|
||||
result, err := pm.GetMeta(ctx, "expired1", "1.0.0")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
|
||||
result, err = pm.GetMeta(ctx, "valid", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, validMeta, result.Meta)
|
||||
})
|
||||
|
||||
t.Run("handles empty cache", func(t *testing.T) {
|
||||
pm := NewProviderManager(&mockProvider{})
|
||||
pm.cleanupExpired()
|
||||
})
|
||||
}
|
||||
|
||||
func TestProviderManager_cacheKey(t *testing.T) {
|
||||
pm := NewProviderManager(&mockProvider{})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pluginID string
|
||||
version string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "basic key",
|
||||
pluginID: "test-plugin",
|
||||
version: "1.0.0",
|
||||
expected: "test-plugin:1.0.0",
|
||||
},
|
||||
{
|
||||
name: "empty version",
|
||||
pluginID: "test-plugin",
|
||||
version: "",
|
||||
expected: "test-plugin:",
|
||||
},
|
||||
{
|
||||
name: "empty plugin ID",
|
||||
pluginID: "",
|
||||
version: "1.0.0",
|
||||
expected: ":1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
key := pm.cacheKey(tt.pluginID, tt.version)
|
||||
assert.Equal(t, tt.expected, key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockProvider struct {
|
||||
getMetaFunc func(ctx context.Context, pluginID, version string) (*Result, error)
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
if m.getMetaFunc != nil {
|
||||
return m.getMetaFunc(ctx, pluginID, version)
|
||||
}
|
||||
return nil, ErrMetaNotFound
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMetaNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
// Result contains plugin metadata along with its recommended TTL.
|
||||
type Result struct {
|
||||
Meta pluginsv0alpha1.PluginMetaJSONData
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
// Provider is used for retrieving plugin metadata.
|
||||
type Provider interface {
|
||||
// GetMeta retrieves plugin metadata for the given plugin ID and version.
|
||||
// Returns the Result containing the PluginMetaJSONData and its recommended TTL.
|
||||
// If the plugin is not found, returns ErrMetaNotFound.
|
||||
GetMeta(ctx context.Context, pluginID, version string) (*Result, error)
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/plugins/pkg/app/meta"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
)
|
||||
|
||||
var (
|
||||
_ rest.Scoper = (*PluginMetaStorage)(nil)
|
||||
_ rest.SingularNameProvider = (*PluginMetaStorage)(nil)
|
||||
_ rest.Getter = (*PluginMetaStorage)(nil)
|
||||
_ rest.Lister = (*PluginMetaStorage)(nil)
|
||||
_ rest.Storage = (*PluginMetaStorage)(nil)
|
||||
_ rest.TableConvertor = (*PluginMetaStorage)(nil)
|
||||
)
|
||||
|
||||
type PluginMetaStorage struct {
|
||||
metaManager *meta.ProviderManager
|
||||
client *pluginsv0alpha1.PluginClient
|
||||
clientFactory func(context.Context) (*pluginsv0alpha1.PluginClient, error)
|
||||
clientErr error
|
||||
clientOnce sync.Once
|
||||
|
||||
gr schema.GroupResource
|
||||
namespacer claims.NamespaceFormatter
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
func NewPluginMetaStorage(
|
||||
metaManager *meta.ProviderManager,
|
||||
clientFactory func(context.Context) (*pluginsv0alpha1.PluginClient, error),
|
||||
namespacer claims.NamespaceFormatter,
|
||||
) *PluginMetaStorage {
|
||||
gr := schema.GroupResource{
|
||||
Group: pluginsv0alpha1.APIGroup,
|
||||
Resource: strings.ToLower(pluginsv0alpha1.PluginMetaKind().Plural()),
|
||||
}
|
||||
|
||||
return &PluginMetaStorage{
|
||||
metaManager: metaManager,
|
||||
clientFactory: clientFactory,
|
||||
gr: gr,
|
||||
namespacer: namespacer,
|
||||
tableConverter: rest.NewDefaultTableConvertor(gr),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PluginMetaStorage) getClient(ctx context.Context) (*pluginsv0alpha1.PluginClient, error) {
|
||||
s.clientOnce.Do(func() {
|
||||
client, err := s.clientFactory(ctx)
|
||||
if err != nil {
|
||||
s.clientErr = err
|
||||
s.client = nil
|
||||
return
|
||||
}
|
||||
s.client = client
|
||||
})
|
||||
|
||||
return s.client, s.clientErr
|
||||
}
|
||||
|
||||
func (s *PluginMetaStorage) New() runtime.Object {
|
||||
return pluginsv0alpha1.PluginMetaKind().ZeroValue()
|
||||
}
|
||||
|
||||
func (s *PluginMetaStorage) Destroy() {}
|
||||
|
||||
func (s *PluginMetaStorage) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *PluginMetaStorage) GetSingularName() string {
|
||||
return strings.ToLower(pluginsv0alpha1.PluginMetaKind().Kind())
|
||||
}
|
||||
|
||||
func (s *PluginMetaStorage) NewList() runtime.Object {
|
||||
return pluginsv0alpha1.PluginMetaKind().ZeroListValue()
|
||||
}
|
||||
|
||||
func (s *PluginMetaStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginClient, err := s.getClient(ctx)
|
||||
if err != nil {
|
||||
return nil, apierrors.NewInternalError(fmt.Errorf("failed to get plugin client: %w", err))
|
||||
}
|
||||
|
||||
plugins, err := pluginClient.ListAll(ctx, ns.Value, resource.ListOptions{})
|
||||
if err != nil {
|
||||
logging.DefaultLogger.Error("Failed to list plugins", "namespace", ns.Value, "error", err)
|
||||
return nil, apierrors.NewInternalError(fmt.Errorf("failed to list plugins: %w", err))
|
||||
}
|
||||
|
||||
// Convert each Plugin to PluginMeta
|
||||
metaItems := make([]pluginsv0alpha1.PluginMeta, 0, len(plugins.Items))
|
||||
for _, plugin := range plugins.Items {
|
||||
result, err := s.metaManager.GetMeta(ctx, plugin.Spec.Id, plugin.Spec.Version)
|
||||
if err != nil {
|
||||
// Log error but continue with other plugins
|
||||
logging.DefaultLogger.Warn("Failed to fetch metadata for plugin", "pluginId", plugin.Spec.Id, "version", plugin.Spec.Version, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
pluginMeta := createPluginMetaFromPluginMetaJSONData(result.Meta, plugin.Name, plugin.Namespace)
|
||||
metaItems = append(metaItems, *pluginMeta)
|
||||
}
|
||||
|
||||
list := &pluginsv0alpha1.PluginMetaList{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: pluginsv0alpha1.APIGroup + "/" + pluginsv0alpha1.APIVersion,
|
||||
Kind: pluginsv0alpha1.PluginMetaKind().Kind() + "List",
|
||||
},
|
||||
Items: metaItems,
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *PluginMetaStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginClient, err := s.getClient(ctx)
|
||||
if err != nil {
|
||||
return nil, apierrors.NewInternalError(fmt.Errorf("failed to get plugin client: %w", err))
|
||||
}
|
||||
|
||||
plugin, err := pluginClient.Get(ctx, resource.Identifier{
|
||||
Namespace: ns.Value,
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := s.metaManager.GetMeta(ctx, plugin.Spec.Id, plugin.Spec.Version)
|
||||
if err != nil {
|
||||
if errors.Is(err, meta.ErrMetaNotFound) {
|
||||
gr := schema.GroupResource{
|
||||
Group: pluginsv0alpha1.APIGroup,
|
||||
Resource: name,
|
||||
}
|
||||
return nil, apierrors.NewNotFound(gr, plugin.Spec.Id)
|
||||
}
|
||||
|
||||
logging.DefaultLogger.Error("Failed to fetch plugin metadata", "pluginId", plugin.Spec.Id, "version", plugin.Spec.Version, "error", err)
|
||||
return nil, apierrors.NewInternalError(fmt.Errorf("failed to fetch plugin metadata: %w", err))
|
||||
}
|
||||
|
||||
return createPluginMetaFromPluginMetaJSONData(result.Meta, name, ns.Value), nil
|
||||
}
|
||||
|
||||
// createPluginMetaFromPluginMetaJSONData creates a PluginMeta k8s object from PluginMetaJSONData and plugin metadata.
|
||||
func createPluginMetaFromPluginMetaJSONData(pluginJSON pluginsv0alpha1.PluginMetaJSONData, name, namespace string) *pluginsv0alpha1.PluginMeta {
|
||||
pluginMeta := &pluginsv0alpha1.PluginMeta{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJSON: pluginJSON,
|
||||
},
|
||||
}
|
||||
|
||||
// Set the GroupVersionKind
|
||||
pluginMeta.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: pluginsv0alpha1.APIGroup,
|
||||
Version: pluginsv0alpha1.APIVersion,
|
||||
Kind: pluginsv0alpha1.PluginMetaKind().Kind(),
|
||||
})
|
||||
|
||||
return pluginMeta
|
||||
}
|
||||
Reference in New Issue
Block a user