Files
grafana/pkg/registry/apps/annotation/sql_adapter.go
Serge Zaitsev c15b1b6f10 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
2025-12-01 12:28:46 +01:00

256 lines
5.6 KiB
Go

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
}