Plugins: API sync (#112452)
This commit is contained in:
1 parent
8b12bbcc55
commit
dc77da11cf
25 files changed
+1214
-47
No files matched your search
@@ -5,9 +5,11 @@ pluginInstallV0Alpha1: {
|
||||
plural: "plugininstalls"
|
||||
scope: "Namespaced"
|
||||
schema: {
|
||||
spec: {
|
||||
id: string
|
||||
version: string
|
||||
}
|
||||
spec: {
|
||||
id: string
|
||||
version: string
|
||||
url?: string
|
||||
class: "core" | "external" | "cdn"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,22 @@ package v0alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginInstallSpec struct {
|
||||
Id string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Id string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
Class PluginInstallSpecClass `json:"class"`
|
||||
}
|
||||
|
||||
// NewPluginInstallSpec creates a new PluginInstallSpec object.
|
||||
func NewPluginInstallSpec() *PluginInstallSpec {
|
||||
return &PluginInstallSpec{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginInstallSpecClass string
|
||||
|
||||
const (
|
||||
PluginInstallSpecClassCore PluginInstallSpecClass = "core"
|
||||
PluginInstallSpecClassExternal PluginInstallSpecClass = "external"
|
||||
PluginInstallSpecClassCdn PluginInstallSpecClass = "cdn"
|
||||
)
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
rawSchemaPluginMetav0alpha1 = []byte(`{"Dependencies":{"additionalProperties":false,"properties":{"extensions":{"additionalProperties":false,"properties":{"exposedComponents":{"description":"+listType=set","items":{"type":"string"},"type":"array"}},"type":"object"},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"enum":["app","datasource","panel"],"type":"string"}},"required":["id","type","name"],"type":"object"},"type":"array"}},"required":["grafanaDependency"],"type":"object"},"EnterpriseFeatures":{"additionalProperties":false,"properties":{"healthDiagnosticsErrors":{"default":false,"description":"Allow additional properties","type":"boolean"}},"type":"object"},"Extensions":{"additionalProperties":false,"properties":{"addedComponents":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedLinks":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"}},"type":"object"},"IAM":{"additionalProperties":false,"properties":{"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"Include":{"additionalProperties":false,"properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"enum":["Admin","Editor","Viewer"],"type":"string"},"type":{"enum":["dashboard","page","panel","datasource"],"type":"string"},"uid":{"type":"string"}},"type":"object"},"Info":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"description":"Optional fields","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","items":{"type":"string"},"type":"array"},"links":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"},"logos":{"additionalProperties":false,"properties":{"large":{"type":"string"},"small":{"type":"string"}},"required":["small","large"],"type":"object"},"screenshots":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"path":{"type":"string"}},"type":"object"},"type":"array"},"updated":{"format":"date-time","type":"string"},"version":{"type":"string"}},"required":["keywords","logos","updated","version"],"type":"object"},"JSONData":{"additionalProperties":false,"description":"JSON configuration schema for Grafana plugins\nConverted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json","properties":{"alerting":{"description":"Optional fields","type":"boolean"},"annotations":{"type":"boolean"},"autoEnabled":{"type":"boolean"},"backend":{"type":"boolean"},"buildMode":{"type":"string"},"builtIn":{"type":"boolean"},"category":{"enum":["tsdb","logging","cloud","tracing","profiling","sql","enterprise","iot","other"],"type":"string"},"dependencies":{"$ref":"#/components/schemas/Dependencies","description":"Dependency information"},"enterpriseFeatures":{"$ref":"#/components/schemas/EnterpriseFeatures"},"executable":{"type":"string"},"extensions":{"$ref":"#/components/schemas/Extensions"},"hideFromList":{"type":"boolean"},"iam":{"$ref":"#/components/schemas/IAM"},"id":{"description":"Unique name of the plugin","type":"string"},"includes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Include"},"type":"array"},"info":{"$ref":"#/components/schemas/Info","description":"Metadata for the plugin"},"logs":{"type":"boLine truncated
|
||||
versionSchemaPluginMetav0alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaPluginMetav0alpha1, &versionSchemaPluginMetav0alpha1)
|
||||
rawSchemaPluginInstallv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"PluginInstall":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"id":{"type":"string"},"version":{"type":"string"}},"required":["id","version"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`)
|
||||
rawSchemaPluginInstallv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"PluginInstall":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external","cdn"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`)
|
||||
versionSchemaPluginInstallv0alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaPluginInstallv0alpha1, &versionSchemaPluginInstallv0alpha1)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
errorsK8s "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
PluginInstallSourceAnnotation = "plugins.grafana.app/install-source"
|
||||
)
|
||||
|
||||
// Class represents the plugin class type in an unversioned internal format.
|
||||
// This intentionally duplicates the versioned API type (PluginInstallSpecClass) to decouple
|
||||
// internal code from API version changes, making it easier to support multiple API versions.
|
||||
type Class = string
|
||||
|
||||
const (
|
||||
ClassCore Class = "core"
|
||||
ClassExternal Class = "external"
|
||||
ClassCDN Class = "cdn"
|
||||
)
|
||||
|
||||
type Source = string
|
||||
|
||||
const (
|
||||
SourceUnknown Source = "unknown"
|
||||
SourcePluginStore Source = "plugin-store"
|
||||
)
|
||||
|
||||
type PluginInstall struct {
|
||||
ID string
|
||||
Version string
|
||||
URL string
|
||||
Class Class
|
||||
Source Source
|
||||
}
|
||||
|
||||
func (p *PluginInstall) ToPluginInstallV0Alpha1(namespace string) *pluginsv0alpha1.PluginInstall {
|
||||
var url *string = nil
|
||||
if p.URL != "" {
|
||||
url = &p.URL
|
||||
}
|
||||
return &pluginsv0alpha1.PluginInstall{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: p.ID,
|
||||
Annotations: map[string]string{
|
||||
PluginInstallSourceAnnotation: p.Source,
|
||||
},
|
||||
},
|
||||
Spec: pluginsv0alpha1.PluginInstallSpec{
|
||||
Id: p.ID,
|
||||
Version: p.Version,
|
||||
Url: url,
|
||||
Class: pluginsv0alpha1.PluginInstallSpecClass(p.Class),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginInstall) ShouldUpdate(existing *pluginsv0alpha1.PluginInstall) bool {
|
||||
update := p.ToPluginInstallV0Alpha1(existing.Namespace)
|
||||
if source, ok := existing.Annotations[PluginInstallSourceAnnotation]; ok && source != p.Source {
|
||||
return true
|
||||
}
|
||||
if existing.Spec.Version != update.Spec.Version {
|
||||
return true
|
||||
}
|
||||
if existing.Spec.Class != update.Spec.Class {
|
||||
return true // this should never really happen
|
||||
}
|
||||
if !equalStringPointers(existing.Spec.Url, update.Spec.Url) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func equalStringPointers(a, b *string) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
type InstallRegistrar struct {
|
||||
clientGenerator resource.ClientGenerator
|
||||
client *pluginsv0alpha1.PluginInstallClient
|
||||
clientOnce sync.Once
|
||||
}
|
||||
|
||||
func NewInstallRegistrar(clientGenerator resource.ClientGenerator) *InstallRegistrar {
|
||||
return &InstallRegistrar{
|
||||
clientGenerator: clientGenerator,
|
||||
clientOnce: sync.Once{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InstallRegistrar) GetClient() (*pluginsv0alpha1.PluginInstallClient, error) {
|
||||
r.clientOnce.Do(func() {
|
||||
client, err := pluginsv0alpha1.NewPluginInstallClientFromGenerator(r.clientGenerator)
|
||||
if err != nil {
|
||||
r.client = nil
|
||||
return
|
||||
}
|
||||
r.client = client
|
||||
})
|
||||
|
||||
return r.client, nil
|
||||
}
|
||||
|
||||
// Register creates or updates a plugin install in the registry.
|
||||
func (r *InstallRegistrar) Register(ctx context.Context, namespace string, install *PluginInstall) error {
|
||||
client, err := r.GetClient()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
identifier := resource.Identifier{
|
||||
Namespace: namespace,
|
||||
Name: install.ID,
|
||||
}
|
||||
|
||||
existing, err := client.Get(ctx, identifier)
|
||||
if err != nil && !errorsK8s.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing != nil && install.ShouldUpdate(existing) {
|
||||
_, err = client.Update(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.UpdateOptions{ResourceVersion: existing.ResourceVersion})
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Create(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.CreateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Unregister removes a plugin install from the registry.
|
||||
func (r *InstallRegistrar) Unregister(ctx context.Context, namespace string, name string, source Source) error {
|
||||
client, err := r.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
identifier := resource.Identifier{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
}
|
||||
existing, err := client.Get(ctx, identifier)
|
||||
if err != nil && !errorsK8s.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
// if the source is different, do not unregister
|
||||
if existingSource, ok := existing.Annotations[PluginInstallSourceAnnotation]; ok && existingSource != source {
|
||||
return nil
|
||||
}
|
||||
return client.Delete(ctx, identifier, resource.DeleteOptions{})
|
||||
}
|
||||
Reference in new issue
Block a user