From b918739a98ee9176a88b14d1a95bf78f49bc0ecc Mon Sep 17 00:00:00 2001 From: "Arati R." <33031346+suntala@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:36:46 +0200 Subject: [PATCH] K8s/Folders: Add basic folder integration tests (create, read) (#93232) * Add initial folder tests * Add test for get/reading folders * Compare legacy and k8s create and read * Remove dependency on grafanaAPIServerWithExperimentalAPIs --- pkg/apis/folder/v0alpha1/register.go | 9 +- pkg/registry/apis/folders/register.go | 4 +- pkg/tests/apis/folder/folders_test.go | 311 +++++++++++++++++- .../folder/testdata/folder-test-create.yaml | 7 + 4 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 pkg/tests/apis/folder/testdata/folder-test-create.yaml diff --git a/pkg/apis/folder/v0alpha1/register.go b/pkg/apis/folder/v0alpha1/register.go index 7decb813d5c..4f317ac272b 100644 --- a/pkg/apis/folder/v0alpha1/register.go +++ b/pkg/apis/folder/v0alpha1/register.go @@ -10,10 +10,11 @@ import ( ) const ( - GROUP = "folder.grafana.app" - VERSION = "v0alpha1" - RESOURCE = "folders" - APIVERSION = GROUP + "/" + VERSION + GROUP = "folder.grafana.app" + VERSION = "v0alpha1" + RESOURCE = "folders" + APIVERSION = GROUP + "/" + VERSION + RESOURCEGROUP = RESOURCE + "." + GROUP ) var FolderResourceInfo = utils.NewResourceInfo(GROUP, VERSION, diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index a22989ee283..fc7056d02be 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -47,8 +47,8 @@ func RegisterAPIService(cfg *setting.Cfg, accessControl accesscontrol.AccessControl, registerer prometheus.Registerer, ) *FolderAPIBuilder { - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil // skip registration unless opting into experimental apis + if !features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) { + return nil // skip registration unless opting into Kubernetes folders } builder := &FolderAPIBuilder{ diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index d6b4c765d2b..6cb319b9b03 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -1,12 +1,23 @@ package playlist import ( + "context" "encoding/json" + "net/http" + "slices" + "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/schema" + folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" @@ -16,14 +27,20 @@ func TestMain(m *testing.M) { testsuite.Run(m) } +var gvr = schema.GroupVersionResource{ + Group: "folder.grafana.app", + Version: "v0alpha1", + Resource: "folders", +} + func TestIntegrationFoldersApp(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for experimental APIs + AppModeProduction: true, EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service + featuremgmt.FlagKubernetesFolders, }, }) @@ -34,7 +51,6 @@ func TestIntegrationFoldersApp(t *testing.T) { v1Disco, err := json.MarshalIndent(resources, "", " ") require.NoError(t, err) - // fmt.Printf("%s", string(v1Disco)) require.JSONEq(t, `{ "kind": "APIResourceList", @@ -86,4 +102,293 @@ func TestIntegrationFoldersApp(t *testing.T) { ] }`, string(v1Disco)) }) + + t.Run("with k8s api flag", func(t *testing.T) { + doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFolders, + }, + })) + }) + + t.Run("with dual write (file, mode 0)", func(t *testing.T) { + doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "file", // write the files to disk + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode0, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFolders, + }, + })) + }) + + t.Run("with dual write (file, mode 1)", func(t *testing.T) { + doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "file", // write the files to disk + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode1, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFolders, + }, + })) + }) + + t.Run("with dual write (unified storage, mode 0)", func(t *testing.T) { + doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode0, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFolders, + }, + })) + }) + + t.Run("with dual write (unified storage, mode 1)", func(t *testing.T) { + doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode1, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFolders, + }, + })) + }) + + t.Run("with dual write (unified-grpc, mode 0)", func(t *testing.T) { + doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified-grpc", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode0, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFolders, + }, + })) + }) + + t.Run("with dual write (unified-grpc, mode 1)", func(t *testing.T) { + doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified-grpc", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode1, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFolders, + }, + })) + }) + + t.Run("with dual write (etcd, mode 0)", func(t *testing.T) { + // NOTE: running local etcd, that will be wiped clean! + t.Skip("local etcd testing") + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "etcd", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode0, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFolders, + }, + }) + + // Clear the collection before starting (etcd) + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + err := client.Resource.DeleteCollection(context.Background(), metav1.DeleteOptions{}, metav1.ListOptions{}) + require.NoError(t, err) + + doFolderTests(t, helper) + }) + + t.Run("with dual write (etcd, mode 1)", func(t *testing.T) { + // NOTE: running local etcd, that will be wiped clean! + t.Skip("local etcd testing") + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "etcd", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: grafanarest.Mode1, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFolders, + }, + }) + + // Clear the collection before starting (etcd) + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + err := client.Resource.DeleteCollection(context.Background(), metav1.DeleteOptions{}, metav1.ListOptions{}) + require.NoError(t, err) + + doFolderTests(t, helper) + }) +} + +func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper { + t.Run("Check folder CRUD (just create for now) in legacy API appears in k8s apis", func(t *testing.T) { + client := helper.GetResourceClient(apis.ResourceClientArgs{ + // #TODO: figure out permissions topic + User: helper.Org1.Admin, + GVR: gvr, + }) + + // #TODO fill out the payload: parentUID, description + // and check about uid orgid and siU + legacyPayload := `{ + "title": "Test", + "uid": "" + }` + legacyCreate := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(legacyPayload), + }, &folder.Folder{}) + require.NotNil(t, legacyCreate.Result) + uid := legacyCreate.Result.UID + require.NotEmpty(t, uid) + + expectedResult := `{ + "apiVersion": "folder.grafana.app/v0alpha1", + "kind": "Folder", + "metadata": { + "annotations": { + "grafana.app/originPath": "${originPath}", + "grafana.app/originName": "SQL" + }, + "creationTimestamp": "${creationTimestamp}", + "name": "` + uid + `", + "namespace": "default", + "resourceVersion": "${resourceVersion}", + "uid": "${uid}" + }, + "spec": { + "title": "Test" + } + }` + + // Get should return the same result + found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + require.NoError(t, err) + require.JSONEq(t, expectedResult, client.SanitizeJSON(found)) + }) + + t.Run("Do CRUD (just CR for now) via k8s (and check that legacy api still works)", func(t *testing.T) { + client := helper.GetResourceClient(apis.ResourceClientArgs{ + // #TODO: figure out permissions topic + User: helper.Org1.Admin, + GVR: gvr, + }) + + // Create the folder "test" + first, err := client.Resource.Create(context.Background(), + helper.LoadYAMLOrJSONFile("testdata/folder-test-create.yaml"), + metav1.CreateOptions{}, + ) + require.NoError(t, err) + require.Equal(t, "test", first.GetName()) + uids := []string{first.GetName()} + + // Create (with name generation) two folders + for i := 0; i < 2; i++ { + out, err := client.Resource.Create(context.Background(), + helper.LoadYAMLOrJSONFile("testdata/folder-generate.yaml"), + metav1.CreateOptions{}, + ) + require.NoError(t, err) + uids = append(uids, out.GetName()) + } + slices.Sort(uids) // make list compare stable + + // Check all playlists + for _, uid := range uids { + getFromBothAPIs(t, helper, client, uid, nil) + } + }) + return helper +} + +// This does a get with both k8s and legacy API, and verifies the results are the same +func getFromBothAPIs(t *testing.T, + helper *apis.K8sTestHelper, + client *apis.K8sResourceClient, + uid string, + // Optionally match some expect some values + expect *folder.Folder, +) *unstructured.Unstructured { + t.Helper() + + found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, uid, found.GetName()) + + dto := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodGet, + Path: "/api/folders/" + uid, + }, &folder.Folder{}).Result + require.NotNil(t, dto) + require.Equal(t, uid, dto.UID) + + spec, ok := found.Object["spec"].(map[string]any) + require.True(t, ok) + require.Equal(t, dto.UID, found.GetName()) + require.Equal(t, dto.Title, spec["title"]) + // #TODO add checks for other fields + + if expect != nil { + if expect.Title != "" { + require.Equal(t, expect.Title, dto.Title) + require.Equal(t, expect.Title, spec["title"]) + } + if expect.UID != "" { + require.Equal(t, expect.UID, dto.UID) + require.Equal(t, expect.UID, found.GetName()) + } + } + return found } diff --git a/pkg/tests/apis/folder/testdata/folder-test-create.yaml b/pkg/tests/apis/folder/testdata/folder-test-create.yaml new file mode 100644 index 00000000000..defcee9ac1e --- /dev/null +++ b/pkg/tests/apis/folder/testdata/folder-test-create.yaml @@ -0,0 +1,7 @@ +apiVersion: folder.grafana.app/v0alpha1 +kind: Folder +metadata: + name: test +spec: + title: Test folder (created from k8s; 2 items; POST) +