K8s/Annotations: Use manager/source annotations rather than repo (#101313)

Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
This commit is contained in:
Ryan McKinley
2025-03-05 08:54:20 +02:00
committed by GitHub
co-authored by Stephanie Hingtgen
parent e7baf9804e
commit dc2defd84f
37 changed files with 589 additions and 573 deletions
+18 -10
View File
@@ -1,28 +1,27 @@
package utils
import "time"
// ManagerProperties is used to identify the manager of the resource.
type ManagerProperties struct {
// The kind of manager, which is responsible for managing the resource.
// Examples include "git", "terraform", "kubectl", etc.
Kind ManagerKind
Kind ManagerKind `json:"kind,omitempty"`
// The identity of the manager, which refers to a specific instance of the manager.
// The format & the value depends on the manager kind.
Identity string
Identity string `json:"id,omitempty"`
// AllowsEdits indicates whether the manager allows edits to the resource.
// If set to true, it means that other requesters can edit the resource.
AllowsEdits bool
AllowsEdits bool `json:"allowEdits,omitempty"`
// Suspended indicates whether the manager is suspended.
// If set to true, then the manager skip updates to the resource.
Suspended bool
Suspended bool `json:"suspended,omitempty"`
}
// ManagerKind is the type of manager, which is responsible for managing the resource.
// It can be a user or a tool or a generic API client.
// +enum
type ManagerKind string
// Known values for ManagerKind.
@@ -31,6 +30,11 @@ const (
ManagerKindRepo ManagerKind = "repo"
ManagerKindTerraform ManagerKind = "terraform"
ManagerKindKubectl ManagerKind = "kubectl"
ManagerKindPlugin ManagerKind = "plugin"
// Deprecated: this is used as a shim/migration path for legacy file provisioning
// Previously this was a "file:" prefix
ManagerKindClassicFP ManagerKind = "classic-file-provisioning"
)
// ParseManagerKindString parses a string into a ManagerKind.
@@ -44,6 +48,10 @@ func ParseManagerKindString(v string) ManagerKind {
return ManagerKindTerraform
case string(ManagerKindKubectl):
return ManagerKindKubectl
case string(ManagerKindPlugin):
return ManagerKindPlugin
case string(ManagerKindClassicFP): // nolint:staticcheck
return ManagerKindClassicFP // nolint:staticcheck
default:
return ManagerKindUnknown
}
@@ -55,13 +63,13 @@ func ParseManagerKindString(v string) ManagerKind {
type SourceProperties struct {
// The path to the source of the resource.
// Can be a file path, a URL, etc.
Path string
Path string `json:"path,omitempty"`
// The checksum of the source of the resource.
// An example could be a git commit hash.
Checksum string
Checksum string `json:"checksum,omitempty"`
// The timestamp of the source of the resource.
// The unix millis timestamp of the source of the resource.
// An example could be the file modification time.
Timestamp time.Time
TimestampMillis int64 `json:"timestampMillis,omitempty"`
}
+64 -136
View File
@@ -41,10 +41,10 @@ const AnnoKeyMessage = "grafana.app/message"
// Identify where values came from
const AnnoKeyRepoName = "grafana.app/repoName"
const AnnoKeyRepoPath = "grafana.app/repoPath"
const AnnoKeyRepoHash = "grafana.app/repoHash"
const AnnoKeyRepoTimestamp = "grafana.app/repoTimestamp"
const oldAnnoKeyRepoName = "grafana.app/repoName"
const oldAnnoKeyRepoPath = "grafana.app/repoPath"
const oldAnnoKeyRepoHash = "grafana.app/repoHash"
const oldAnnoKeyRepoTimestamp = "grafana.app/repoTimestamp"
// Annotations used to store manager properties
@@ -56,41 +56,13 @@ const AnnoKeyManagerSuspended = "grafana.app/managerSuspended"
// Annotations used to store source properties
const AnnoKeySourcePath = "grafana.app/sourcePath"
const AnnoKeySourceHash = "grafana.app/sourceHash"
const AnnoKeySourceChecksum = "grafana.app/sourceChecksum"
const AnnoKeySourceTimestamp = "grafana.app/sourceTimestamp"
// LabelKeyDeprecatedInternalID gives the deprecated internal ID of a resource
// Deprecated: will be removed in grafana 13
const LabelKeyDeprecatedInternalID = "grafana.app/deprecatedInternalID"
// 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"
// 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"`
// The path within the named repository above (external_id in the existing dashboard provisioning)
Path string `json:"path,omitempty"`
// Verification/identification hash (check_sum in existing dashboard provisioning)
Hash string `json:"hash,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 GrafanaMetaAccessor interface {
metav1.Object
@@ -128,13 +100,6 @@ type GrafanaMetaAccessor interface {
// Deprecated: This will be removed in Grafana 13
SetDeprecatedInternalID(id int64)
GetRepositoryInfo() (*ResourceRepositoryInfo, error)
SetRepositoryInfo(info *ResourceRepositoryInfo)
GetRepositoryName() string
GetRepositoryPath() string
GetRepositoryHash() string
GetRepositoryTimestamp() (*time.Time, error)
GetSpec() (any, error)
SetSpec(any) error
@@ -351,90 +316,6 @@ func (m *grafanaMetaAccessor) SetDeprecatedInternalID(id int64) {
m.obj.SetLabels(labels)
}
// 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()
if anno == nil {
if info == nil {
return
}
anno = make(map[string]string, 0)
}
// remove legacy values
delete(anno, oldAnnoKeyOriginHash)
delete(anno, oldAnnoKeyOriginPath)
delete(anno, oldAnnoKeyOriginHash)
delete(anno, oldAnnoKeyOriginTimestamp)
delete(anno, AnnoKeyRepoName)
delete(anno, AnnoKeyRepoPath)
delete(anno, AnnoKeyRepoHash)
delete(anno, AnnoKeyRepoTimestamp)
if info != nil && info.Name != "" {
anno[AnnoKeyRepoName] = info.Name
if info.Path != "" {
anno[AnnoKeyRepoPath] = info.Path
}
if info.Hash != "" {
anno[AnnoKeyRepoHash] = info.Hash
}
if info.Timestamp != nil {
anno[AnnoKeyRepoTimestamp] = info.Timestamp.UTC().Format(time.RFC3339)
}
}
m.obj.SetAnnotations(anno)
}
func (m *grafanaMetaAccessor) GetRepositoryInfo() (*ResourceRepositoryInfo, error) {
v, ok := m.getAnnoValue(AnnoKeyRepoName, oldAnnoKeyOriginName)
if !ok {
return nil, nil
}
t, err := m.GetRepositoryTimestamp()
return &ResourceRepositoryInfo{
Name: v,
Path: m.GetRepositoryPath(),
Hash: m.GetRepositoryHash(),
Timestamp: t,
}, err
}
func (m *grafanaMetaAccessor) GetRepositoryName() string {
v, _ := m.getAnnoValue(AnnoKeyRepoName, oldAnnoKeyOriginName)
return v // will be empty string
}
func (m *grafanaMetaAccessor) GetRepositoryPath() string {
v, _ := m.getAnnoValue(AnnoKeyRepoPath, oldAnnoKeyOriginPath)
return v // will be empty string
}
func (m *grafanaMetaAccessor) GetRepositoryHash() string {
v, _ := m.getAnnoValue(AnnoKeyRepoHash, oldAnnoKeyOriginHash)
return v // will be empty string
}
func (m *grafanaMetaAccessor) GetRepositoryTimestamp() (*time.Time, error) {
v, ok := m.getAnnoValue(AnnoKeyRepoTimestamp, oldAnnoKeyOriginTimestamp)
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
}
// GetAnnotations implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetAnnotations() map[string]string {
return m.obj.GetAnnotations()
@@ -751,7 +632,7 @@ func (m *grafanaMetaAccessor) GetManagerProperties() (ManagerProperties, bool) {
res := ManagerProperties{
Identity: "",
Kind: ManagerKindUnknown,
AllowsEdits: true,
AllowsEdits: false,
Suspended: false,
}
@@ -759,6 +640,15 @@ func (m *grafanaMetaAccessor) GetManagerProperties() (ManagerProperties, bool) {
id, ok := annot[AnnoKeyManagerIdentity]
if !ok || id == "" {
// Temporarily support the repo name annotation
repo := annot[oldAnnoKeyRepoName]
if repo != "" {
return ManagerProperties{
Kind: ManagerKindRepo,
Identity: repo,
}, true
}
// If the identity is not set, we should ignore the other annotations and return the default values.
//
// This is to prevent inadvertently marking resources as managed,
@@ -788,10 +678,31 @@ func (m *grafanaMetaAccessor) SetManagerProperties(v ManagerProperties) {
annot = make(map[string]string, 4)
}
annot[AnnoKeyManagerIdentity] = v.Identity
annot[AnnoKeyManagerKind] = string(v.Kind)
annot[AnnoKeyManagerAllowsEdits] = strconv.FormatBool(v.AllowsEdits)
annot[AnnoKeyManagerSuspended] = strconv.FormatBool(v.Suspended)
if v.Identity != "" {
annot[AnnoKeyManagerIdentity] = v.Identity
} else {
delete(annot, AnnoKeyManagerIdentity)
}
if string(v.Kind) != "" {
annot[AnnoKeyManagerKind] = string(v.Kind)
} else {
delete(annot, AnnoKeyManagerKind)
}
if v.AllowsEdits {
annot[AnnoKeyManagerAllowsEdits] = strconv.FormatBool(v.AllowsEdits)
} else {
delete(annot, AnnoKeyManagerAllowsEdits)
}
if v.Suspended {
annot[AnnoKeyManagerSuspended] = strconv.FormatBool(v.Suspended)
} else {
delete(annot, AnnoKeyManagerSuspended)
}
// Clean up old annotation access
delete(annot, oldAnnoKeyRepoName)
m.obj.SetAnnotations(annot)
}
@@ -810,16 +721,27 @@ func (m *grafanaMetaAccessor) GetSourceProperties() (SourceProperties, bool) {
if path, ok := annot[AnnoKeySourcePath]; ok && path != "" {
res.Path = path
found = true
} else if path, ok := annot[oldAnnoKeyRepoPath]; ok && path != "" {
res.Path = path
found = true
}
if hash, ok := annot[AnnoKeySourceHash]; ok && hash != "" {
if hash, ok := annot[AnnoKeySourceChecksum]; ok && hash != "" {
res.Checksum = hash
found = true
} else if hash, ok := annot[oldAnnoKeyRepoHash]; ok && hash != "" {
res.Checksum = hash
found = true
}
if timestamp, ok := annot[AnnoKeySourceTimestamp]; ok && timestamp != "" {
if t, err := time.Parse(time.RFC3339, timestamp); err == nil {
res.Timestamp = t
t, ok := annot[AnnoKeySourceTimestamp]
if !ok {
t, ok = annot[oldAnnoKeyRepoTimestamp]
}
if ok && t != "" {
var err error
res.TimestampMillis, err = strconv.ParseInt(t, 10, 64)
if err != nil {
found = true
}
}
@@ -835,14 +757,20 @@ func (m *grafanaMetaAccessor) SetSourceProperties(v SourceProperties) {
if v.Path != "" {
annot[AnnoKeySourcePath] = v.Path
} else {
delete(annot, AnnoKeySourcePath)
}
if v.Checksum != "" {
annot[AnnoKeySourceHash] = v.Checksum
annot[AnnoKeySourceChecksum] = v.Checksum
} else {
delete(annot, AnnoKeySourceChecksum)
}
if !v.Timestamp.IsZero() {
annot[AnnoKeySourceTimestamp] = v.Timestamp.Format(time.RFC3339)
if v.TimestampMillis > 0 {
annot[AnnoKeySourceTimestamp] = strconv.FormatInt(v.TimestampMillis, 10)
} else {
delete(annot, AnnoKeySourceTimestamp)
}
m.obj.SetAnnotations(annot)
+51 -43
View File
@@ -131,10 +131,13 @@ func (in *Spec2) DeepCopy() *Spec2 {
}
func TestMetaAccessor(t *testing.T) {
repoInfo := &utils.ResourceRepositoryInfo{
Name: "test",
Path: "a/b/c",
Hash: "kkk",
repoInfo := utils.ManagerProperties{
Kind: utils.ManagerKindRepo,
Identity: "test",
}
sourceInfo := utils.SourceProperties{
Path: "a/b/c",
Checksum: "kkk",
}
t.Run("fails for non resource objects", func(t *testing.T) {
@@ -202,14 +205,13 @@ func TestMetaAccessor(t *testing.T) {
},
}
meta.SetRepositoryInfo(repoInfo)
meta.SetManagerProperties(repoInfo)
meta.SetFolder("folderUID")
require.Equal(t, map[string]string{
"grafana.app/repoName": "test",
"grafana.app/repoPath": "a/b/c",
"grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID",
"grafana.app/managedBy": "repo",
"grafana.app/managerId": "test",
"grafana.app/folder": "folderUID",
}, res.GetAnnotations())
meta.SetNamespace("aaa")
@@ -255,14 +257,16 @@ func TestMetaAccessor(t *testing.T) {
meta, err := utils.MetaAccessor(res)
require.NoError(t, err)
meta.SetRepositoryInfo(repoInfo)
meta.SetManagerProperties(repoInfo)
meta.SetSourceProperties(sourceInfo)
meta.SetFolder("folderUID")
require.Equal(t, map[string]string{
"grafana.app/repoName": "test",
"grafana.app/repoPath": "a/b/c",
"grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID",
"grafana.app/managedBy": "repo",
"grafana.app/managerId": "test",
"grafana.app/sourcePath": "a/b/c",
"grafana.app/sourceChecksum": "kkk",
"grafana.app/folder": "folderUID",
}, res.GetAnnotations())
meta.SetNamespace("aaa")
@@ -306,14 +310,13 @@ func TestMetaAccessor(t *testing.T) {
meta, err := utils.MetaAccessor(res)
require.NoError(t, err)
meta.SetRepositoryInfo(repoInfo)
meta.SetManagerProperties(repoInfo)
meta.SetFolder("folderUID")
require.Equal(t, map[string]string{
"grafana.app/repoName": "test",
"grafana.app/repoPath": "a/b/c",
"grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID",
"grafana.app/managedBy": "repo",
"grafana.app/managerId": "test",
"grafana.app/folder": "folderUID",
}, res.GetAnnotations())
meta.SetNamespace("aaa")
@@ -347,7 +350,7 @@ func TestMetaAccessor(t *testing.T) {
require.Equal(t, "ZZ", res.Status.Title)
})
t.Run("test reading old originInfo (now repository)", func(t *testing.T) {
t.Run("test reading old repo fields (now manager+source)", func(t *testing.T) {
res := &TestResource2{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
@@ -362,11 +365,15 @@ func TestMetaAccessor(t *testing.T) {
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)
manager, ok := meta.GetManagerProperties()
require.True(t, ok)
require.Equal(t, utils.ManagerKindRepo, manager.Kind)
require.Equal(t, "test", manager.Identity)
source, ok := meta.GetSourceProperties()
require.True(t, ok)
require.Equal(t, "a/b/c", source.Path)
require.Equal(t, "zzz", source.Checksum)
})
t.Run("blob info", func(t *testing.T) {
@@ -391,14 +398,16 @@ func TestMetaAccessor(t *testing.T) {
meta, err := utils.MetaAccessor(obj)
require.NoError(t, err)
meta.SetRepositoryInfo(repoInfo)
meta.SetManagerProperties(repoInfo)
meta.SetSourceProperties(sourceInfo)
meta.SetFolder("folderUID")
require.Equal(t, map[string]string{
"grafana.app/repoName": "test",
"grafana.app/repoPath": "a/b/c",
"grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID",
"grafana.app/managedBy": "repo",
"grafana.app/managerId": "test",
"grafana.app/sourcePath": "a/b/c",
"grafana.app/sourceChecksum": "kkk",
"grafana.app/folder": "folderUID",
}, obj.GetAnnotations())
require.Equal(t, "HELLO", obj.Spec.Title)
@@ -414,14 +423,13 @@ func TestMetaAccessor(t *testing.T) {
meta, err = utils.MetaAccessor(obj2)
require.NoError(t, err)
meta.SetRepositoryInfo(repoInfo)
meta.SetManagerProperties(repoInfo)
meta.SetFolder("folderUID")
require.Equal(t, map[string]string{
"grafana.app/repoName": "test",
"grafana.app/repoPath": "a/b/c",
"grafana.app/repoHash": "kkk",
"grafana.app/folder": "folderUID",
"grafana.app/managedBy": "repo",
"grafana.app/managerId": "test",
"grafana.app/folder": "folderUID",
}, obj2.GetAnnotations())
require.Equal(t, "xxx", meta.FindTitle("xxx"))
@@ -447,7 +455,7 @@ func TestMetaAccessor(t *testing.T) {
wantProperties: utils.ManagerProperties{
Identity: "",
Kind: utils.ManagerKindUnknown,
AllowsEdits: true,
AllowsEdits: false,
Suspended: false,
},
wantOK: false,
@@ -479,7 +487,7 @@ func TestMetaAccessor(t *testing.T) {
wantProperties: utils.ManagerProperties{
Identity: "",
Kind: utils.ManagerKindUnknown,
AllowsEdits: true,
AllowsEdits: false,
Suspended: false,
},
wantOK: false,
@@ -534,14 +542,14 @@ func TestMetaAccessor(t *testing.T) {
{
name: "set and get valid values",
setProperties: &utils.SourceProperties{
Path: "path",
Checksum: "hash",
Timestamp: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC),
Path: "path",
Checksum: "hash",
TimestampMillis: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(),
},
wantProperties: utils.SourceProperties{
Path: "path",
Checksum: "hash",
Timestamp: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC),
Path: "path",
Checksum: "hash",
TimestampMillis: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(),
},
wantOK: true,
},