Files
grafana/apps/plugins/pkg/app/install/registrar.go
T
2025-11-04 10:47:58 -05:00

173 lines
4.3 KiB
Go

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.Plugin {
var url *string = nil
if p.URL != "" {
url = &p.URL
}
return &pluginsv0alpha1.Plugin{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: p.ID,
Annotations: map[string]string{
PluginInstallSourceAnnotation: p.Source,
},
},
Spec: pluginsv0alpha1.PluginSpec{
Id: p.ID,
Version: p.Version,
Url: url,
Class: pluginsv0alpha1.PluginSpecClass(p.Class),
},
}
}
func (p *PluginInstall) ShouldUpdate(existing *pluginsv0alpha1.Plugin) 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.PluginClient
clientErr error
clientOnce sync.Once
}
func NewInstallRegistrar(clientGenerator resource.ClientGenerator) *InstallRegistrar {
return &InstallRegistrar{
clientGenerator: clientGenerator,
clientOnce: sync.Once{},
}
}
func (r *InstallRegistrar) GetClient() (*pluginsv0alpha1.PluginClient, error) {
r.clientOnce.Do(func() {
client, err := pluginsv0alpha1.NewPluginClientFromGenerator(r.clientGenerator)
if err != nil {
r.clientErr = err
r.client = nil
return
}
r.client = client
})
return r.client, r.clientErr
}
// 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 err
}
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 {
if install.ShouldUpdate(existing) {
_, err = client.Update(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.UpdateOptions{ResourceVersion: existing.ResourceVersion})
return err
}
return nil
}
_, 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 plugin doesn't exist, nothing to unregister
if existing == nil {
return nil
}
// 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{})
}