Provisioning: Rename k8s origin metadata to repo (#96524)

This commit is contained in:
Ryan McKinley 2024-11-15 17:26:14 +03:00 committed by GitHub
parent 2e62f75166
commit cc6d057a18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 198 additions and 154 deletions

View File

@ -540,8 +540,8 @@ func TestHTTPServer_FolderMetadataK8s(t *testing.T) {
"creationTimestamp": "2024-09-17T04:16:35Z", "creationTimestamp": "2024-09-17T04:16:35Z",
"annotations": { "annotations": {
"grafana.app/createdBy": "user:fdxsqt7t5ryf4a", "grafana.app/createdBy": "user:fdxsqt7t5ryf4a",
"grafana.app/originName": "SQL", "grafana.app/repoName": "SQL",
"grafana.app/originPath": "3" "grafana.app/repoPath": "3"
} }
}, },
"spec": { "spec": {

View File

@ -29,23 +29,35 @@ const AnnoKeyMessage = "grafana.app/message"
// Identify where values came from // Identify where values came from
const AnnoKeyOriginName = "grafana.app/originName" const AnnoKeyRepoName = "grafana.app/repoName"
const AnnoKeyOriginPath = "grafana.app/originPath" const AnnoKeyRepoPath = "grafana.app/repoPath"
const AnnoKeyOriginHash = "grafana.app/originHash" const AnnoKeyRepoHash = "grafana.app/repoHash"
const AnnoKeyOriginTimestamp = "grafana.app/originTimestamp" const AnnoKeyRepoTimestamp = "grafana.app/repoTimestamp"
// #TODO revisit keeping these folder-specific annotations once we have complete support for mode 1 // These can be removed once we verify that non of the dual-write sources
// (for dashboards/playlists/etc) depend on the saved internal ID in SQL
const oldAnnoKeyOriginName = "grafana.app/originName"
const oldAnnoKeyOriginPath = "grafana.app/originPath"
const oldAnnoKeyOriginHash = "grafana.app/originHash"
const oldAnnoKeyOriginTimestamp = "grafana.app/originTimestamp"
const AnnoKeyFullPath = "grafana.app/fullPath" // annoKeyFullPath encodes the full path in folder resources
const AnnoKeyFullPathUIDs = "grafana.app/fullPathUIDs" // revisit keeping these folder-specific annotations once we have complete support for mode 1
// Deprecated: this goes away when folders have a better solution
const annoKeyFullPath = "grafana.app/fullPath"
// ResourceOriginInfo is saved in annotations. This is used to identify where the resource came from // annoKeyFullPathUIDs encodes the full path in folder resources
// This object can model the same data as our existing provisioning table or a more general git sync // Deprecated: this goes away when folders have a better solution
type ResourceOriginInfo struct { const annoKeyFullPathUIDs = "grafana.app/fullPathUIDs"
// Name of the origin/provisioning source
// ResourceRepositoryInfo is encoded into kubernetes metadata annotations.
// This value identifies indicates the state of the resource in its provisioning source when
// the spec was last saved. Currently this is derived from the dashboards provisioning table.
type ResourceRepositoryInfo struct {
// Name of the repository/provisioning source
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// The path within the named origin above (external_id in the existing dashboard provisioing) // The path within the named repository above (external_id in the existing dashboard provisioning)
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
// Verification/identification hash (check_sum in existing dashboard provisioning) // Verification/identification hash (check_sum in existing dashboard provisioning)
@ -90,12 +102,12 @@ type GrafanaMetaAccessor interface {
SetBlob(v *BlobInfo) SetBlob(v *BlobInfo)
GetBlob() *BlobInfo GetBlob() *BlobInfo
GetOriginInfo() (*ResourceOriginInfo, error) GetRepositoryInfo() (*ResourceRepositoryInfo, error)
SetOriginInfo(info *ResourceOriginInfo) SetRepositoryInfo(info *ResourceRepositoryInfo)
GetOriginName() string GetRepositoryName() string
GetOriginPath() string GetRepositoryPath() string
GetOriginHash() string GetRepositoryHash() string
GetOriginTimestamp() (*time.Time, error) GetRepositoryTimestamp() (*time.Time, error)
GetSpec() (any, error) GetSpec() (any, error)
SetSpec(any) error SetSpec(any) error
@ -271,7 +283,16 @@ func (m *grafanaMetaAccessor) SetSlug(v string) {
m.SetAnnotation(AnnoKeySlug, v) m.SetAnnotation(AnnoKeySlug, v)
} }
func (m *grafanaMetaAccessor) SetOriginInfo(info *ResourceOriginInfo) { // This allows looking up a primary and secondary key -- if either exist the value will be returned
func (m *grafanaMetaAccessor) getAnnoValue(primary, secondary string) (string, bool) {
v, ok := m.obj.GetAnnotations()[primary]
if !ok {
v, ok = m.obj.GetAnnotations()[secondary]
}
return v, ok
}
func (m *grafanaMetaAccessor) SetRepositoryInfo(info *ResourceRepositoryInfo) {
anno := m.obj.GetAnnotations() anno := m.obj.GetAnnotations()
if anno == nil { if anno == nil {
if info == nil { if info == nil {
@ -280,53 +301,62 @@ func (m *grafanaMetaAccessor) SetOriginInfo(info *ResourceOriginInfo) {
anno = make(map[string]string, 0) anno = make(map[string]string, 0)
} }
delete(anno, AnnoKeyOriginName) // remove legacy values
delete(anno, AnnoKeyOriginPath) delete(anno, oldAnnoKeyOriginHash)
delete(anno, AnnoKeyOriginHash) delete(anno, oldAnnoKeyOriginPath)
delete(anno, AnnoKeyOriginTimestamp) delete(anno, oldAnnoKeyOriginHash)
delete(anno, oldAnnoKeyOriginTimestamp)
delete(anno, AnnoKeyRepoName)
delete(anno, AnnoKeyRepoPath)
delete(anno, AnnoKeyRepoHash)
delete(anno, AnnoKeyRepoTimestamp)
if info != nil && info.Name != "" { if info != nil && info.Name != "" {
anno[AnnoKeyOriginName] = info.Name anno[AnnoKeyRepoName] = info.Name
if info.Path != "" { if info.Path != "" {
anno[AnnoKeyOriginPath] = info.Path anno[AnnoKeyRepoPath] = info.Path
} }
if info.Hash != "" { if info.Hash != "" {
anno[AnnoKeyOriginHash] = info.Hash anno[AnnoKeyRepoHash] = info.Hash
} }
if info.Timestamp != nil { if info.Timestamp != nil {
anno[AnnoKeyOriginTimestamp] = info.Timestamp.UTC().Format(time.RFC3339) anno[AnnoKeyRepoTimestamp] = info.Timestamp.UTC().Format(time.RFC3339)
} }
} }
m.obj.SetAnnotations(anno) m.obj.SetAnnotations(anno)
} }
func (m *grafanaMetaAccessor) GetOriginInfo() (*ResourceOriginInfo, error) { func (m *grafanaMetaAccessor) GetRepositoryInfo() (*ResourceRepositoryInfo, error) {
v, ok := m.obj.GetAnnotations()[AnnoKeyOriginName] v, ok := m.getAnnoValue(AnnoKeyRepoName, oldAnnoKeyOriginName)
if !ok { if !ok {
return nil, nil return nil, nil
} }
t, err := m.GetOriginTimestamp() t, err := m.GetRepositoryTimestamp()
return &ResourceOriginInfo{ return &ResourceRepositoryInfo{
Name: v, Name: v,
Path: m.GetOriginPath(), Path: m.GetRepositoryPath(),
Hash: m.GetOriginHash(), Hash: m.GetRepositoryHash(),
Timestamp: t, Timestamp: t,
}, err }, err
} }
func (m *grafanaMetaAccessor) GetOriginName() string { func (m *grafanaMetaAccessor) GetRepositoryName() string {
return m.get(AnnoKeyOriginName) v, _ := m.getAnnoValue(AnnoKeyRepoName, oldAnnoKeyOriginName)
return v // will be empty string
} }
func (m *grafanaMetaAccessor) GetOriginPath() string { func (m *grafanaMetaAccessor) GetRepositoryPath() string {
return m.get(AnnoKeyOriginPath) v, _ := m.getAnnoValue(AnnoKeyRepoPath, oldAnnoKeyOriginPath)
return v // will be empty string
} }
func (m *grafanaMetaAccessor) GetOriginHash() string { func (m *grafanaMetaAccessor) GetRepositoryHash() string {
return m.get(AnnoKeyOriginHash) v, _ := m.getAnnoValue(AnnoKeyRepoHash, oldAnnoKeyOriginHash)
return v // will be empty string
} }
func (m *grafanaMetaAccessor) GetOriginTimestamp() (*time.Time, error) { func (m *grafanaMetaAccessor) GetRepositoryTimestamp() (*time.Time, error) {
v, ok := m.obj.GetAnnotations()[AnnoKeyOriginTimestamp] v, ok := m.getAnnoValue(AnnoKeyRepoTimestamp, oldAnnoKeyOriginTimestamp)
if !ok || v == "" { if !ok || v == "" {
return nil, nil return nil, nil
} }
@ -617,19 +647,23 @@ func (m *grafanaMetaAccessor) SetStatus(s any) (err error) {
} }
func (m *grafanaMetaAccessor) GetFullPath() string { func (m *grafanaMetaAccessor) GetFullPath() string {
return m.get(AnnoKeyFullPath) // nolint:staticcheck
return m.get(annoKeyFullPath)
} }
func (m *grafanaMetaAccessor) SetFullPath(path string) { func (m *grafanaMetaAccessor) SetFullPath(path string) {
m.SetAnnotation(AnnoKeyFullPath, path) // nolint:staticcheck
m.SetAnnotation(annoKeyFullPath, path)
} }
func (m *grafanaMetaAccessor) GetFullPathUIDs() string { func (m *grafanaMetaAccessor) GetFullPathUIDs() string {
return m.get(AnnoKeyFullPathUIDs) // nolint:staticcheck
return m.get(annoKeyFullPathUIDs)
} }
func (m *grafanaMetaAccessor) SetFullPathUIDs(path string) { func (m *grafanaMetaAccessor) SetFullPathUIDs(path string) {
m.SetAnnotation(AnnoKeyFullPathUIDs, path) // nolint:staticcheck
m.SetAnnotation(annoKeyFullPathUIDs, path)
} }
func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string { func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string {

View File

@ -4,11 +4,12 @@ import (
"encoding/json" "encoding/json"
"testing" "testing"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"github.com/grafana/grafana/pkg/apimachinery/utils"
) )
type TestResource struct { type TestResource struct {
@ -129,7 +130,7 @@ func (in *Spec2) DeepCopy() *Spec2 {
} }
func TestMetaAccessor(t *testing.T) { func TestMetaAccessor(t *testing.T) {
originInfo := &utils.ResourceOriginInfo{ repoInfo := &utils.ResourceRepositoryInfo{
Name: "test", Name: "test",
Path: "a/b/c", Path: "a/b/c",
Hash: "kkk", Hash: "kkk",
@ -177,13 +178,13 @@ func TestMetaAccessor(t *testing.T) {
}, },
} }
meta.SetOriginInfo(originInfo) meta.SetRepositoryInfo(repoInfo)
meta.SetFolder("folderUID") meta.SetFolder("folderUID")
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"grafana.app/originName": "test", "grafana.app/repoName": "test",
"grafana.app/originPath": "a/b/c", "grafana.app/repoPath": "a/b/c",
"grafana.app/originHash": "kkk", "grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID", "grafana.app/folder": "folderUID",
}, res.GetAnnotations()) }, res.GetAnnotations())
@ -229,13 +230,13 @@ func TestMetaAccessor(t *testing.T) {
meta, err := utils.MetaAccessor(res) meta, err := utils.MetaAccessor(res)
require.NoError(t, err) require.NoError(t, err)
meta.SetOriginInfo(originInfo) meta.SetRepositoryInfo(repoInfo)
meta.SetFolder("folderUID") meta.SetFolder("folderUID")
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"grafana.app/originName": "test", "grafana.app/repoName": "test",
"grafana.app/originPath": "a/b/c", "grafana.app/repoPath": "a/b/c",
"grafana.app/originHash": "kkk", "grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID", "grafana.app/folder": "folderUID",
}, res.GetAnnotations()) }, res.GetAnnotations())
@ -280,13 +281,13 @@ func TestMetaAccessor(t *testing.T) {
meta, err := utils.MetaAccessor(res) meta, err := utils.MetaAccessor(res)
require.NoError(t, err) require.NoError(t, err)
meta.SetOriginInfo(originInfo) meta.SetRepositoryInfo(repoInfo)
meta.SetFolder("folderUID") meta.SetFolder("folderUID")
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"grafana.app/originName": "test", "grafana.app/repoName": "test",
"grafana.app/originPath": "a/b/c", "grafana.app/repoPath": "a/b/c",
"grafana.app/originHash": "kkk", "grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID", "grafana.app/folder": "folderUID",
}, res.GetAnnotations()) }, res.GetAnnotations())
@ -321,6 +322,28 @@ func TestMetaAccessor(t *testing.T) {
require.Equal(t, "ZZ", res.Status.Title) require.Equal(t, "ZZ", res.Status.Title)
}) })
t.Run("test reading old originInfo (now repository)", func(t *testing.T) {
res := &TestResource2{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"grafana.app/repoName": "test",
"grafana.app/repoPath": "a/b/c",
"grafana.app/repoHash": "zzz",
"grafana.app/folder": "folderUID",
},
},
Spec: Spec2{},
}
meta, err := utils.MetaAccessor(res)
require.NoError(t, err)
info, err := meta.GetRepositoryInfo()
require.NoError(t, err)
require.Equal(t, "test", info.Name)
require.Equal(t, "a/b/c", info.Path)
require.Equal(t, "zzz", info.Hash)
})
t.Run("blob info", func(t *testing.T) { t.Run("blob info", func(t *testing.T) {
info := &utils.BlobInfo{UID: "AAA", Size: 123, Hash: "xyz", MimeType: "application/json", Charset: "utf-8"} info := &utils.BlobInfo{UID: "AAA", Size: 123, Hash: "xyz", MimeType: "application/json", Charset: "utf-8"}
anno := info.String() anno := info.String()
@ -343,13 +366,13 @@ func TestMetaAccessor(t *testing.T) {
meta, err := utils.MetaAccessor(obj) meta, err := utils.MetaAccessor(obj)
require.NoError(t, err) require.NoError(t, err)
meta.SetOriginInfo(originInfo) meta.SetRepositoryInfo(repoInfo)
meta.SetFolder("folderUID") meta.SetFolder("folderUID")
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"grafana.app/originName": "test", "grafana.app/repoName": "test",
"grafana.app/originPath": "a/b/c", "grafana.app/repoPath": "a/b/c",
"grafana.app/originHash": "kkk", "grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID", "grafana.app/folder": "folderUID",
}, obj.GetAnnotations()) }, obj.GetAnnotations())
@ -366,13 +389,13 @@ func TestMetaAccessor(t *testing.T) {
meta, err = utils.MetaAccessor(obj2) meta, err = utils.MetaAccessor(obj2)
require.NoError(t, err) require.NoError(t, err)
meta.SetOriginInfo(originInfo) meta.SetRepositoryInfo(repoInfo)
meta.SetFolder("folderUID") meta.SetFolder("folderUID")
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"grafana.app/originName": "test", "grafana.app/repoName": "test",
"grafana.app/originPath": "a/b/c", "grafana.app/repoPath": "a/b/c",
"grafana.app/originHash": "kkk", "grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID", "grafana.app/folder": "folderUID",
}, obj2.GetAnnotations()) }, obj2.GetAnnotations())

