diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index e85f8ce1b69..9a11eae0a9b 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -429,7 +429,8 @@ func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, servic return 0, "", dashboards.ErrFolderInvalidUID } - // When we expect folders in unified storage, they should have a manager indicated + // When we expect folders in unified storage, they should have a manager indicated. + // NOTE: when everything has been running in mode5 for a while, this check can be removed. if err == nil && result != nil && result.ManagedBy == "" && fr.foldersInUnified { result, err = service.UpdateFolderWithManagedByAnnotation(ctx, result, fr.Cfg.Name) if err != nil { diff --git a/pkg/storage/legacysql/dualwrite/dualwriter.go b/pkg/storage/legacysql/dualwrite/dualwriter.go index ba4e757742e..365a5d5f14c 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter.go @@ -257,6 +257,19 @@ func (d *dualWriter) Create(ctx context.Context, in runtime.Object, createValida if permissions != "" { objCopy.SetAnnotation(utils.AnnoKeyGrantPermissions, permissions) } + + // Propagate annotations and labels to the object saved in + // unified storage, making sure the `deprecatedID` is saved + // as well as provisioning metadata, when present. + for name, val := range accIn.GetAnnotations() { + objCopy.SetAnnotation(name, val) + } + + legacyAcc, err := meta.Accessor(createdFromLegacy) + if err != nil { + return nil, err + } + objCopy.SetLabels(legacyAcc.GetLabels()) } // If unified storage is the primary storage, let's just create it in the foreground and return it. diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index ee958bcc7ec..e692b991ffb 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -25,6 +25,7 @@ import ( folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/utils" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/db" @@ -1983,6 +1984,31 @@ func TestIntegrationDeleteNestedFoldersPostorder(t *testing.T) { } } +func setupProvisioningDir(t *testing.T, opts *testinfra.GrafanaOpts) { + opts.Dir, opts.DirPath = testinfra.CreateGrafDir(t, *opts) + + // Create provisioning directories + provDashboardsDir := fmt.Sprintf("%s/conf/provisioning/dashboards", opts.Dir) + provDashboardsCfg := fmt.Sprintf("%s/dev.yaml", provDashboardsDir) + blob := []byte(fmt.Sprintf(` +apiVersion: 1 + +providers: +- name: 'provisioned dashboards' + type: file + orgId: 1 + folder: 'GrafanaCloud' + options: + path: %s`, provDashboardsDir)) + err := os.WriteFile(provDashboardsCfg, blob, 0o644) + require.NoError(t, err) + input, err := os.ReadFile(filepath.Join("testdata/dashboard.json")) + require.NoError(t, err) + provDashboardFile := filepath.Join(provDashboardsDir, "dashboard.json") + err = os.WriteFile(provDashboardFile, input, 0o644) + require.NoError(t, err) +} + // Test deleting folder with provisioned dashboard has proper handling with forceDeleteRules func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -2010,29 +2036,8 @@ func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) { featuremgmt.FlagUnifiedStorageSearch, }, } - // Setup Grafana with provisioning - ops.Dir, ops.DirPath = testinfra.CreateGrafDir(t, ops) - // Create provisioning directories - provDashboardsDir := fmt.Sprintf("%s/conf/provisioning/dashboards", ops.Dir) - provDashboardsCfg := fmt.Sprintf("%s/dev.yaml", provDashboardsDir) - blob := []byte(fmt.Sprintf(` -apiVersion: 1 - -providers: -- name: 'provisioned dashboards' - type: file - orgId: 1 - folder: 'GrafanaCloud' - options: - path: %s`, provDashboardsDir)) - err := os.WriteFile(provDashboardsCfg, blob, 0o644) - require.NoError(t, err) - input, err := os.ReadFile(filepath.Join("testdata/dashboard.json")) - require.NoError(t, err) - provDashboardFile := filepath.Join(provDashboardsDir, "dashboard.json") - err = os.WriteFile(provDashboardFile, input, 0o644) - require.NoError(t, err) + setupProvisioningDir(t, &ops) helper := apis.NewK8sTestHelper(t, ops) client := helper.GetResourceClient(apis.ResourceClientArgs{ @@ -2059,7 +2064,7 @@ providers: } }, 10*time.Second, 25*time.Millisecond) - _, err = client.Resource.Get(context.Background(), folderUID, metav1.GetOptions{}) + _, err := client.Resource.Get(context.Background(), folderUID, metav1.GetOptions{}) require.NoError(t, err, "folder %s should exist", folderUID) // Verify dashboards exist verifyDashboardExists := func(shouldExist bool) { @@ -2116,3 +2121,59 @@ providers: }) } } + +// Test that folders created during provisioning using the dual writer have the +// appropriate labels and annotations in unified storage. +func TestIntegrationProvisionedFolderPropagatesLabelsAndAnnotations(t *testing.T) { + mode3 := grafanarest.DualWriterMode(3) + ops := testinfra.GrafanaOpts{ + DisableAnonymous: true, + AppModeProduction: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: mode3, + }, + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode3, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagUnifiedStorageSearch, + }, + } + + setupProvisioningDir(t, &ops) + helper := apis.NewK8sTestHelper(t, ops) + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + + var folderList folders.FolderList + require.EventuallyWithT(t, func(collect *assert.CollectT) { + resp := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodGet, + Path: fmt.Sprintf("/apis/folder.grafana.app/v1beta1/namespaces/%s/folders", client.Args.Namespace), + }, &map[string]interface{}{}) + require.NotNil(t, resp.Response) + require.Equal(t, http.StatusOK, resp.Response.StatusCode) + require.NoError(t, json.Unmarshal(resp.Body, &folderList)) + }, 10*time.Second, 25*time.Millisecond) + + require.Len(t, folderList.Items, 1) + + accessor, err := utils.MetaAccessor(&folderList.Items[0]) + require.NoError(t, err) + + expectedLabels := map[string]string{"grafana.app/deprecatedInternalID": "1"} + expectedAnnotations := map[string]string{ + "grafana.app/createdBy": "access-policy:service", + "grafana.app/managedBy": "classic-file-provisioning", + "grafana.app/managerId": "provisioned dashboards", + } + + require.Equal(t, expectedLabels, accessor.GetLabels()) + require.Equal(t, expectedAnnotations, accessor.GetAnnotations()) +}