package utils_test import ( "encoding/json" "testing" "time" "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" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/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"` // Read/write raw status Status Spec `json:"status,omitempty"` // Secure values as map Secure common.InlineSecureValues `json:"secure,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"` // Exercise read/write pointer status Status *Spec `json:"status,omitempty"` // This time defined with a strict struct Secure ExplicitSecureValues `json:"secure,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 ExplicitSecureValues struct { // Non-pointer Value1 common.InlineSecureValue `json:"v1,omitempty"` // Pointer value Value2 common.InlineSecureValue `json:"v2,omitempty"` } // 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) { 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) { _, err := utils.MetaAccessor(nil) require.Error(t, err) _, 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 properties", func(t *testing.T) { obj, err := utils.MetaAccessor(&unstructured.Unstructured{ Object: map[string]any{}, }) require.NoError(t, err) rv := obj.GetResourceVersion() require.Equal(t, "", rv) _, ok := obj.GetRuntimeObject() require.True(t, ok) anno := obj.GetAnnotations() require.Nil(t, anno) rvInt, err := obj.GetResourceVersionInt64() require.NoError(t, err) require.Equal(t, int64(0), rvInt) obj.SetResourceVersion("not a number") rv = obj.GetResourceVersion() require.Equal(t, "not a number", rv) rvInt, err = obj.GetResourceVersionInt64() require.Error(t, err) require.Equal(t, int64(0), rvInt) obj.SetUpdatedBy("updatedBy") require.Equal(t, "updatedBy", obj.GetUpdatedBy()) anno = obj.GetAnnotations() require.Len(t, anno, 1) // One key obj.SetAnnotation(utils.AnnoKeyUpdatedBy, "") anno = obj.GetAnnotations() require.Empty(t, anno) // removed the key obj.SetCreatedBy("createdBy") require.Equal(t, "createdBy", obj.GetCreatedBy()) obj.SetFolder("folder") require.Equal(t, "folder", obj.GetFolder()) }) t.Run("get and set grafana labels (unstructured)", func(t *testing.T) { res := &unstructured.Unstructured{ Object: map[string]any{}, } meta, err := utils.MetaAccessor(res) require.NoError(t, err) // should return 0 when not set require.Equal(t, meta.GetDeprecatedInternalID(), int64(0)) // 0 is not allowed meta.SetDeprecatedInternalID(0) require.Equal(t, map[string]string(nil), res.GetLabels()) // should be able to set and get meta.SetDeprecatedInternalID(1) require.Equal(t, map[string]string{ "grafana.app/deprecatedInternalID": "1", }, res.GetLabels()) require.Equal(t, meta.GetDeprecatedInternalID(), int64(1)) }) t.Run("get and set grafana metadata (unstructured)", func(t *testing.T) { // Error reading spec+status when missing res := &unstructured.Unstructured{ Object: map[string]any{}, } meta, err := utils.MetaAccessor(res) require.NoError(t, err) spec, err := meta.GetSpec() require.Error(t, err) require.Nil(t, spec) status, err := meta.GetStatus() require.Error(t, err) require.Nil(t, status) // Now set a spec and status res.Object = map[string]any{ "spec": map[string]any{ "hello": "world", "title": "Title", }, "status": map[string]any{ "sloth": "🦥", }, } require.Equal(t, "", meta.GetFolder()) require.Equal(t, "", meta.GetAnnotation("missing annotation")) meta.SetManagerProperties(repoInfo) meta.SetFolder("folderUID") require.Equal(t, map[string]string{ "grafana.app/managedBy": "repo", "grafana.app/managerId": "test", "grafana.app/folder": "folderUID", }, res.GetAnnotations()) meta.SetNamespace("aaa") meta.SetResourceVersionInt64(12345) require.Equal(t, "aaa", res.GetNamespace()) require.Equal(t, "aaa", meta.GetNamespace()) rv, err := meta.GetResourceVersionInt64() require.NoError(t, err) require.Equal(t, int64(12345), rv) require.Equal(t, "Title", meta.FindTitle("")) // Make sure access to spec works for Unstructured spec, err = meta.GetSpec() require.NoError(t, err) require.Equal(t, res.Object["spec"], spec) spec = &map[string]string{"a": "b"} err = meta.SetSpec(spec) require.NoError(t, err) spec, err = meta.GetSpec() require.NoError(t, err) require.Equal(t, res.Object["spec"], spec) // Make sure access to spec works for Unstructured status, err = meta.GetStatus() require.NoError(t, err) require.Equal(t, res.Object["status"], status) status = &map[string]string{"a": "b"} err = meta.SetStatus(status) require.NoError(t, err) status, err = meta.GetStatus() require.NoError(t, err) require.Equal(t, res.Object["status"], status) // Check write/read on unstructured object err = meta.SetSecureValues(common.InlineSecureValues{ "a": {Name: "bbbb"}, }) require.NoError(t, err) secure, err := meta.GetSecureValues() require.NoError(t, err) require.JSONEq(t, `{"a": {"name": "bbbb"}}`, asJSON(secure, true)) }) t.Run("get and set grafana metadata (TestResource)", func(t *testing.T) { res := &TestResource{ Spec: Spec{ Title: "test", }, Secure: common.InlineSecureValues{ "x": common.InlineSecureValue{ Create: "hello", }, }, // Status is empty, but not nil! } meta, err := utils.MetaAccessor(res) require.NoError(t, err) meta.SetManagerProperties(repoInfo) meta.SetSourceProperties(sourceInfo) meta.SetFolder("folderUID") require.Equal(t, map[string]string{ "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") meta.SetResourceVersionInt64(12345) require.Equal(t, "aaa", res.GetNamespace()) require.Equal(t, "aaa", meta.GetNamespace()) rv, err := meta.GetResourceVersionInt64() require.NoError(t, err) require.Equal(t, int64(12345), rv) // Make sure access to spec works for Unstructured spec, err := meta.GetSpec() require.NoError(t, err) require.Equal(t, res.Spec, spec) err = meta.SetSpec(Spec{Title: "t2"}) require.NoError(t, err) spec, err = meta.GetSpec() require.NoError(t, err) require.Equal(t, res.Spec, spec) require.Equal(t, `{"title":"t2"}`, asJSON(spec, false)) // Check read/write status status, err := meta.GetStatus() require.NoError(t, err) require.NotNil(t, status) err = meta.SetStatus(Spec{Title: "111"}) require.NoError(t, err) status, err = meta.GetStatus() require.NoError(t, err) require.Equal(t, res.Status, status) require.Equal(t, "111", res.Status.Title) require.Equal(t, `{"title":"111"}`, asJSON(status, false)) // Check read/write secure values secure, err := meta.GetSecureValues() require.NoError(t, err) require.JSONEq(t, `{"x": {"create": "[REDACTED]"}}`, asJSON(secure, true)) err = meta.SetSecureValues(common.InlineSecureValues{ "a": {Name: "bbbb"}, }) require.NoError(t, err) secure, err = meta.GetSecureValues() require.NoError(t, err) require.JSONEq(t, `{"a": {"name": "bbbb"}}`, asJSON(secure, true)) }) t.Run("get and set grafana metadata (TestResource2)", func(t *testing.T) { res := &TestResource2{ Spec: Spec2{}, Status: &Spec{Title: "X"}, Secure: ExplicitSecureValues{ Value1: common.InlineSecureValue{Name: "hello"}, }, } meta, err := utils.MetaAccessor(res) require.NoError(t, err) meta.SetManagerProperties(repoInfo) meta.SetFolder("folderUID") require.Equal(t, map[string]string{ "grafana.app/managedBy": "repo", "grafana.app/managerId": "test", "grafana.app/folder": "folderUID", }, res.GetAnnotations()) meta.SetNamespace("aaa") meta.SetResourceVersionInt64(12345) require.Equal(t, "aaa", res.GetNamespace()) require.Equal(t, "aaa", meta.GetNamespace()) rv, err := meta.GetResourceVersionInt64() require.NoError(t, err) require.Equal(t, int64(12345), rv) // Make sure access to spec works for TestResource2 spec, err := meta.GetSpec() require.NoError(t, err) require.Equal(t, res.Spec, spec) err = meta.SetSpec(Spec2{}) require.NoError(t, err) spec, err = meta.GetSpec() require.NoError(t, err) require.Equal(t, res.Spec, spec) // Make sure access to spec works for TestResource2 status, err := meta.GetStatus() require.NoError(t, err) require.Equal(t, res.Status, status) err = meta.SetStatus(&Spec{Title: "ZZ"}) require.NoError(t, err) status, err = meta.GetStatus() require.NoError(t, err) require.Equal(t, res.Status, status) require.Equal(t, "ZZ", res.Status.Title) secure, err := meta.GetSecureValues() require.NoError(t, err) require.JSONEq(t, `{"v1": {"name": "hello"}}`, asJSON(secure, true)) // Check that we can write err = meta.SetSecureValues(common.InlineSecureValues{ "v2": {Name: "bbb"}, }) require.NoError(t, err) secure, err = meta.GetSecureValues() require.NoError(t, err) require.JSONEq(t, `{"v2": {"name": "bbb"}}`, asJSON(secure, true)) // NOTE: v1 was removed err = meta.SetSecureValues(common.InlineSecureValues{ "UNKNOWN": {Name: "bbb"}, // not a valid name }) require.Error(t, err) }) t.Run("test reading old repo fields (now manager+source)", 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) 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) { info := &utils.BlobInfo{UID: "AAA", Size: 123, Hash: "xyz", MimeType: "application/json", Charset: "utf-8"} anno := info.String() require.Equal(t, "AAA; size=123; hash=xyz; mime=application/json; charset=utf-8", anno) copy := utils.ParseBlobInfo(anno) require.Equal(t, info, copy) }) t.Run("find titles", func(t *testing.T) { // with a k8s object that has Spec.Title obj := &TestResource{ TypeMeta: metav1.TypeMeta{ Kind: "TestKIND", APIVersion: "aaa/v1alpha2", }, Spec: Spec{ Title: "HELLO", }, } meta, err := utils.MetaAccessor(obj) require.NoError(t, err) meta.SetManagerProperties(repoInfo) meta.SetSourceProperties(sourceInfo) meta.SetFolder("folderUID") require.Equal(t, map[string]string{ "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) require.Equal(t, "HELLO", meta.FindTitle("")) obj.Spec.Title = "" require.Equal(t, "", meta.FindTitle("xxx")) gvk := meta.GetGroupVersionKind() require.Equal(t, "aaa/v1alpha2, Kind=TestKIND", gvk.String()) // with a k8s object without Spec.Title obj2 := &TestResource2{} meta, err = utils.MetaAccessor(obj2) require.NoError(t, err) meta.SetManagerProperties(repoInfo) meta.SetFolder("folderUID") require.Equal(t, map[string]string{ "grafana.app/managedBy": "repo", "grafana.app/managerId": "test", "grafana.app/folder": "folderUID", }, obj2.GetAnnotations()) require.Equal(t, "xxx", meta.FindTitle("xxx")) rt, ok := meta.GetRuntimeObject() require.Equal(t, obj2, rt) require.True(t, ok) spec, err := meta.GetSpec() require.Equal(t, obj2.Spec, spec) require.NoError(t, err) }) t.Run("ManagerProperties", func(t *testing.T) { tests := []struct { name string setProperties *utils.ManagerProperties wantProperties utils.ManagerProperties wantOK bool }{ { name: "get default values", wantProperties: utils.ManagerProperties{ Identity: "", Kind: utils.ManagerKindUnknown, AllowsEdits: false, Suspended: false, }, wantOK: false, }, { name: "set and get valid values", setProperties: &utils.ManagerProperties{ Identity: "identity", Kind: utils.ManagerKindTerraform, AllowsEdits: false, Suspended: false, }, wantProperties: utils.ManagerProperties{ Identity: "identity", Kind: utils.ManagerKindTerraform, AllowsEdits: false, Suspended: false, }, wantOK: true, }, { name: "set empty identity returns default values", setProperties: &utils.ManagerProperties{ Identity: "", Kind: utils.ManagerKindRepo, AllowsEdits: false, Suspended: false, }, wantProperties: utils.ManagerProperties{ Identity: "", Kind: utils.ManagerKindUnknown, AllowsEdits: false, Suspended: false, }, wantOK: false, }, { name: "invalid kind falls back to generic kind", setProperties: &utils.ManagerProperties{ Identity: "identity", Kind: utils.ManagerKind("invalid"), AllowsEdits: false, Suspended: true, }, wantProperties: utils.ManagerProperties{ Identity: "identity", Kind: utils.ManagerKindUnknown, AllowsEdits: false, Suspended: true, }, wantOK: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { res := &TestResource2{} meta, err := utils.MetaAccessor(res) require.NoError(t, err) if tt.setProperties != nil { meta.SetManagerProperties(*tt.setProperties) } mp, ok := meta.GetManagerProperties() require.Equal(t, tt.wantOK, ok) require.Equal(t, tt.wantProperties, mp) }) } }) t.Run("manage secure values", func(t *testing.T) { raw := &unstructured.Unstructured{ Object: map[string]any{}, } obj, _ := utils.MetaAccessor(raw) sv, err := obj.GetSecureValues() require.NoError(t, err) require.Nil(t, sv) err = obj.SetSecureValues(common.InlineSecureValues{ "A": common.InlineSecureValue{Name: "NameForA"}, }) require.NoError(t, err) require.NotNil(t, raw.Object["secure"]) sv, err = obj.GetSecureValues() require.NoError(t, err) require.Equal(t, "NameForA", sv["A"].Name) // Manually set secure to an invalid property: raw.Object["secure"] = t sv, err = obj.GetSecureValues() require.Error(t, err) require.Nil(t, sv) delete(raw.Object, "secure") }) t.Run("SourceProperties", func(t *testing.T) { tests := []struct { name string setProperties *utils.SourceProperties wantProperties utils.SourceProperties wantOK bool }{ { name: "get default values", wantProperties: utils.SourceProperties{}, wantOK: false, }, { name: "set and get valid values", setProperties: &utils.SourceProperties{ Path: "path", Checksum: "hash", TimestampMillis: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), }, wantProperties: utils.SourceProperties{ Path: "path", Checksum: "hash", TimestampMillis: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), }, wantOK: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { res := &TestResource2{} meta, err := utils.MetaAccessor(res) require.NoError(t, err) if tt.setProperties != nil { meta.SetSourceProperties(*tt.setProperties) } sp, ok := meta.GetSourceProperties() require.Equal(t, tt.wantProperties, sp) require.Equal(t, tt.wantOK, ok) }) } }) } func asJSON(v any, pretty bool) string { if v == nil { return "" } if pretty { bytes, _ := json.MarshalIndent(v, "", " ") return string(bytes) } bytes, _ := json.Marshal(v) return string(bytes) }