View File

@ -10,10 +10,11 @@ import (
"sync" "sync"
"time" "time"
"github.com/grafana/authlib/claims"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr" "k8s.io/utils/ptr"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apimachinery/utils"
@ -286,14 +287,14 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
return nil, err return nil, err
} }
meta.SetOriginInfo(&utils.ResourceOriginInfo{ meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
Name: origin_name.String, Name: origin_name.String,
Path: originPath, Path: originPath,
Hash: origin_hash.String, Hash: origin_hash.String,
Timestamp: &ts, Timestamp: &ts,
}) })
} else if plugin_id != "" { } else if plugin_id != "" {
meta.SetOriginInfo(&utils.ResourceOriginInfo{ meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
Name: "plugin", Name: "plugin",
Path: plugin_id, Path: plugin_id,
}) })
@ -516,7 +517,7 @@ func (a *dashboardSqlAccess) GetLibraryPanels(ctx context.Context, query Library
meta.SetCreatedBy(p.CreatedBy) meta.SetCreatedBy(p.CreatedBy)
meta.SetUpdatedBy(p.UpdatedBy) meta.SetUpdatedBy(p.UpdatedBy)
meta.SetUpdatedTimestamp(&p.Updated) meta.SetUpdatedTimestamp(&p.Updated)
meta.SetOriginInfo(&utils.ResourceOriginInfo{ meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
Name: "SQL", Name: "SQL",
Path: strconv.FormatInt(p.ID, 10), Path: strconv.FormatInt(p.ID, 10),
}) })

