diff --git a/pkg/codegen/tmpl/core_resource.tmpl b/pkg/codegen/tmpl/core_resource.tmpl index 01207539348..8c4f6cb5839 100644 --- a/pkg/codegen/tmpl/core_resource.tmpl +++ b/pkg/codegen/tmpl/core_resource.tmpl @@ -1,6 +1,8 @@ package {{ .PackageName }} import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/kinds" ) @@ -10,10 +12,12 @@ type K8sResource = kinds.GrafanaResource[Spec, Status] // NewResource creates a new instance of the resource with a given name (UID) func NewK8sResource(name string, s *Spec) K8sResource { return K8sResource{ - Kind: "{{ .KindName }}", - APIVersion: "v{{ .Version }}-alpha", - Metadata: kinds.GrafanaResourceMetadata{ - Name: name, + TypeMeta: v1.TypeMeta{ + Kind: "{{ .KindName }}", + APIVersion: "v{{ .Version }}-alpha", + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, Annotations: make(map[string]string), Labels: make(map[string]string), }, diff --git a/pkg/kinds/accesspolicy/accesspolicy_gen.go b/pkg/kinds/accesspolicy/accesspolicy_gen.go index 2c843382788..90e97fd9315 100644 --- a/pkg/kinds/accesspolicy/accesspolicy_gen.go +++ b/pkg/kinds/accesspolicy/accesspolicy_gen.go @@ -10,6 +10,8 @@ package accesspolicy import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/kinds" ) @@ -19,9 +21,11 @@ type K8sResource = kinds.GrafanaResource[Spec, Status] // NewResource creates a new instance of the resource with a given name (UID) func NewK8sResource(name string, s *Spec) K8sResource { return K8sResource{ - Kind: "AccessPolicy", - APIVersion: "v0-0-alpha", - Metadata: kinds.GrafanaResourceMetadata{ + TypeMeta: v1.TypeMeta{ + Kind: "AccessPolicy", + APIVersion: "v0-0-alpha", + }, + ObjectMeta: v1.ObjectMeta{ Name: name, Annotations: make(map[string]string), Labels: make(map[string]string), diff --git a/pkg/kinds/dashboard/dashboard_gen.go b/pkg/kinds/dashboard/dashboard_gen.go index 3022cd93658..183f56b8ff3 100644 --- a/pkg/kinds/dashboard/dashboard_gen.go +++ b/pkg/kinds/dashboard/dashboard_gen.go @@ -10,6 +10,8 @@ package dashboard import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/kinds" ) @@ -19,9 +21,11 @@ type K8sResource = kinds.GrafanaResource[Spec, Status] // NewResource creates a new instance of the resource with a given name (UID) func NewK8sResource(name string, s *Spec) K8sResource { return K8sResource{ - Kind: "Dashboard", - APIVersion: "v0-0-alpha", - Metadata: kinds.GrafanaResourceMetadata{ + TypeMeta: v1.TypeMeta{ + Kind: "Dashboard", + APIVersion: "v0-0-alpha", + }, + ObjectMeta: v1.ObjectMeta{ Name: name, Annotations: make(map[string]string), Labels: make(map[string]string), diff --git a/pkg/kinds/general.go b/pkg/kinds/general.go index bc437ce1fb8..61c092036b7 100644 --- a/pkg/kinds/general.go +++ b/pkg/kinds/general.go @@ -1,390 +1,18 @@ package kinds import ( - "fmt" - "time" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// ResourceOriginInfo is saved in annotations. This is used to identify where the resource came from -// This object can model the same data as our existing provisioning table or a more general git sync -type ResourceOriginInfo struct { - // Name of the origin/provisioning source - Name string `json:"name,omitempty"` - - // The path within the named origin above (external_id in the existing dashboard provisioing) - Path string `json:"path,omitempty"` - - // Verification/identification key (check_sum in existing dashboard provisioning) - Key string `json:"key,omitempty"` - - // Origin modification timestamp when the resource was saved - // This will be before the resource updated time - Timestamp *time.Time `json:"time,omitempty"` - - // Avoid extending - _ any `json:"-"` -} - -// GrafanaResourceMetadata is standard k8s object metadata with helper functions -type GrafanaResourceMetadata v1.ObjectMeta - // GrafanaResource is a generic kubernetes resource with a helper for the common grafana metadata // This is a temporary solution until this object (or similar) can be moved to the app-sdk or kindsys type GrafanaResource[Spec any, Status any] struct { - APIVersion string `json:"apiVersion"` - Kind string `json:"kind"` + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` - Metadata GrafanaResourceMetadata `json:"metadata"` - Spec *Spec `json:"spec,omitempty"` - Status *Status `json:"status,omitempty"` + Spec *Spec `json:"spec,omitempty"` + Status *Status `json:"status,omitempty"` // Avoid extending _ any `json:"-"` } - -// Annotation keys -const annoKeyCreatedBy = "grafana.app/createdBy" -const annoKeyUpdatedTimestamp = "grafana.app/updatedTimestamp" -const annoKeyUpdatedBy = "grafana.app/updatedBy" - -// The folder identifier -const annoKeyFolder = "grafana.app/folder" -const annoKeySlug = "grafana.app/slug" -const annoKeyTitle = "grafana.app/title" - -// Identify where values came from -const annoKeyOriginName = "grafana.app/originName" -const annoKeyOriginPath = "grafana.app/originPath" -const annoKeyOriginKey = "grafana.app/originKey" -const annoKeyOriginTimestamp = "grafana.app/originTimestamp" - -func (m *GrafanaResourceMetadata) set(key string, val string) { - if val == "" { - if m.Annotations != nil { - delete(m.Annotations, key) - } - return - } - if m.Annotations == nil { - m.Annotations = make(map[string]string) - } - m.Annotations[key] = val -} - -func (m *GrafanaResourceMetadata) get(key string) string { - if m.Annotations == nil { - return "" - } - return m.Annotations[key] -} - -func (m *GrafanaResourceMetadata) GetUpdatedTimestamp() (*time.Time, error) { - v, ok := m.Annotations[annoKeyUpdatedTimestamp] - if !ok || v == "" { - return nil, nil - } - t, err := time.Parse(time.RFC3339, v) - if err != nil { - return nil, fmt.Errorf("invalid updated timestamp: %s", err.Error()) - } - return &t, nil -} - -func (m *GrafanaResourceMetadata) SetUpdatedTimestampMillis(v int64) { - if v > 0 { - t := time.UnixMilli(v) - m.SetUpdatedTimestamp(&t) - } else { - m.SetUpdatedTimestamp(nil) - } -} - -func (m *GrafanaResourceMetadata) SetUpdatedTimestamp(v *time.Time) { - txt := "" - if v != nil { - txt = v.UTC().Format(time.RFC3339) - } - m.set(annoKeyUpdatedTimestamp, txt) -} - -func (m *GrafanaResourceMetadata) GetCreatedBy() string { - return m.Annotations[annoKeyCreatedBy] -} - -func (m *GrafanaResourceMetadata) SetCreatedBy(user string) { - m.set(annoKeyCreatedBy, user) -} - -func (m *GrafanaResourceMetadata) GetUpdatedBy() string { - return m.Annotations[annoKeyUpdatedBy] -} - -func (m *GrafanaResourceMetadata) SetUpdatedBy(user string) { - m.set(annoKeyUpdatedBy, user) -} - -func (m *GrafanaResourceMetadata) GetFolder() string { - return m.Annotations[annoKeyFolder] -} - -func (m *GrafanaResourceMetadata) SetFolder(uid string) { - m.set(annoKeyFolder, uid) -} - -func (m *GrafanaResourceMetadata) GetSlug() string { - return m.get(annoKeySlug) -} - -func (m *GrafanaResourceMetadata) SetSlug(v string) { - m.set(annoKeySlug, v) -} - -func (m *GrafanaResourceMetadata) GetTitle() string { - return m.get(annoKeyTitle) -} - -func (m *GrafanaResourceMetadata) SetTitle(v string) { - m.set(annoKeyTitle, v) -} - -func (m *GrafanaResourceMetadata) SetOriginInfo(info *ResourceOriginInfo) { - delete(m.Annotations, annoKeyOriginName) - delete(m.Annotations, annoKeyOriginPath) - delete(m.Annotations, annoKeyOriginKey) - delete(m.Annotations, annoKeyOriginTimestamp) - if info != nil && info.Name != "" { - m.set(annoKeyOriginName, info.Name) - m.set(annoKeyOriginKey, info.Key) - m.set(annoKeyOriginPath, info.Path) - if info.Timestamp != nil { - m.Annotations[annoKeyOriginTimestamp] = info.Timestamp.Format(time.RFC3339) - } - } -} - -// GetOriginInfo returns the origin info stored in k8s metadata annotations -func (m *GrafanaResourceMetadata) GetOriginInfo() (*ResourceOriginInfo, error) { - v, ok := m.Annotations[annoKeyOriginName] - if !ok { - return nil, nil - } - t, err := m.GetOriginTimestamp() - return &ResourceOriginInfo{ - Name: v, - Path: m.GetOriginPath(), - Key: m.GetOriginKey(), - Timestamp: t, - }, err -} - -func (m *GrafanaResourceMetadata) GetOriginName() string { - return m.Annotations[annoKeyOriginName] -} - -func (m *GrafanaResourceMetadata) GetOriginPath() string { - return m.Annotations[annoKeyOriginPath] -} - -func (m *GrafanaResourceMetadata) GetOriginKey() string { - return m.Annotations[annoKeyOriginKey] -} - -func (m *GrafanaResourceMetadata) GetOriginTimestamp() (*time.Time, error) { - v, ok := m.Annotations[annoKeyOriginTimestamp] - if !ok || v == "" { - return nil, nil - } - t, err := time.Parse(time.RFC3339, v) - if err != nil { - return nil, fmt.Errorf("invalid origin timestamp: %s", err.Error()) - } - return &t, nil -} - -// Accessor functions for k8s objects -type GrafanaResourceMetaAccessor interface { - GetUpdatedTimestamp() (*time.Time, error) - SetUpdatedTimestamp(v *time.Time) - GetCreatedBy() string - SetCreatedBy(user string) - GetUpdatedBy() string - SetUpdatedBy(user string) - GetFolder() string - SetFolder(uid string) - GetSlug() string - SetSlug(v string) - GetTitle() string - SetTitle(v string) - GetOriginInfo() (*ResourceOriginInfo, error) - SetOriginInfo(info *ResourceOriginInfo) - GetOriginName() string - GetOriginPath() string - GetOriginKey() string - GetOriginTimestamp() (*time.Time, error) -} - -var _ GrafanaResourceMetaAccessor = (*grafanaResourceMetaAccessor)(nil) -var _ GrafanaResourceMetaAccessor = (*GrafanaResourceMetadata)(nil) - -type grafanaResourceMetaAccessor struct { - obj v1.Object -} - -func MetaAccessor(obj v1.Object) GrafanaResourceMetaAccessor { - return &grafanaResourceMetaAccessor{obj} -} - -func (m *grafanaResourceMetaAccessor) set(key string, val string) { - anno := m.obj.GetAnnotations() - if val == "" { - if anno != nil { - delete(anno, key) - } - } else { - if anno == nil { - anno = make(map[string]string) - } - anno[key] = val - } - m.obj.SetAnnotations(anno) -} - -func (m *grafanaResourceMetaAccessor) get(key string) string { - return m.obj.GetAnnotations()[key] -} - -func (m *grafanaResourceMetaAccessor) GetUpdatedTimestamp() (*time.Time, error) { - v, ok := m.obj.GetAnnotations()[annoKeyUpdatedTimestamp] - if !ok || v == "" { - return nil, nil - } - t, err := time.Parse(time.RFC3339, v) - if err != nil { - return nil, fmt.Errorf("invalid updated timestamp: %s", err.Error()) - } - return &t, nil -} - -func (m *grafanaResourceMetaAccessor) SetUpdatedTimestampMillis(v int64) { - if v > 0 { - t := time.UnixMilli(v) - m.SetUpdatedTimestamp(&t) - } else { - m.SetUpdatedTimestamp(nil) - } -} - -func (m *grafanaResourceMetaAccessor) SetUpdatedTimestamp(v *time.Time) { - txt := "" - if v != nil { - txt = v.UTC().Format(time.RFC3339) - } - m.set(annoKeyUpdatedTimestamp, txt) -} - -func (m *grafanaResourceMetaAccessor) GetCreatedBy() string { - return m.get(annoKeyCreatedBy) -} - -func (m *grafanaResourceMetaAccessor) SetCreatedBy(user string) { - m.set(annoKeyCreatedBy, user) -} - -func (m *grafanaResourceMetaAccessor) GetUpdatedBy() string { - return m.get(annoKeyUpdatedBy) -} - -func (m *grafanaResourceMetaAccessor) SetUpdatedBy(user string) { - m.set(annoKeyUpdatedBy, user) -} - -func (m *grafanaResourceMetaAccessor) GetFolder() string { - return m.get(annoKeyFolder) -} - -func (m *grafanaResourceMetaAccessor) SetFolder(uid string) { - m.set(annoKeyFolder, uid) -} - -func (m *grafanaResourceMetaAccessor) GetSlug() string { - return m.get(annoKeySlug) -} - -func (m *grafanaResourceMetaAccessor) SetSlug(v string) { - m.set(annoKeySlug, v) -} -func (m *grafanaResourceMetaAccessor) GetTitle() string { - return m.get(annoKeyTitle) -} - -func (m *grafanaResourceMetaAccessor) SetTitle(v string) { - m.set(annoKeyTitle, v) -} -func (m *grafanaResourceMetaAccessor) SetOriginInfo(info *ResourceOriginInfo) { - anno := m.obj.GetAnnotations() - if anno == nil { - if info == nil { - return - } - anno = make(map[string]string, 0) - m.obj.SetAnnotations(anno) - } - - delete(anno, annoKeyOriginName) - delete(anno, annoKeyOriginPath) - delete(anno, annoKeyOriginKey) - delete(anno, annoKeyOriginTimestamp) - if info != nil && info.Name != "" { - anno[annoKeyOriginName] = info.Name - if info.Path != "" { - anno[annoKeyOriginPath] = info.Path - } - if info.Key != "" { - anno[annoKeyOriginKey] = info.Key - } - if info.Timestamp != nil { - anno[annoKeyOriginTimestamp] = info.Timestamp.Format(time.RFC3339) - } - } - m.obj.SetAnnotations(anno) -} - -func (m *grafanaResourceMetaAccessor) GetOriginInfo() (*ResourceOriginInfo, error) { - v, ok := m.obj.GetAnnotations()[annoKeyOriginName] - if !ok { - return nil, nil - } - t, err := m.GetOriginTimestamp() - return &ResourceOriginInfo{ - Name: v, - Path: m.GetOriginPath(), - Key: m.GetOriginKey(), - Timestamp: t, - }, err -} - -func (m *grafanaResourceMetaAccessor) GetOriginName() string { - return m.get(annoKeyOriginName) -} - -func (m *grafanaResourceMetaAccessor) GetOriginPath() string { - return m.get(annoKeyOriginPath) -} - -func (m *grafanaResourceMetaAccessor) GetOriginKey() string { - return m.get(annoKeyOriginKey) -} - -func (m *grafanaResourceMetaAccessor) GetOriginTimestamp() (*time.Time, error) { - v, ok := m.obj.GetAnnotations()[annoKeyOriginTimestamp] - if !ok || v == "" { - return nil, nil - } - t, err := time.Parse(time.RFC3339, v) - if err != nil { - return nil, fmt.Errorf("invalid origin timestamp: %s", err.Error()) - } - return &t, nil -} diff --git a/pkg/kinds/general_test.go b/pkg/kinds/general_test.go deleted file mode 100644 index 6f5f74d7bd1..00000000000 --- a/pkg/kinds/general_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package kinds - -import ( - "testing" - - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestMetaAccessor(t *testing.T) { - originInfo := &ResourceOriginInfo{ - Name: "test", - Path: "a/b/c", - Key: "kkk", - } - - // Verify that you can set annotations when they do not exist - dummy := &GrafanaResourceMetadata{} - dummy.SetOriginInfo(originInfo) - dummy.SetFolder("folderUID") - - // with any k8s object - obj := &unstructured.Unstructured{} - meta := MetaAccessor(obj) - meta.SetOriginInfo(originInfo) - meta.SetFolder("folderUID") - - require.Equal(t, map[string]string{ - "grafana.app/originName": "test", - "grafana.app/originPath": "a/b/c", - "grafana.app/originKey": "kkk", - "grafana.app/folder": "folderUID", - }, dummy.Annotations) - require.Equal(t, dummy.Annotations, obj.GetAnnotations()) -} diff --git a/pkg/kinds/librarypanel/librarypanel_gen.go b/pkg/kinds/librarypanel/librarypanel_gen.go index 051a738b9f1..e13b6addf9f 100644 --- a/pkg/kinds/librarypanel/librarypanel_gen.go +++ b/pkg/kinds/librarypanel/librarypanel_gen.go @@ -10,6 +10,8 @@ package librarypanel import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/kinds" ) @@ -19,9 +21,11 @@ type K8sResource = kinds.GrafanaResource[Spec, Status] // NewResource creates a new instance of the resource with a given name (UID) func NewK8sResource(name string, s *Spec) K8sResource { return K8sResource{ - Kind: "LibraryPanel", - APIVersion: "v0-0-alpha", - Metadata: kinds.GrafanaResourceMetadata{ + TypeMeta: v1.TypeMeta{ + Kind: "LibraryPanel", + APIVersion: "v0-0-alpha", + }, + ObjectMeta: v1.ObjectMeta{ Name: name, Annotations: make(map[string]string), Labels: make(map[string]string), diff --git a/pkg/kinds/preferences/preferences_gen.go b/pkg/kinds/preferences/preferences_gen.go index aec2fc93fbd..9240e81efa1 100644 --- a/pkg/kinds/preferences/preferences_gen.go +++ b/pkg/kinds/preferences/preferences_gen.go @@ -10,6 +10,8 @@ package preferences import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/kinds" ) @@ -19,9 +21,11 @@ type K8sResource = kinds.GrafanaResource[Spec, Status] // NewResource creates a new instance of the resource with a given name (UID) func NewK8sResource(name string, s *Spec) K8sResource { return K8sResource{ - Kind: "Preferences", - APIVersion: "v0-0-alpha", - Metadata: kinds.GrafanaResourceMetadata{ + TypeMeta: v1.TypeMeta{ + Kind: "Preferences", + APIVersion: "v0-0-alpha", + }, + ObjectMeta: v1.ObjectMeta{ Name: name, Annotations: make(map[string]string), Labels: make(map[string]string), diff --git a/pkg/kinds/publicdashboard/publicdashboard_gen.go b/pkg/kinds/publicdashboard/publicdashboard_gen.go index 158fe87cf63..c9e106313f3 100644 --- a/pkg/kinds/publicdashboard/publicdashboard_gen.go +++ b/pkg/kinds/publicdashboard/publicdashboard_gen.go @@ -10,6 +10,8 @@ package publicdashboard import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/kinds" ) @@ -19,9 +21,11 @@ type K8sResource = kinds.GrafanaResource[Spec, Status] // NewResource creates a new instance of the resource with a given name (UID) func NewK8sResource(name string, s *Spec) K8sResource { return K8sResource{ - Kind: "PublicDashboard", - APIVersion: "v0-0-alpha", - Metadata: kinds.GrafanaResourceMetadata{ + TypeMeta: v1.TypeMeta{ + Kind: "PublicDashboard", + APIVersion: "v0-0-alpha", + }, + ObjectMeta: v1.ObjectMeta{ Name: name, Annotations: make(map[string]string), Labels: make(map[string]string), diff --git a/pkg/kinds/role/role_gen.go b/pkg/kinds/role/role_gen.go index a3bfb86a502..8b1bfb99472 100644 --- a/pkg/kinds/role/role_gen.go +++ b/pkg/kinds/role/role_gen.go @@ -10,6 +10,8 @@ package role import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/kinds" ) @@ -19,9 +21,11 @@ type K8sResource = kinds.GrafanaResource[Spec, Status] // NewResource creates a new instance of the resource with a given name (UID) func NewK8sResource(name string, s *Spec) K8sResource { return K8sResource{ - Kind: "Role", - APIVersion: "v0-0-alpha", - Metadata: kinds.GrafanaResourceMetadata{ + TypeMeta: v1.TypeMeta{ + Kind: "Role", + APIVersion: "v0-0-alpha", + }, + ObjectMeta: v1.ObjectMeta{ Name: name, Annotations: make(map[string]string), Labels: make(map[string]string), diff --git a/pkg/kinds/rolebinding/rolebinding_gen.go b/pkg/kinds/rolebinding/rolebinding_gen.go index c5177cafb9a..e12f62c1cd7 100644 --- a/pkg/kinds/rolebinding/rolebinding_gen.go +++ b/pkg/kinds/rolebinding/rolebinding_gen.go @@ -10,6 +10,8 @@ package rolebinding import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/kinds" ) @@ -19,9 +21,11 @@ type K8sResource = kinds.GrafanaResource[Spec, Status] // NewResource creates a new instance of the resource with a given name (UID) func NewK8sResource(name string, s *Spec) K8sResource { return K8sResource{ - Kind: "RoleBinding", - APIVersion: "v0-0-alpha", - Metadata: kinds.GrafanaResourceMetadata{ + TypeMeta: v1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: "v0-0-alpha", + }, + ObjectMeta: v1.ObjectMeta{ Name: name, Annotations: make(map[string]string), Labels: make(map[string]string), diff --git a/pkg/kinds/team/team_gen.go b/pkg/kinds/team/team_gen.go index c09d04fdf36..c4be50f7133 100644 --- a/pkg/kinds/team/team_gen.go +++ b/pkg/kinds/team/team_gen.go @@ -10,6 +10,8 @@ package team import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/kinds" ) @@ -19,9 +21,11 @@ type K8sResource = kinds.GrafanaResource[Spec, Status] // NewResource creates a new instance of the resource with a given name (UID) func NewK8sResource(name string, s *Spec) K8sResource { return K8sResource{ - Kind: "Team", - APIVersion: "v0-0-alpha", - Metadata: kinds.GrafanaResourceMetadata{ + TypeMeta: v1.TypeMeta{ + Kind: "Team", + APIVersion: "v0-0-alpha", + }, + ObjectMeta: v1.ObjectMeta{ Name: name, Annotations: make(map[string]string), Labels: make(map[string]string), diff --git a/pkg/registry/apis/dashboard/access/sql_dashboards.go b/pkg/registry/apis/dashboard/access/sql_dashboards.go index 9e65f11278a..b0e8304fa6f 100644 --- a/pkg/registry/apis/dashboard/access/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/access/sql_dashboards.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/kinds" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" @@ -65,7 +64,7 @@ func NewDashboardAccess(sql db.DB, namespacer request.NamespaceMapper, dashStore } } -const selector = `SELECT +const selector = `SELECT dashboard.org_id, dashboard.id, dashboard.uid,slug, dashboard.folder_uid, @@ -79,10 +78,10 @@ const selector = `SELECT dashboard.version, title, dashboard.data - FROM dashboard + FROM dashboard LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id - LEFT OUTER JOIN user AS CreatedUSER ON dashboard.created_by = CreatedUSER.id - LEFT OUTER JOIN user AS UpdatedUSER ON dashboard.created_by = UpdatedUSER.id + LEFT OUTER JOIN user AS CreatedUSER ON dashboard.created_by = CreatedUSER.id + LEFT OUTER JOIN user AS UpdatedUSER ON dashboard.created_by = UpdatedUSER.id WHERE is_folder = false` // GetDashboards implements DashboardAccess. @@ -303,7 +302,10 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { dash.Namespace = a.namespacer(orgId) dash.UID = utils.CalculateClusterWideUID(dash) dash.SetCreationTimestamp(v1.NewTime(created)) - meta := kinds.MetaAccessor(dash) + meta, err := utils.MetaAccessor(dash) + if err != nil { + return nil, err + } meta.SetUpdatedTimestamp(&updated) meta.SetSlug(slug) if createdByID > 0 { @@ -328,14 +330,14 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { return nil, err } - meta.SetOriginInfo(&kinds.ResourceOriginInfo{ + meta.SetOriginInfo(&utils.ResourceOriginInfo{ Name: origin_name.String, Path: originPath, Key: origin_key.String, Timestamp: &ts, }) } else if plugin_id != "" { - meta.SetOriginInfo(&kinds.ResourceOriginInfo{ + meta.SetOriginInfo(&utils.ResourceOriginInfo{ Name: "plugin", Path: plugin_id, }) @@ -403,7 +405,10 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das dash.Spec.Remove("uid") } - meta := kinds.MetaAccessor(dash) + meta, err := utils.MetaAccessor(dash) + if err != nil { + return nil, false, err + } out, err := a.dashStore.SaveDashboard(ctx, dashboards.SaveDashboardCommand{ OrgID: orgId, Dashboard: simplejson.NewFromAny(dash.Spec.UnstructuredContent()), diff --git a/pkg/registry/apis/datasource/connections.go b/pkg/registry/apis/datasource/connections.go index a10692e51f7..56cf94bd333 100644 --- a/pkg/registry/apis/datasource/connections.go +++ b/pkg/registry/apis/datasource/connections.go @@ -12,7 +12,6 @@ import ( common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/kinds" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" ) @@ -63,7 +62,7 @@ func (s *connectionAccess) Get(ctx context.Context, name string, options *metav1 if err != nil { return nil, err } - return s.asConnection(ds, ns), nil + return s.asConnection(ds, ns) } func (s *connectionAccess) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { @@ -78,13 +77,14 @@ func (s *connectionAccess) List(ctx context.Context, options *internalversion.Li vals, err := s.builder.getDataSources(ctx) if err == nil { for _, ds := range vals { - result.Items = append(result.Items, *s.asConnection(ds, ns)) + v, _ := s.asConnection(ds, ns) + result.Items = append(result.Items, *v) } } return result, err } -func (s *connectionAccess) asConnection(ds *datasources.DataSource, ns string) *v0alpha1.DataSourceConnection { +func (s *connectionAccess) asConnection(ds *datasources.DataSource, ns string) (*v0alpha1.DataSourceConnection, error) { v := &v0alpha1.DataSourceConnection{ TypeMeta: s.resourceInfo.TypeMeta(), ObjectMeta: metav1.ObjectMeta{ @@ -96,7 +96,9 @@ func (s *connectionAccess) asConnection(ds *datasources.DataSource, ns string) * Title: ds.Name, } v.UID = utils.CalculateClusterWideUID(v) // indicates if the value changed on the server - meta := kinds.MetaAccessor(v) - meta.SetUpdatedTimestamp(&ds.Updated) - return v + meta, err := utils.MetaAccessor(v) + if err != nil { + meta.SetUpdatedTimestamp(&ds.Updated) + } + return v, err } diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index 21f0d977d24..1803995cac3 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -6,27 +6,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/grafana/grafana/pkg/apis/folders/v0alpha1" - "github.com/grafana/grafana/pkg/kinds" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" ) func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) *v0alpha1.Folder { - meta := kinds.GrafanaResourceMetadata{} - meta.SetUpdatedTimestampMillis(v.Updated.UnixMilli()) - if v.ID > 0 { // nolint:staticcheck - meta.SetOriginInfo(&kinds.ResourceOriginInfo{ - Name: "SQL", - Key: fmt.Sprintf("%d", v.ID), // nolint:staticcheck - }) - } - if v.CreatedBy > 0 { - meta.SetCreatedBy(fmt.Sprintf("user:%d", v.CreatedBy)) - } - if v.UpdatedBy > 0 { - meta.SetUpdatedBy(fmt.Sprintf("user:%d", v.UpdatedBy)) - } f := &v0alpha1.Folder{ TypeMeta: v0alpha1.FolderResourceInfo.TypeMeta(), ObjectMeta: metav1.ObjectMeta{ @@ -34,13 +19,30 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) ResourceVersion: fmt.Sprintf("%d", v.Updated.UnixMilli()), CreationTimestamp: metav1.NewTime(v.Created), Namespace: namespacer(v.OrgID), - Annotations: meta.Annotations, }, Spec: v0alpha1.Spec{ Title: v.Title, Description: v.Description, }, } + + meta, err := utils.MetaAccessor(f) + if err == nil { + meta.SetUpdatedTimestamp(&v.Updated) + if v.ID > 0 { // nolint:staticcheck + meta.SetOriginInfo(&utils.ResourceOriginInfo{ + Name: "SQL", + Key: fmt.Sprintf("%d", v.ID), // nolint:staticcheck + }) + } + if v.CreatedBy > 0 { + meta.SetCreatedBy(fmt.Sprintf("user:%d", v.CreatedBy)) + } + if v.UpdatedBy > 0 { + meta.SetUpdatedBy(fmt.Sprintf("user:%d", v.UpdatedBy)) + } + } + f.UID = utils.CalculateClusterWideUID(f) return f } diff --git a/pkg/registry/apis/folders/legacy_storage.go b/pkg/registry/apis/folders/legacy_storage.go index 402850a582e..d27a9c5db68 100644 --- a/pkg/registry/apis/folders/legacy_storage.go +++ b/pkg/registry/apis/folders/legacy_storage.go @@ -13,10 +13,10 @@ import ( "github.com/grafana/grafana/pkg/apis/folders/v0alpha1" "github.com/grafana/grafana/pkg/infra/appcontext" - "github.com/grafana/grafana/pkg/kinds" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" "github.com/grafana/grafana/pkg/util" ) @@ -148,7 +148,10 @@ func (s *legacyStorage) Create(ctx context.Context, p.Spec.Title = strings.ReplaceAll(p.Spec.Title, "${RAND}", rand) } - accessor := kinds.MetaAccessor(p) + accessor, err := utils.MetaAccessor(p) + if err != nil { + return nil, err + } parent := accessor.GetFolder() out, err := s.service.Create(ctx, &folder.CreateFolderCommand{ @@ -202,8 +205,10 @@ func (s *legacyStorage) Update(ctx context.Context, return nil, created, fmt.Errorf("expected old object to be a folder also") } - oldParent := kinds.MetaAccessor(old).GetFolder() - newParent := kinds.MetaAccessor(f).GetFolder() + mOld, _ := utils.MetaAccessor(old) + mNew, _ := utils.MetaAccessor(f) + oldParent := mOld.GetFolder() + newParent := mNew.GetFolder() if oldParent != newParent { _, err = s.service.Move(ctx, &folder.MoveFolderCommand{ SignedInUser: user, diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 381dcb0cf22..e5ff6a32bae 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -15,7 +15,6 @@ import ( common "k8s.io/kube-openapi/pkg/common" "github.com/grafana/grafana/pkg/apis/folders/v0alpha1" - "github.com/grafana/grafana/pkg/kinds" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" @@ -116,7 +115,7 @@ func (b *FolderAPIBuilder) GetAPIGroupInfo( func(obj any) ([]interface{}, error) { r, ok := obj.(*v0alpha1.Folder) if ok { - accessor := kinds.MetaAccessor(r) + accessor, _ := utils.MetaAccessor(r) return []interface{}{ r.Name, r.Spec.Title, diff --git a/pkg/registry/apis/playlist/conversions.go b/pkg/registry/apis/playlist/conversions.go index 88b67c69cba..43317aac080 100644 --- a/pkg/registry/apis/playlist/conversions.go +++ b/pkg/registry/apis/playlist/conversions.go @@ -11,7 +11,6 @@ import ( "k8s.io/apimachinery/pkg/types" playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" - "github.com/grafana/grafana/pkg/kinds" "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" playlistsvc "github.com/grafana/grafana/pkg/services/playlist" @@ -78,14 +77,6 @@ func convertToK8sResource(v *playlistsvc.PlaylistDTO, namespacer request.Namespa }) } - meta := kinds.GrafanaResourceMetadata{} - meta.SetUpdatedTimestampMillis(v.UpdatedAt) - if v.Id > 0 { - meta.SetOriginInfo(&kinds.ResourceOriginInfo{ - Name: "SQL", - Key: fmt.Sprintf("%d", v.Id), - }) - } p := &playlist.Playlist{ ObjectMeta: metav1.ObjectMeta{ Name: v.Uid, @@ -93,10 +84,20 @@ func convertToK8sResource(v *playlistsvc.PlaylistDTO, namespacer request.Namespa ResourceVersion: fmt.Sprintf("%d", v.UpdatedAt), CreationTimestamp: metav1.NewTime(time.UnixMilli(v.CreatedAt)), Namespace: namespacer(v.OrgID), - Annotations: meta.Annotations, }, Spec: spec, } + meta, err := utils.MetaAccessor(p) + if err == nil { + meta.SetUpdatedTimestampMillis(v.UpdatedAt) + if v.Id > 0 { + meta.SetOriginInfo(&utils.ResourceOriginInfo{ + Name: "SQL", + Key: fmt.Sprintf("%d", v.Id), + }) + } + } + p.UID = utils.CalculateClusterWideUID(p) return p } @@ -123,8 +124,9 @@ func convertToLegacyUpdateCommand(p *playlist.Playlist, orgId int64) (*playlists // Read legacy ID from metadata annotations func getLegacyID(item *unstructured.Unstructured) int64 { - meta := kinds.GrafanaResourceMetadata{ - Annotations: item.GetAnnotations(), + meta, err := utils.MetaAccessor(item) + if err != nil { + return 0 } info, _ := meta.GetOriginInfo() if info != nil && info.Name == "SQL" { diff --git a/pkg/services/grafana-apiserver/storage/entity/utils.go b/pkg/services/grafana-apiserver/storage/entity/utils.go index 724d931c972..b20e40c9ebe 100644 --- a/pkg/services/grafana-apiserver/storage/entity/utils.go +++ b/pkg/services/grafana-apiserver/storage/entity/utils.go @@ -15,7 +15,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/endpoints/request" - "github.com/grafana/grafana/pkg/kinds" + "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" entityStore "github.com/grafana/grafana/pkg/services/store/entity" ) @@ -50,7 +50,10 @@ func entityToResource(rsp *entityStore.Entity, res runtime.Object, codec runtime metaAccessor.SetResourceVersion(fmt.Sprintf("%d", rsp.ResourceVersion)) metaAccessor.SetCreationTimestamp(metav1.Unix(rsp.CreatedAt/1000, rsp.CreatedAt%1000*1000000)) - grafanaAccessor := kinds.MetaAccessor(metaAccessor) + grafanaAccessor, err := utils.MetaAccessor(metaAccessor) + if err != nil { + return err + } if rsp.Folder != "" { grafanaAccessor.SetFolder(rsp.Folder) @@ -66,11 +69,10 @@ func entityToResource(rsp *entityStore.Entity, res runtime.Object, codec runtime grafanaAccessor.SetUpdatedTimestamp(&updatedAt) } grafanaAccessor.SetSlug(rsp.Slug) - grafanaAccessor.SetTitle(rsp.Title) if rsp.Origin != nil { originTime := time.UnixMilli(rsp.Origin.Time).UTC() - grafanaAccessor.SetOriginInfo(&kinds.ResourceOriginInfo{ + grafanaAccessor.SetOriginInfo(&utils.ResourceOriginInfo{ Name: rsp.Origin.Source, Key: rsp.Origin.Key, // Path: rsp.Origin.Path, @@ -103,7 +105,10 @@ func resourceToEntity(key string, res runtime.Object, requestInfo *request.Reque return nil, err } - grafanaAccessor := kinds.MetaAccessor(metaAccessor) + grafanaAccessor, err := utils.MetaAccessor(metaAccessor) + if err != nil { + return nil, err + } rv, _ := strconv.ParseInt(metaAccessor.GetResourceVersion(), 10, 64) rsp := &entityStore.Entity{ @@ -121,7 +126,7 @@ func resourceToEntity(key string, res runtime.Object, requestInfo *request.Reque CreatedBy: grafanaAccessor.GetCreatedBy(), UpdatedBy: grafanaAccessor.GetUpdatedBy(), Slug: grafanaAccessor.GetSlug(), - Title: grafanaAccessor.GetTitle(), + Title: grafanaAccessor.FindTitle(metaAccessor.GetName()), Origin: &entityStore.EntityOriginInfo{ Source: grafanaAccessor.GetOriginName(), Key: grafanaAccessor.GetOriginKey(), diff --git a/pkg/services/grafana-apiserver/storage/entity/utils_test.go b/pkg/services/grafana-apiserver/storage/entity/utils_test.go index 88828707aea..bd2dbd84f71 100644 --- a/pkg/services/grafana-apiserver/storage/entity/utils_test.go +++ b/pkg/services/grafana-apiserver/storage/entity/utils_test.go @@ -86,6 +86,7 @@ func TestResourceToEntity(t *testing.T) { expectedKey: "/playlist.grafana.app/playlists/default/test-uid", expectedGroupVersion: apiVersion, expectedName: "test-name", + expectedTitle: "A playlist", expectedGuid: "test-uid", expectedVersion: "1", expectedFolder: "test-folder", @@ -154,7 +155,7 @@ func TestEntityToResource(t *testing.T) { Key: "/playlist.grafana.app/playlists/default/test-uid", GroupVersion: "v0alpha1", Name: "test-uid", - Title: "test-name", + Title: "A playlist", Guid: "test-guid", Folder: "test-folder", CreatedBy: "test-created-by", @@ -180,7 +181,6 @@ func TestEntityToResource(t *testing.T) { "grafana.app/createdBy": "test-created-by", "grafana.app/folder": "test-folder", "grafana.app/slug": "test-slug", - "grafana.app/title": "test-name", "grafana.app/updatedBy": "test-updated-by", "grafana.app/updatedTimestamp": updatedAtStr, }, diff --git a/pkg/services/grafana-apiserver/utils/meta.go b/pkg/services/grafana-apiserver/utils/meta.go new file mode 100644 index 00000000000..a0574e25da9 --- /dev/null +++ b/pkg/services/grafana-apiserver/utils/meta.go @@ -0,0 +1,266 @@ +package utils + +import ( + "fmt" + "reflect" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Annotation keys + +const AnnoKeyCreatedBy = "grafana.app/createdBy" +const AnnoKeyUpdatedTimestamp = "grafana.app/updatedTimestamp" +const AnnoKeyUpdatedBy = "grafana.app/updatedBy" +const AnnoKeyFolder = "grafana.app/folder" +const AnnoKeySlug = "grafana.app/slug" + +// Identify where values came from + +const AnnoKeyOriginName = "grafana.app/originName" +const AnnoKeyOriginPath = "grafana.app/originPath" +const AnnoKeyOriginKey = "grafana.app/originKey" +const AnnoKeyOriginTimestamp = "grafana.app/originTimestamp" + +// ResourceOriginInfo is saved in annotations. This is used to identify where the resource came from +// This object can model the same data as our existing provisioning table or a more general git sync +type ResourceOriginInfo struct { + // Name of the origin/provisioning source + Name string `json:"name,omitempty"` + + // The path within the named origin above (external_id in the existing dashboard provisioing) + Path string `json:"path,omitempty"` + + // Verification/identification key (check_sum in existing dashboard provisioning) + Key string `json:"key,omitempty"` + + // Origin modification timestamp when the resource was saved + // This will be before the resource updated time + Timestamp *time.Time `json:"time,omitempty"` + + // Avoid extending + _ any `json:"-"` +} + +// Accessor functions for k8s objects +type GrafanaResourceMetaAccessor interface { + GetUpdatedTimestamp() (*time.Time, error) + SetUpdatedTimestamp(v *time.Time) + SetUpdatedTimestampMillis(unix int64) + GetCreatedBy() string + SetCreatedBy(user string) + GetUpdatedBy() string + SetUpdatedBy(user string) + GetFolder() string + SetFolder(uid string) + GetSlug() string + SetSlug(v string) + GetOriginInfo() (*ResourceOriginInfo, error) + SetOriginInfo(info *ResourceOriginInfo) + GetOriginName() string + GetOriginPath() string + GetOriginKey() string + GetOriginTimestamp() (*time.Time, error) + + // Find a title in the object + // This will reflect the object and try to get: + // * spec.title + // * spec.name + // * title + // and return an empty string if nothing was found + FindTitle(defaultTitle string) string +} + +var _ GrafanaResourceMetaAccessor = (*grafanaResourceMetaAccessor)(nil) + +type grafanaResourceMetaAccessor struct { + raw interface{} // the original object (it implements metav1.Object) + obj metav1.Object +} + +// Accessor takes an arbitrary object pointer and returns meta.Interface. +// obj must be a pointer to an API type. An error is returned if the minimum +// required fields are missing. Fields that are not required return the default +// value and are a no-op if set. +func MetaAccessor(raw interface{}) (GrafanaResourceMetaAccessor, error) { + obj, err := meta.Accessor(raw) + if err != nil { + return nil, err + } + return &grafanaResourceMetaAccessor{raw, obj}, nil +} + +func (m *grafanaResourceMetaAccessor) set(key string, val string) { + anno := m.obj.GetAnnotations() + if val == "" { + if anno != nil { + delete(anno, key) + } + } else { + if anno == nil { + anno = make(map[string]string) + } + anno[key] = val + } + m.obj.SetAnnotations(anno) +} + +func (m *grafanaResourceMetaAccessor) get(key string) string { + return m.obj.GetAnnotations()[key] +} + +func (m *grafanaResourceMetaAccessor) GetUpdatedTimestamp() (*time.Time, error) { + v, ok := m.obj.GetAnnotations()[AnnoKeyUpdatedTimestamp] + if !ok || v == "" { + return nil, nil + } + t, err := time.Parse(time.RFC3339, v) + if err != nil { + return nil, fmt.Errorf("invalid updated timestamp: %s", err.Error()) + } + return &t, nil +} + +func (m *grafanaResourceMetaAccessor) SetUpdatedTimestampMillis(v int64) { + if v > 0 { + t := time.UnixMilli(v) + m.SetUpdatedTimestamp(&t) + } else { + m.set(AnnoKeyUpdatedTimestamp, "") // will clear the annotation + } +} + +func (m *grafanaResourceMetaAccessor) SetUpdatedTimestamp(v *time.Time) { + txt := "" + if v != nil && v.Unix() != 0 { + txt = v.UTC().Format(time.RFC3339) + } + m.set(AnnoKeyUpdatedTimestamp, txt) +} + +func (m *grafanaResourceMetaAccessor) GetCreatedBy() string { + return m.get(AnnoKeyCreatedBy) +} + +func (m *grafanaResourceMetaAccessor) SetCreatedBy(user string) { + m.set(AnnoKeyCreatedBy, user) +} + +func (m *grafanaResourceMetaAccessor) GetUpdatedBy() string { + return m.get(AnnoKeyUpdatedBy) +} + +func (m *grafanaResourceMetaAccessor) SetUpdatedBy(user string) { + m.set(AnnoKeyUpdatedBy, user) +} + +func (m *grafanaResourceMetaAccessor) GetFolder() string { + return m.get(AnnoKeyFolder) +} + +func (m *grafanaResourceMetaAccessor) SetFolder(uid string) { + m.set(AnnoKeyFolder, uid) +} + +func (m *grafanaResourceMetaAccessor) GetSlug() string { + return m.get(AnnoKeySlug) +} + +func (m *grafanaResourceMetaAccessor) SetSlug(v string) { + m.set(AnnoKeySlug, v) +} + +func (m *grafanaResourceMetaAccessor) SetOriginInfo(info *ResourceOriginInfo) { + anno := m.obj.GetAnnotations() + if anno == nil { + if info == nil { + return + } + anno = make(map[string]string, 0) + } + + delete(anno, AnnoKeyOriginName) + delete(anno, AnnoKeyOriginPath) + delete(anno, AnnoKeyOriginKey) + delete(anno, AnnoKeyOriginTimestamp) + if info != nil && info.Name != "" { + anno[AnnoKeyOriginName] = info.Name + if info.Path != "" { + anno[AnnoKeyOriginPath] = info.Path + } + if info.Key != "" { + anno[AnnoKeyOriginKey] = info.Key + } + if info.Timestamp != nil { + anno[AnnoKeyOriginTimestamp] = info.Timestamp.Format(time.RFC3339) + } + } + m.obj.SetAnnotations(anno) +} + +func (m *grafanaResourceMetaAccessor) GetOriginInfo() (*ResourceOriginInfo, error) { + v, ok := m.obj.GetAnnotations()[AnnoKeyOriginName] + if !ok { + return nil, nil + } + t, err := m.GetOriginTimestamp() + return &ResourceOriginInfo{ + Name: v, + Path: m.GetOriginPath(), + Key: m.GetOriginKey(), + Timestamp: t, + }, err +} + +func (m *grafanaResourceMetaAccessor) GetOriginName() string { + return m.get(AnnoKeyOriginName) +} + +func (m *grafanaResourceMetaAccessor) GetOriginPath() string { + return m.get(AnnoKeyOriginPath) +} + +func (m *grafanaResourceMetaAccessor) GetOriginKey() string { + return m.get(AnnoKeyOriginKey) +} + +func (m *grafanaResourceMetaAccessor) GetOriginTimestamp() (*time.Time, error) { + v, ok := m.obj.GetAnnotations()[AnnoKeyOriginTimestamp] + if !ok || v == "" { + return nil, nil + } + t, err := time.Parse(time.RFC3339, v) + if err != nil { + return nil, fmt.Errorf("invalid origin timestamp: %s", err.Error()) + } + return &t, nil +} + +func (m *grafanaResourceMetaAccessor) FindTitle(defaultTitle string) string { + // look for Spec.Title or Spec.Name + r := reflect.ValueOf(m.raw) + if r.Kind() == reflect.Ptr || r.Kind() == reflect.Interface { + r = r.Elem() + } + if r.Kind() == reflect.Struct { + spec := r.FieldByName("Spec") + if spec.Kind() == reflect.Struct { + title := spec.FieldByName("Title") + if title.IsValid() && title.Kind() == reflect.String { + return title.String() + } + name := spec.FieldByName("Name") + if name.IsValid() && name.Kind() == reflect.String { + return name.String() + } + } + + title := r.FieldByName("Title") + if title.IsValid() && title.Kind() == reflect.String { + return title.String() + } + } + return defaultTitle +} diff --git a/pkg/services/grafana-apiserver/utils/meta_test.go b/pkg/services/grafana-apiserver/utils/meta_test.go new file mode 100644 index 00000000000..9d9cfedd3ce --- /dev/null +++ b/pkg/services/grafana-apiserver/utils/meta_test.go @@ -0,0 +1,208 @@ +package utils_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" +) + +type TestResource struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec Spec `json:"spec,omitempty"` +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResource) DeepCopyInto(out *TestResource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Playlist. +func (in *TestResource) DeepCopy() *TestResource { + if in == nil { + return nil + } + out := new(TestResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResource) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// Spec defines model for Spec. +type Spec struct { + // Name of the object. + Title string `json:"title"` +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec) DeepCopyInto(out *Spec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec) DeepCopy() *Spec { + if in == nil { + return nil + } + out := new(Spec) + in.DeepCopyInto(out) + return out +} + +type TestResource2 struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec Spec2 `json:"spec,omitempty"` +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResource2) DeepCopyInto(out *TestResource2) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Playlist. +func (in *TestResource2) DeepCopy() *TestResource2 { + if in == nil { + return nil + } + out := new(TestResource2) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResource2) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// Spec defines model for Spec. +type Spec2 struct{} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Spec2) DeepCopyInto(out *Spec2) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec. +func (in *Spec2) DeepCopy() *Spec2 { + if in == nil { + return nil + } + out := new(Spec2) + in.DeepCopyInto(out) + return out +} + +func TestMetaAccessor(t *testing.T) { + originInfo := &utils.ResourceOriginInfo{ + Name: "test", + Path: "a/b/c", + Key: "kkk", + } + + t.Run("fails for non resource objects", func(t *testing.T) { + _, err := utils.MetaAccessor("hello") + require.Error(t, err) + + _, err = utils.MetaAccessor(unstructured.Unstructured{}) + require.Error(t, err) // Not a pointer! + + _, err = utils.MetaAccessor(&unstructured.Unstructured{}) + require.NoError(t, err) // Must be a pointer + + _, err = utils.MetaAccessor(&TestResource{ + Spec: Spec{ + Title: "HELLO", + }, + }) + require.NoError(t, err) // Must be a pointer + }) + + t.Run("get and set grafana metadata", func(t *testing.T) { + res := &unstructured.Unstructured{} + meta, err := utils.MetaAccessor(res) + require.NoError(t, err) + + meta.SetOriginInfo(originInfo) + meta.SetFolder("folderUID") + + require.Equal(t, map[string]string{ + "grafana.app/originName": "test", + "grafana.app/originPath": "a/b/c", + "grafana.app/originKey": "kkk", + "grafana.app/folder": "folderUID", + }, res.GetAnnotations()) + }) + + t.Run("find titles", func(t *testing.T) { + // with a k8s object that has Spec.Title + obj := &TestResource{ + Spec: Spec{ + Title: "HELLO", + }, + } + + meta, err := utils.MetaAccessor(obj) + require.NoError(t, err) + meta.SetOriginInfo(originInfo) + meta.SetFolder("folderUID") + + require.Equal(t, map[string]string{ + "grafana.app/originName": "test", + "grafana.app/originPath": "a/b/c", + "grafana.app/originKey": "kkk", + "grafana.app/folder": "folderUID", + }, obj.GetAnnotations()) + + require.Equal(t, "HELLO", obj.Spec.Title) + require.Equal(t, "HELLO", meta.FindTitle("")) + obj.Spec.Title = "" + require.Equal(t, "", meta.FindTitle("xxx")) + + // with a k8s object without Spec.Title + obj2 := &TestResource2{} + + meta, err = utils.MetaAccessor(obj2) + require.NoError(t, err) + meta.SetOriginInfo(originInfo) + meta.SetFolder("folderUID") + + require.Equal(t, map[string]string{ + "grafana.app/originName": "test", + "grafana.app/originPath": "a/b/c", + "grafana.app/originKey": "kkk", + "grafana.app/folder": "folderUID", + }, obj2.GetAnnotations()) + + require.Equal(t, "xxx", meta.FindTitle("xxx")) + }) +} diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index 88a2dcfa545..df8c1abd7a4 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -3,13 +3,9 @@ package model import ( "encoding/json" "errors" - "fmt" "time" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/kinds" "github.com/grafana/grafana/pkg/kinds/librarypanel" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type LibraryConnectionKind int @@ -85,32 +81,6 @@ type LibraryElementDTO struct { SchemaVersion int64 `json:"schemaVersion,omitempty"` } -func (dto *LibraryElementDTO) ToResource() kinds.GrafanaResource[simplejson.Json, simplejson.Json] { - body := &simplejson.Json{} - _ = body.FromDB(dto.Model) - parent := librarypanel.NewK8sResource(dto.UID, nil) - res := kinds.GrafanaResource[simplejson.Json, simplejson.Json]{ - Kind: parent.Kind, - APIVersion: parent.APIVersion, - Metadata: kinds.GrafanaResourceMetadata{ - Name: dto.UID, - Annotations: make(map[string]string), - Labels: make(map[string]string), - ResourceVersion: fmt.Sprintf("%d", dto.Version), - CreationTimestamp: v1.NewTime(dto.Meta.Created), - }, - Spec: body, - } - - if dto.FolderUID != "" { - res.Metadata.SetFolder(dto.FolderUID) - } - res.Metadata.SetCreatedBy(fmt.Sprintf("user:%d", dto.Meta.CreatedBy.Id)) - res.Metadata.SetUpdatedBy(fmt.Sprintf("user:%d", dto.Meta.UpdatedBy.Id)) - res.Metadata.SetUpdatedTimestamp(&dto.Meta.Updated) - return res -} - // LibraryElementSearchResult is the search result for entities. type LibraryElementSearchResult struct { TotalCount int64 `json:"totalCount"` diff --git a/pkg/services/libraryelements/model/model_test.go b/pkg/services/libraryelements/model/model_test.go deleted file mode 100644 index 5bdbb068b88..00000000000 --- a/pkg/services/libraryelements/model/model_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package model - -import ( - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/grafana/grafana/pkg/kinds/librarypanel" - "github.com/stretchr/testify/require" -) - -func TestLibaryPanelConversion(t *testing.T) { - body := `{}` - - src := LibraryElementDTO{ - Kind: 0, // always library panel - FolderUID: "TheFolderUID", - UID: "TheUID", - Version: 10, - Model: json.RawMessage(body), - Meta: LibraryElementDTOMeta{ - Created: time.UnixMilli(946713600000).UTC(), // 2000-01-01 - Updated: time.UnixMilli(1262332800000).UTC(), // 2010-01-01, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ - Id: 11, - }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ - Id: 12, - }, - }, - } - - dst := src.ToResource() - - require.Equal(t, src.UID, dst.Metadata.Name) - - out, err := json.MarshalIndent(dst, "", " ") - require.NoError(t, err) - fmt.Printf("%s", string(out)) - require.JSONEq(t, `{ - "apiVersion": "v0-0-alpha", - "kind": "LibraryPanel", - "metadata": { - "name": "TheUID", - "resourceVersion": "10", - "creationTimestamp": "2000-01-01T08:00:00Z", - "annotations": { - "grafana.app/createdBy": "user:11", - "grafana.app/folder": "TheFolderUID", - "grafana.app/updatedBy": "user:12", - "grafana.app/updatedTimestamp": "2010-01-01T08:00:00Z" - } - }, - "spec": {} - }`, string(out)) -} diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index 060ab84dfbb..a4d4fe0a6a6 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -4,9 +4,6 @@ import ( "errors" "time" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/kinds/team" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/search/model" @@ -36,18 +33,6 @@ type Team struct { Updated time.Time `json:"updated"` } -func (t *Team) ToResource() team.K8sResource { - r := team.NewK8sResource(t.UID, &team.Spec{ - Name: t.Name, - }) - r.Metadata.CreationTimestamp = v1.NewTime(t.Created) - r.Metadata.SetUpdatedTimestamp(&t.Updated) - if t.Email != "" { - r.Spec.Email = &t.Email - } - return r -} - // --------------------- // COMMANDS diff --git a/pkg/services/team/model_test.go b/pkg/services/team/model_test.go deleted file mode 100644 index 614b3ea2abb..00000000000 --- a/pkg/services/team/model_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package team - -import ( - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestTeamConversion(t *testing.T) { - src := Team{ - ID: 123, - UID: "abc", - Name: "TeamA", - Email: "team@a.org", - OrgID: 11, - Created: time.UnixMilli(946713600000).UTC(), // 2000-01-01 - Updated: time.UnixMilli(1262332800000).UTC(), // 2010-01-01 - } - - dst := src.ToResource() - - require.Equal(t, src.Name, dst.Spec.Name) - - out, err := json.MarshalIndent(dst, "", " ") - require.NoError(t, err) - fmt.Printf("%s", string(out)) - require.JSONEq(t, `{ - "apiVersion": "v0-0-alpha", - "kind": "Team", - "metadata": { - "name": "abc", - "creationTimestamp": "2000-01-01T08:00:00Z", - "annotations": { - "grafana.app/updatedTimestamp": "2010-01-01T08:00:00Z" - } - }, - "spec": { - "email": "team@a.org", - "name": "TeamA" - } - }`, string(out)) -}