Chore: Annotation store interface (#114100)
* annotation legacy store with api server, read only * annotations are not addressable by ID for read operations * add ownership for an app * typo, of course * fix go workspace * update workspace * copy annotation app in dockerfile * experimenting with store interface * finalising interfaces * add tags as custom handler * implement tags handler * add missing config file * mute linter * update generated files * update workspace
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
)
|
||||
|
||||
type memoryStore struct {
|
||||
mu sync.RWMutex
|
||||
data map[string]*annotationV0.Annotation
|
||||
}
|
||||
|
||||
func NewMemoryStore() Store {
|
||||
return &memoryStore{
|
||||
data: make(map[string]*annotationV0.Annotation),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memoryStore) Get(ctx context.Context, namespace, name string) (*annotationV0.Annotation, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
key := namespace + "/" + name
|
||||
anno, ok := m.data[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
return anno.DeepCopy(), nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) List(ctx context.Context, namespace string, opts ListOptions) (*AnnotationList, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
//nolint:prealloc
|
||||
var result []annotationV0.Annotation // no, we can't pre-alloc it, we don't know the size yet
|
||||
|
||||
for _, anno := range m.data {
|
||||
if anno.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.DashboardUID != "" && (anno.Spec.DashboardUID == nil || *anno.Spec.DashboardUID != opts.DashboardUID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.PanelID != 0 && (anno.Spec.PanelID == nil || *anno.Spec.PanelID != opts.PanelID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.From > 0 && anno.Spec.Time < opts.From {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.To > 0 && anno.Spec.Time > opts.To {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, *anno.DeepCopy())
|
||||
|
||||
if opts.Limit > 0 && int64(len(result)) >= opts.Limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &AnnotationList{Items: result}, nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) Create(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if anno.Name == "" {
|
||||
anno.Name = uuid.New().String()
|
||||
}
|
||||
|
||||
key := anno.Namespace + "/" + anno.Name
|
||||
|
||||
if _, exists := m.data[key]; exists {
|
||||
return nil, fmt.Errorf("annotation already exists")
|
||||
}
|
||||
|
||||
created := anno.DeepCopy()
|
||||
if created.CreationTimestamp.IsZero() {
|
||||
created.CreationTimestamp = metav1.Now()
|
||||
}
|
||||
|
||||
m.data[key] = created
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) Update(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := anno.Namespace + "/" + anno.Name
|
||||
|
||||
if _, exists := m.data[key]; !exists {
|
||||
return nil, fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
updated := anno.DeepCopy()
|
||||
m.data[key] = updated
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) Delete(ctx context.Context, namespace, name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := namespace + "/" + name
|
||||
|
||||
if _, exists := m.data[key]; !exists {
|
||||
return fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
delete(m.data, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) ListTags(ctx context.Context, namespace string, opts TagListOptions) ([]Tag, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
tagCounts := make(map[string]int64)
|
||||
|
||||
for _, anno := range m.data {
|
||||
if anno.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
for _, tag := range anno.Spec.Tags {
|
||||
if opts.Prefix == "" || strings.HasPrefix(tag, opts.Prefix) {
|
||||
tagCounts[tag]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags := make([]Tag, 0, len(tagCounts))
|
||||
for name, count := range tagCounts {
|
||||
tags = append(tags, Tag{Name: name, Count: count})
|
||||
}
|
||||
|
||||
if opts.Limit > 0 && len(tags) > opts.Limit {
|
||||
tags = tags[:opts.Limit]
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
@@ -21,12 +22,11 @@ import (
|
||||
"github.com/grafana/grafana/apps/annotation/pkg/apis"
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
annotationapp "github.com/grafana/grafana/apps/annotation/pkg/app"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
apiserverrest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
grafrequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@@ -46,15 +46,32 @@ func RegisterAppInstaller(
|
||||
cfg *setting.Cfg,
|
||||
features featuremgmt.FeatureToggles,
|
||||
service annotations.Repository,
|
||||
cleaner annotations.Cleaner,
|
||||
) (*AnnotationAppInstaller, error) {
|
||||
installer := &AnnotationAppInstaller{
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
var tagHandler func(context.Context, app.CustomRouteResponseWriter, *app.CustomRouteRequest) error
|
||||
if service != nil {
|
||||
mapper := grafrequest.GetNamespaceMapper(cfg)
|
||||
sqlAdapter := NewSQLAdapter(service, cleaner, mapper, cfg)
|
||||
installer.legacy = &legacyStorage{
|
||||
store: sqlAdapter,
|
||||
mapper: mapper,
|
||||
}
|
||||
// Create the tags handler using the sqlAdapter as TagProvider
|
||||
tagHandler = newTagsHandler(sqlAdapter)
|
||||
}
|
||||
|
||||
provider := simple.NewAppProvider(apis.LocalManifest(), nil, annotationapp.New)
|
||||
|
||||
appConfig := app.Config{
|
||||
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
||||
KubeConfig: restclient.Config{},
|
||||
ManifestData: *apis.LocalManifest().ManifestData,
|
||||
SpecificConfig: &annotationapp.AnnotationConfig{
|
||||
TagHandler: tagHandler,
|
||||
},
|
||||
}
|
||||
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator())
|
||||
if err != nil {
|
||||
@@ -62,13 +79,6 @@ func RegisterAppInstaller(
|
||||
}
|
||||
installer.AppInstaller = i
|
||||
|
||||
if service != nil {
|
||||
installer.legacy = &legacyStorage{
|
||||
service: service,
|
||||
namespacer: request.GetNamespaceMapper(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
return installer, nil
|
||||
}
|
||||
|
||||
@@ -79,9 +89,11 @@ func (a *AnnotationAppInstaller) GetLegacyStorage(requested schema.GroupVersionR
|
||||
Version: kind.Version(),
|
||||
Resource: kind.Plural(),
|
||||
}
|
||||
|
||||
if requested.String() != gvr.String() {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.legacy.tableConverter = utils.NewTableConverter(
|
||||
gvr.GroupResource(),
|
||||
utils.TableColumns{
|
||||
@@ -114,8 +126,8 @@ var (
|
||||
)
|
||||
|
||||
type legacyStorage struct {
|
||||
service annotations.Repository
|
||||
namespacer request.NamespaceMapper
|
||||
store Store
|
||||
mapper grafrequest.NamespaceMapper
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
@@ -142,21 +154,15 @@ func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Objec
|
||||
}
|
||||
|
||||
func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
orgID, err := request.OrgIDForList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := &annotations.ItemQuery{OrgID: orgID, SignedInUser: user, AlertID: -1}
|
||||
namespace := request.NamespaceValue(ctx)
|
||||
|
||||
opts := ListOptions{}
|
||||
if options.FieldSelector != nil {
|
||||
for _, r := range options.FieldSelector.Requirements() {
|
||||
switch r.Field {
|
||||
case "spec.dashboardUID":
|
||||
if r.Operator == selection.Equals || r.Operator == selection.DoubleEquals {
|
||||
query.DashboardUID = r.Value
|
||||
opts.DashboardUID = r.Value
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.dashboardUID (only = supported)", r.Operator)
|
||||
}
|
||||
@@ -167,7 +173,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid panelID value %q: %w", r.Value, err)
|
||||
}
|
||||
query.PanelID = panelID
|
||||
opts.PanelID = panelID
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.panelID (only = supported)", r.Operator)
|
||||
}
|
||||
@@ -178,13 +184,13 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid time value %q: %w", r.Value, err)
|
||||
}
|
||||
query.From = from
|
||||
opts.From = from
|
||||
case selection.LessThan:
|
||||
to, err := strconv.ParseInt(r.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid time value %q: %w", r.Value, err)
|
||||
}
|
||||
query.To = to
|
||||
opts.To = to
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.time (only >, < supported for ranges)", r.Operator)
|
||||
}
|
||||
@@ -196,13 +202,13 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid timeEnd value %q: %w", r.Value, err)
|
||||
}
|
||||
query.From = from
|
||||
opts.From = from
|
||||
case selection.LessThan:
|
||||
to, err := strconv.ParseInt(r.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid timeEnd value %q: %w", r.Value, err)
|
||||
}
|
||||
query.To = to
|
||||
opts.To = to
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.timeEnd (only >, < supported for ranges)", r.Operator)
|
||||
}
|
||||
@@ -213,31 +219,22 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
}
|
||||
}
|
||||
|
||||
query.Limit = 100
|
||||
opts.Limit = 100
|
||||
if options.Limit > 0 {
|
||||
query.Limit = options.Limit
|
||||
opts.Limit = options.Limit
|
||||
}
|
||||
items, err := s.service.Find(ctx, query)
|
||||
|
||||
result, err := s.store.List(ctx, namespace, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := &annotationV0.AnnotationList{
|
||||
Items: make([]annotationV0.Annotation, len(items)),
|
||||
}
|
||||
for i, item := range items {
|
||||
c, err := toK8sResource(orgID, item, s.namespacer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list.Items[i] = *c
|
||||
}
|
||||
|
||||
// TODO: pagination?
|
||||
return list, nil
|
||||
return &annotationV0.AnnotationList{Items: result.Items}, nil
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
return nil, errors.New("fetching single annotations not supported by legacy storage")
|
||||
namespace := request.NamespaceValue(ctx)
|
||||
return s.store.Get(ctx, namespace, name)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Create(ctx context.Context,
|
||||
@@ -245,22 +242,11 @@ func (s *legacyStorage) Create(ctx context.Context,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
options *metav1.CreateOptions,
|
||||
) (runtime.Object, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
// resource, ok := obj.(*correlationsV0.Correlation)
|
||||
// if !ok {
|
||||
// return nil, fmt.Errorf("expected correlation")
|
||||
// }
|
||||
//
|
||||
// cmd, err := correlations.ToCreateCorrelationCommand(resource)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// out, err := s.service.CreateCorrelation(ctx, *cmd)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return s.Get(ctx, out.UID, &metav1.GetOptions{})
|
||||
resource, ok := obj.(*annotationV0.Annotation)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected annotation")
|
||||
}
|
||||
return s.store.Create(ctx, resource)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Update(ctx context.Context,
|
||||
@@ -272,73 +258,14 @@ func (s *legacyStorage) Update(ctx context.Context,
|
||||
options *metav1.UpdateOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
return nil, false, errors.New("not implemented")
|
||||
// before, err := s.Get(ctx, name, &metav1.GetOptions{})
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
// obj, err := objInfo.UpdatedObject(ctx, before)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
//
|
||||
// resource, ok := obj.(*correlationsV0.Correlation)
|
||||
// if !ok {
|
||||
// return nil, false, fmt.Errorf("expected correlation")
|
||||
// }
|
||||
//
|
||||
// cmd, err := correlations.ToUpdateCorrelationCommand(resource)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
//
|
||||
// out, err := s.service.UpdateCorrelation(ctx, *cmd)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
// obj, err = s.Get(ctx, out.UID, &metav1.GetOptions{})
|
||||
// return obj, false, err
|
||||
}
|
||||
|
||||
// GracefulDeleter
|
||||
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
return nil, false, errors.New("not implemented")
|
||||
// orgID, err := request.OrgIDForList(ctx)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
// err = s.service.DeleteCorrelation(ctx, correlations.DeleteCorrelationCommand{
|
||||
// OrgId: orgID,
|
||||
// UID: name,
|
||||
// })
|
||||
// return nil, (err == nil), err
|
||||
namespace := request.NamespaceValue(ctx)
|
||||
err := s.store.Delete(ctx, namespace, name)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// CollectionDeleter
|
||||
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
return nil, fmt.Errorf("DeleteCollection for annotation not implemented")
|
||||
}
|
||||
|
||||
func toK8sResource(orgID int64, item *annotations.ItemDTO, namespacer request.NamespaceMapper) (*annotationV0.Annotation, error) {
|
||||
annotation := &annotationV0.Annotation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("a-%d", item.ID), // FIXME
|
||||
Namespace: namespacer(orgID),
|
||||
},
|
||||
Spec: annotationV0.AnnotationSpec{
|
||||
Text: item.Text,
|
||||
Time: item.Time,
|
||||
Tags: item.Tags,
|
||||
},
|
||||
}
|
||||
|
||||
if item.DashboardUID != nil && *item.DashboardUID != "" {
|
||||
annotation.Spec.DashboardUID = item.DashboardUID
|
||||
}
|
||||
if item.PanelID != 0 {
|
||||
annotation.Spec.PanelID = &item.PanelID
|
||||
}
|
||||
if item.TimeEnd != 0 {
|
||||
annotation.Spec.TimeEnd = &item.TimeEnd
|
||||
}
|
||||
return annotation, nil
|
||||
return nil, fmt.Errorf("DeleteCollection for annotation is not available")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type sqlAdapter struct {
|
||||
repo annotations.Repository
|
||||
cleaner annotations.Cleaner
|
||||
nsMapper request.NamespaceMapper
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func NewSQLAdapter(repo annotations.Repository, cleaner annotations.Cleaner, nsMapper request.NamespaceMapper, cfg *setting.Cfg) *sqlAdapter {
|
||||
return &sqlAdapter{
|
||||
repo: repo,
|
||||
cleaner: cleaner,
|
||||
nsMapper: nsMapper,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Get(ctx context.Context, namespace, name string) (*annotationV0.Annotation, error) {
|
||||
id, err := parseAnnotationID(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &annotations.ItemQuery{
|
||||
SignedInUser: user,
|
||||
OrgID: orgID,
|
||||
Limit: 1000,
|
||||
AlertID: -1,
|
||||
}
|
||||
|
||||
items, err := a.repo.Find(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if item.ID == id {
|
||||
return a.toK8sResource(item, namespace), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) List(ctx context.Context, namespace string, opts ListOptions) (*AnnotationList, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &annotations.ItemQuery{
|
||||
SignedInUser: user,
|
||||
OrgID: orgID,
|
||||
DashboardUID: opts.DashboardUID,
|
||||
PanelID: opts.PanelID,
|
||||
From: opts.From,
|
||||
To: opts.To,
|
||||
Limit: opts.Limit,
|
||||
AlertID: -1,
|
||||
}
|
||||
|
||||
items, err := a.repo.Find(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]annotationV0.Annotation, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, *a.toK8sResource(item, namespace))
|
||||
}
|
||||
|
||||
return &AnnotationList{
|
||||
Items: result,
|
||||
Continue: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Create(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, anno.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := a.fromK8sResource(anno)
|
||||
item.OrgID = orgID
|
||||
|
||||
if err := a.repo.Save(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
created := anno.DeepCopy()
|
||||
created.Name = fmt.Sprintf("a-%d", item.ID)
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Update(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, anno.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := a.fromK8sResource(anno)
|
||||
item.OrgID = orgID
|
||||
|
||||
if err := a.repo.Update(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return anno, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Delete(ctx context.Context, namespace, name string) error {
|
||||
id, err := parseAnnotationID(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.repo.Delete(ctx, &annotations.DeleteParams{
|
||||
ID: id,
|
||||
OrgID: orgID,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Cleanup(ctx context.Context) (int64, error) {
|
||||
if a.cleaner == nil {
|
||||
return 0, nil
|
||||
}
|
||||
deleted, _, err := a.cleaner.Run(ctx, a.cfg)
|
||||
return deleted, err
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) ListTags(ctx context.Context, namespace string, opts TagListOptions) ([]Tag, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &annotations.TagsQuery{
|
||||
OrgID: orgID,
|
||||
Limit: int64(opts.Limit),
|
||||
Tag: opts.Prefix,
|
||||
}
|
||||
|
||||
result, err := a.repo.FindTags(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags := make([]Tag, len(result.Tags))
|
||||
for i, t := range result.Tags {
|
||||
tags[i] = Tag{Name: t.Tag, Count: t.Count}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) toK8sResource(item *annotations.ItemDTO, namespace string) *annotationV0.Annotation {
|
||||
anno := &annotationV0.Annotation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("a-%d", item.ID),
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: annotationV0.AnnotationSpec{
|
||||
Text: item.Text,
|
||||
Time: item.Time,
|
||||
Tags: item.Tags,
|
||||
},
|
||||
}
|
||||
|
||||
if item.DashboardUID != nil && *item.DashboardUID != "" {
|
||||
anno.Spec.DashboardUID = item.DashboardUID
|
||||
}
|
||||
if item.PanelID != 0 {
|
||||
anno.Spec.PanelID = &item.PanelID
|
||||
}
|
||||
if item.TimeEnd != 0 {
|
||||
anno.Spec.TimeEnd = &item.TimeEnd
|
||||
}
|
||||
|
||||
return anno
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) fromK8sResource(anno *annotationV0.Annotation) *annotations.Item {
|
||||
item := &annotations.Item{
|
||||
Text: anno.Spec.Text,
|
||||
Epoch: anno.Spec.Time,
|
||||
Tags: anno.Spec.Tags,
|
||||
}
|
||||
|
||||
if anno.Name != "" {
|
||||
if id, err := parseAnnotationID(anno.Name); err == nil {
|
||||
item.ID = id
|
||||
}
|
||||
}
|
||||
|
||||
if anno.Spec.DashboardUID != nil {
|
||||
item.DashboardUID = *anno.Spec.DashboardUID
|
||||
}
|
||||
if anno.Spec.PanelID != nil {
|
||||
item.PanelID = *anno.Spec.PanelID
|
||||
}
|
||||
if anno.Spec.TimeEnd != nil {
|
||||
item.EpochEnd = *anno.Spec.TimeEnd
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
func parseAnnotationID(name string) (int64, error) {
|
||||
if len(name) < 3 || name[:2] != "a-" {
|
||||
return 0, fmt.Errorf("invalid annotation name format: %s", name)
|
||||
}
|
||||
return strconv.ParseInt(name[2:], 10, 64)
|
||||
}
|
||||
|
||||
func namespaceToOrgID(ctx context.Context, namespace string) (int64, error) {
|
||||
info, err := claims.ParseNamespace(namespace)
|
||||
return info.OrgID, err
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Get(ctx context.Context, namespace, name string) (*annotationV0.Annotation, error)
|
||||
List(ctx context.Context, namespace string, opts ListOptions) (*AnnotationList, error)
|
||||
Create(ctx context.Context, annotation *annotationV0.Annotation) (*annotationV0.Annotation, error)
|
||||
Update(ctx context.Context, annotation *annotationV0.Annotation) (*annotationV0.Annotation, error)
|
||||
Delete(ctx context.Context, namespace, name string) error
|
||||
}
|
||||
|
||||
type ListOptions struct {
|
||||
DashboardUID string
|
||||
PanelID int64
|
||||
From int64
|
||||
To int64
|
||||
Limit int64
|
||||
Continue string
|
||||
}
|
||||
|
||||
type AnnotationList struct {
|
||||
Items []annotationV0.Annotation
|
||||
Continue string
|
||||
}
|
||||
|
||||
type LifecycleManager interface {
|
||||
Cleanup(ctx context.Context) (int64, error)
|
||||
}
|
||||
|
||||
type TagProvider interface {
|
||||
ListTags(ctx context.Context, namespace string, opts TagListOptions) ([]Tag, error)
|
||||
}
|
||||
|
||||
type TagListOptions struct {
|
||||
Prefix string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Name string
|
||||
Count int64
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
)
|
||||
|
||||
type tagResponse struct {
|
||||
Tags []tagItem `json:"tags"`
|
||||
}
|
||||
|
||||
type tagItem struct {
|
||||
Tag string `json:"tag"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func newTagsHandler(tagProvider TagProvider) func(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error {
|
||||
return func(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error {
|
||||
fmt.Println("Handling /tags request")
|
||||
namespace := request.ResourceIdentifier.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
tags, err := tagProvider.ListTags(ctx, namespace, TagListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]tagItem, len(tags))
|
||||
for i, tag := range tags {
|
||||
items[i] = tagItem{
|
||||
Tag: tag.Name,
|
||||
Count: tag.Count,
|
||||
}
|
||||
}
|
||||
|
||||
response := tagResponse{
|
||||
Tags: items,
|
||||
}
|
||||
|
||||
return json.NewEncoder(writer).Encode(response)
|
||||
}
|
||||
}
|
||||
Generated
+2
-2
@@ -813,7 +813,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl)
|
||||
annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl, cleanupServiceImpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1467,7 +1467,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl)
|
||||
annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl, cleanupServiceImpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user