View File

@ -108,12 +108,12 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob
UID: name, UID: name,
OrgID: info.OrgID, OrgID: info.OrgID,
} }
origin, err := obj.GetOriginInfo() repo, err := obj.GetRepositoryInfo()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if origin != nil && origin.Name == "SQL" { if repo != nil && repo.Name == "SQL" {
dto.ID, err = strconv.ParseInt(origin.Path, 10, 64) dto.ID, err = strconv.ParseInt(repo.Path, 10, 64)
if err == nil { if err == nil {
return nil, err return nil, err
} }

View File

@ -144,7 +144,7 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper)
meta.SetUpdatedTimestamp(&v.Updated) meta.SetUpdatedTimestamp(&v.Updated)
if v.ID > 0 { // nolint:staticcheck if v.ID > 0 { // nolint:staticcheck
meta.SetOriginInfo(&utils.ResourceOriginInfo{ meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
Name: "SQL", Name: "SQL",
Path: fmt.Sprintf("%d", v.ID), // nolint:staticcheck Path: fmt.Sprintf("%d", v.ID), // nolint:staticcheck
Timestamp: &v.Created, Timestamp: &v.Created,
@ -187,13 +187,13 @@ func setParentUID(u *unstructured.Unstructured, parentUid string) error {
func getLegacyID(meta utils.GrafanaMetaAccessor) (int64, error) { func getLegacyID(meta utils.GrafanaMetaAccessor) (int64, error) {
var i int64 var i int64
info, err := meta.GetOriginInfo() repo, err := meta.GetRepositoryInfo()
if err != nil { if err != nil {
return i, err return i, err
} }
if info != nil && info.Name == "SQL" { if repo != nil && repo.Name == "SQL" {
i, err = strconv.ParseInt(info.Path, 10, 64) i, err = strconv.ParseInt(repo.Path, 10, 64)
if err != nil { if err != nil {
return i, err return i, err
} }
@ -208,7 +208,7 @@ func getURL(meta utils.GrafanaMetaAccessor, title string) string {
} }
func getCreated(meta utils.GrafanaMetaAccessor) (*time.Time, error) { func getCreated(meta utils.GrafanaMetaAccessor) (*time.Time, error) {
created, err := meta.GetOriginTimestamp() created, err := meta.GetRepositoryTimestamp()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -110,7 +110,7 @@ func toSAItem(sa legacy.ServiceAccount, ns string) iamv0.ServiceAccount {
} }
obj, _ := utils.MetaAccessor(&item) obj, _ := utils.MetaAccessor(&item)
obj.SetUpdatedTimestamp(&sa.Updated) obj.SetUpdatedTimestamp(&sa.Updated)
obj.SetOriginInfo(&utils.ResourceOriginInfo{ obj.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
Name: "SQL", Name: "SQL",
Path: strconv.FormatInt(sa.ID, 10), Path: strconv.FormatInt(sa.ID, 10),
}) })

View File

@ -136,7 +136,7 @@ func toTeamObject(t team.Team, ns claims.NamespaceInfo) iamv0.Team {
} }
meta, _ := utils.MetaAccessor(&obj) meta, _ := utils.MetaAccessor(&obj)
meta.SetUpdatedTimestamp(&t.Updated) meta.SetUpdatedTimestamp(&t.Updated)
meta.SetOriginInfo(&utils.ResourceOriginInfo{ meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
Name: "SQL", Name: "SQL",
Path: strconv.FormatInt(t.ID, 10), Path: strconv.FormatInt(t.ID, 10),
}) })

View File

@ -137,7 +137,7 @@ func toUserItem(u *user.User, ns string) iamv0.User {
} }
obj, _ := utils.MetaAccessor(item) obj, _ := utils.MetaAccessor(item)
obj.SetUpdatedTimestamp(&u.Updated) obj.SetUpdatedTimestamp(&u.Updated)
obj.SetOriginInfo(&utils.ResourceOriginInfo{ obj.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
Name: "SQL", Name: "SQL",
Path: strconv.FormatInt(u.ID, 10), Path: strconv.FormatInt(u.ID, 10),
}) })

View File

@ -97,7 +97,7 @@ func convertToK8sResource(v *playlistsvc.PlaylistDTO, namespacer request.Namespa
meta.SetUpdatedTimestampMillis(v.UpdatedAt) meta.SetUpdatedTimestampMillis(v.UpdatedAt)
if v.Id > 0 { if v.Id > 0 {
createdAt := time.UnixMilli(v.CreatedAt) createdAt := time.UnixMilli(v.CreatedAt)
meta.SetOriginInfo(&utils.ResourceOriginInfo{ meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
Name: "SQL", Name: "SQL",
Path: fmt.Sprintf("%d", v.Id), Path: fmt.Sprintf("%d", v.Id),
Timestamp: &createdAt, Timestamp: &createdAt,
@ -135,7 +135,7 @@ func getLegacyID(item *unstructured.Unstructured) int64 {
if err != nil { if err != nil {
return 0 return 0
} }
info, _ := meta.GetOriginInfo() info, _ := meta.GetRepositoryInfo()
if info != nil && info.Name == "SQL" { if info != nil && info.Name == "SQL" {
i, err := strconv.ParseInt(info.Path, 10, 64) i, err := strconv.ParseInt(info.Path, 10, 64)
if err == nil { if err == nil {

View File

@ -42,9 +42,9 @@ func TestPlaylistConversion(t *testing.T) {
"resourceVersion": "54321", "resourceVersion": "54321",
"creationTimestamp": "1970-01-01T00:00:12Z", "creationTimestamp": "1970-01-01T00:00:12Z",
"annotations": { "annotations": {
"grafana.app/originPath": "123", "grafana.app/repoPath": "123",
"grafana.app/originName": "SQL", "grafana.app/repoName": "SQL",
"grafana.app/originTimestamp":"1970-01-01T00:00:12Z", "grafana.app/repoTimestamp":"1970-01-01T00:00:12Z",
"grafana.app/updatedTimestamp": "1970-01-01T00:00:54Z" "grafana.app/updatedTimestamp": "1970-01-01T00:00:54Z"
} }
}, },

View File

@ -60,12 +60,12 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime
obj.SetResourceVersion("") obj.SetResourceVersion("")
obj.SetSelfLink("") obj.SetSelfLink("")
// Read+write will verify that origin format is accurate // Read+write will verify that repository format is accurate
origin, err := obj.GetOriginInfo() repo, err := obj.GetRepositoryInfo()
if err != nil { if err != nil {
return nil, err return nil, err
} }
obj.SetOriginInfo(origin) obj.SetRepositoryInfo(repo)
obj.SetUpdatedBy("") obj.SetUpdatedBy("")
obj.SetUpdatedTimestamp(nil) obj.SetUpdatedTimestamp(nil)
obj.SetCreatedBy(user.GetUID()) obj.SetCreatedBy(user.GetUID())
@ -115,11 +115,11 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti
obj.SetResourceVersion("") // removed from saved JSON because the RV is not yet calculated obj.SetResourceVersion("") // removed from saved JSON because the RV is not yet calculated
// Read+write will verify that origin format is accurate // Read+write will verify that origin format is accurate
origin, err := obj.GetOriginInfo() repo, err := obj.GetRepositoryInfo()
if err != nil { if err != nil {
return nil, err return nil, err
} }
obj.SetOriginInfo(origin) obj.SetRepositoryInfo(repo)
obj.SetUpdatedBy(user.GetUID()) obj.SetUpdatedBy(user.GetUID())
obj.SetUpdatedTimestampMillis(time.Now().UnixMilli()) obj.SetUpdatedTimestampMillis(time.Now().UnixMilli())

View File

@ -9,7 +9,7 @@ import (
type WriteAccessHooks struct { type WriteAccessHooks struct {
// When configured, this will make sure a user is allowed to save to a given origin // When configured, this will make sure a user is allowed to save to a given origin
Origin func(ctx context.Context, user claims.AuthInfo, origin string) bool CanWriteValueFromRepoCheck func(ctx context.Context, user claims.AuthInfo, origin string) bool
} }
type LifecycleHooks interface { type LifecycleHooks interface {
@ -20,11 +20,11 @@ type LifecycleHooks interface {
Stop(context.Context) error Stop(context.Context) error
} }
func (a *WriteAccessHooks) CanWriteOrigin(ctx context.Context, user claims.AuthInfo, uid string) error { func (a *WriteAccessHooks) CanWriteValueFromRepository(ctx context.Context, user claims.AuthInfo, uid string) error {
if a.Origin == nil || uid == "UI" { if a.CanWriteValueFromRepoCheck == nil || uid == "UI" {
return nil // default to OK return nil // default to OK
} }
if !a.Origin(ctx, user, uid) { if !a.CanWriteValueFromRepoCheck(ctx, user, uid) {
return fmt.Errorf("not allowed to write resource at origin") return fmt.Errorf("not allowed to write resource at origin")
} }
return nil return nil

View File

@ -394,12 +394,12 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour
} }
} }
origin, err := obj.GetOriginInfo() repo, err := obj.GetRepositoryInfo()
if err != nil { if err != nil {
return nil, NewBadRequestError("invalid origin info") return nil, NewBadRequestError("invalid repository info")
} }
if origin != nil { if repo != nil {
err = s.writeHooks.CanWriteOrigin(ctx, user, origin.Name) err = s.writeHooks.CanWriteValueFromRepository(ctx, user, repo.Name)
if err != nil { if err != nil {
return nil, AsErrorResult(err) return nil, AsErrorResult(err)
} }

View File

@ -60,9 +60,9 @@ func TestSimpleServer(t *testing.T) {
"uid": "xyz", "uid": "xyz",
"namespace": "default", "namespace": "default",
"annotations": { "annotations": {
"grafana.app/originName": "elsewhere", "grafana.app/repoName": "elsewhere",
"grafana.app/originPath": "path/to/item", "grafana.app/repoPath": "path/to/item",
"grafana.app/originTimestamp": "2024-02-02T00:00:00Z" "grafana.app/repoTimestamp": "2024-02-02T00:00:00Z"
} }
}, },
"spec": { "spec": {
@ -176,9 +176,9 @@ func TestSimpleServer(t *testing.T) {
"namespace": "default", "namespace": "default",
"uid": "xyz", "uid": "xyz",
"annotations": { "annotations": {
"grafana.app/originName": "elsewhere", "grafana.app/repoName": "elsewhere",
"grafana.app/originPath": "path/to/item", "grafana.app/repoPath": "path/to/item",
"grafana.app/originTimestamp": "2024-02-02T00:00:00Z" "grafana.app/repoTimestamp": "2024-02-02T00:00:00Z"
} }
}, },
"spec": { "spec": {

View File

@ -8,9 +8,9 @@
"creationTimestamp": "2024-10-30T18:30:54Z", "creationTimestamp": "2024-10-30T18:30:54Z",
"annotations": { "annotations": {
"grafana.app/createdBy": "user:be2g71ke8yoe8b", "grafana.app/createdBy": "user:be2g71ke8yoe8b",
"grafana.app/originHash": "Grafana v9.2.0 (NA)", "grafana.app/repoHash": "Grafana v9.2.0 (NA)",
"grafana.app/originName": "UI", "grafana.app/repoName": "UI",
"grafana.app/originPath": "/dashboard/new" "grafana.app/repoPath": "/dashboard/new"
}, },
"managedFields": [ "managedFields": [
{ {
@ -23,9 +23,9 @@
"f:metadata": { "f:metadata": {
"f:annotations": { "f:annotations": {
".": {}, ".": {},
"f:grafana.app/originHash": {}, "f:grafana.app/repoHash": {},
"f:grafana.app/originName": {}, "f:grafana.app/repoName": {},
"f:grafana.app/originPath": {} "f:grafana.app/repoPath": {}
}, },
"f:generateName": {} "f:generateName": {}
}, },

View File

@ -7,10 +7,7 @@
"uid": "86ab200a-e8b0-47ce-bbc1-8c2e078b0956", "uid": "86ab200a-e8b0-47ce-bbc1-8c2e078b0956",
"creationTimestamp": "2024-10-30T20:24:07Z", "creationTimestamp": "2024-10-30T20:24:07Z",
"annotations": { "annotations": {
"grafana.app/createdBy": "user:be2g71ke8yoe8b", "grafana.app/createdBy": "user:be2g71ke8yoe8b"
"grafana.app/originHash": "Grafana v9.2.0 (NA)",
"grafana.app/originName": "UI",
"grafana.app/originPath": "/dashboard/new"
}, },
"managedFields": [ "managedFields": [
{ {

View File

@ -7,10 +7,7 @@
"uid": "86ab200a-e8b0-47ce-bbc1-8c2e078b0956-2", "uid": "86ab200a-e8b0-47ce-bbc1-8c2e078b0956-2",
"creationTimestamp": "2024-10-30T20:24:07Z", "creationTimestamp": "2024-10-30T20:24:07Z",
"annotations": { "annotations": {
"grafana.app/createdBy": "user:be2g71ke8yoe8b", "grafana.app/createdBy": "user:be2g71ke8yoe8b"
"grafana.app/originHash": "Grafana v9.2.0 (NA)",
"grafana.app/originName": "UI",
"grafana.app/originPath": "/dashboard/new"
}, },
"managedFields": [ "managedFields": [
{ {

View File

@ -7,10 +7,7 @@
"uid": "aaaa-bbbb", "uid": "aaaa-bbbb",
"creationTimestamp": "2024-11-01T19:42:22Z", "creationTimestamp": "2024-11-01T19:42:22Z",
"annotations": { "annotations": {
"grafana.app/createdBy": "user:1", "grafana.app/createdBy": "user:1"
"grafana.app/originName": "SQL",
"grafana.app/originPath": "15",
"grafana.app/originTimestamp": "2024-11-01T19:42:22Z"
} }
}, },
"spec": { "spec": {

View File

@ -8,9 +8,9 @@
"creationTimestamp": "2024-11-01T19:42:22Z", "creationTimestamp": "2024-11-01T19:42:22Z",
"annotations": { "annotations": {
"grafana.app/createdBy": "user:1", "grafana.app/createdBy": "user:1",
"grafana.app/originName": "SQL", "grafana.app/repoName": "SQL",
"grafana.app/originPath": "15", "grafana.app/repoPath": "15",
"grafana.app/originTimestamp": "2024-11-01T19:42:22Z" "grafana.app/repoTimestamp": "2024-11-01T19:42:22Z"
} }
}, },
"spec": { "spec": {

View File

@ -8,9 +8,9 @@
"creationTimestamp": "2024-11-01T19:42:22Z", "creationTimestamp": "2024-11-01T19:42:22Z",
"annotations": { "annotations": {
"grafana.app/createdBy": "user:1", "grafana.app/createdBy": "user:1",
"grafana.app/originName": "SQL", "grafana.app/repoName": "SQL",
"grafana.app/originPath": "15", "grafana.app/repoPath": "15",
"grafana.app/originTimestamp": "2024-11-01T19:42:22Z" "grafana.app/repoTimestamp": "2024-11-01T19:42:22Z"
} }
}, },
"spec": { "spec": {

View File

@ -11,9 +11,6 @@ import {
ResourceList, ResourceList,
ResourceClient, ResourceClient,
ObjectMeta, ObjectMeta,
AnnoKeyOriginPath,
AnnoKeyOriginHash,
AnnoKeyOriginName,
K8sAPIGroupList, K8sAPIGroupList,
} from './types'; } from './types';
@ -55,12 +52,12 @@ export class ScopedResourceClient<T = object, K = string> implements ResourceCli
// THe passed in value is the suggested prefix // THe passed in value is the suggested prefix
obj.metadata.generateName = login ? login.slice(0, 2) : 'g'; obj.metadata.generateName = login ? login.slice(0, 2) : 'g';
} }
setOriginAsUI(obj.metadata); setSavedFromUIAnnotation(obj.metadata);
return getBackendSrv().post(this.url, obj); return getBackendSrv().post(this.url, obj);
} }
public async update(obj: Resource<T, K>): Promise<Resource<T, K>> { public async update(obj: Resource<T, K>): Promise<Resource<T, K>> {
setOriginAsUI(obj.metadata); setSavedFromUIAnnotation(obj.metadata);
return getBackendSrv().put<Resource<T, K>>(`${this.url}/${obj.metadata.name}`, obj); return getBackendSrv().put<Resource<T, K>>(`${this.url}/${obj.metadata.name}`, obj);
} }
@ -103,13 +100,11 @@ export class ScopedResourceClient<T = object, K = string> implements ResourceCli
} }
// add the origin annotations so we know what was set from the UI // add the origin annotations so we know what was set from the UI
function setOriginAsUI(meta: Partial<ObjectMeta>) { function setSavedFromUIAnnotation(meta: Partial<ObjectMeta>) {
if (!meta.annotations) { if (!meta.annotations) {
meta.annotations = {}; meta.annotations = {};
} }
meta.annotations[AnnoKeyOriginName] = 'UI'; meta.annotations['grafana.app/saved-from-ui'] = config.buildInfo.versionString;
meta.annotations[AnnoKeyOriginPath] = window.location.pathname;
meta.annotations[AnnoKeyOriginHash] = config.buildInfo.versionString;
} }
export class DatasourceAPIVersions { export class DatasourceAPIVersions {

View File

@ -37,10 +37,10 @@ export const AnnoKeyMessage = 'grafana.app/message';
export const AnnoKeySlug = 'grafana.app/slug'; export const AnnoKeySlug = 'grafana.app/slug';
// Identify where values came from // Identify where values came from
export const AnnoKeyOriginName = 'grafana.app/originName'; export const AnnoKeyRepoName = 'grafana.app/repoName';
export const AnnoKeyOriginPath = 'grafana.app/originPath'; export const AnnoKeyRepoPath = 'grafana.app/repoPath';
export const AnnoKeyOriginHash = 'grafana.app/originHash'; export const AnnoKeyRepoHash = 'grafana.app/repoHash';
const AnnoKeyOriginTimestamp = 'grafana.app/originTimestamp'; const AnnoKeyRepoTimestamp = 'grafana.app/repoTimestamp';
type GrafanaAnnotations = { type GrafanaAnnotations = {
[AnnoKeyCreatedBy]?: string; [AnnoKeyCreatedBy]?: string;
@ -49,10 +49,10 @@ type GrafanaAnnotations = {
[AnnoKeyFolder]?: string; [AnnoKeyFolder]?: string;
[AnnoKeySlug]?: string; [AnnoKeySlug]?: string;
[AnnoKeyOriginName]?: string; [AnnoKeyRepoName]?: string;
[AnnoKeyOriginPath]?: string; [AnnoKeyRepoPath]?: string;
[AnnoKeyOriginHash]?: string; [AnnoKeyRepoHash]?: string;
[AnnoKeyOriginTimestamp]?: string; [AnnoKeyRepoTimestamp]?: string;
// Any key value // Any key value
[key: string]: string | undefined; [key: string]: string | undefined;