From 700e6e328799cb6ef4680fa233d713cbc696001b Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 12 Oct 2023 16:15:16 +0200 Subject: [PATCH 001/160] AuthN: Add service account token generation to `ExtSvcAccountsService` (#76327) * Manage service account secrets * Wip * WIP * WIP * Revert to keep a light interface * Implement SaveExternalService * Remove unecessary functions from the interface * Remove unused field * Better log * Leave ext svc credentials out of the extsvcauth package for now * Remove todo * Add tests to SaveExternalService * Test that secret has been removed from store * Lint * Nit. * Rename commands and structs Co-authored-by: Kalle Persson * Account for PR feedback Co-authored-by: Andres Martinez Gotor * Linting * Add nosec comment G101 - this is not a hardcoded secret * Lowercase kvStoreType --------- Co-authored-by: Kalle Persson Co-authored-by: Andres Martinez Gotor --- pkg/services/extsvcauth/errors.go | 3 +- .../extsvcauth/extsvcaccounts/models.go | 19 +- .../extsvcauth/extsvcaccounts/service.go | 136 ++++++++++- .../extsvcauth/extsvcaccounts/service_test.go | 214 +++++++++++++++++- pkg/services/extsvcauth/models.go | 3 + 5 files changed, 354 insertions(+), 21 deletions(-) diff --git a/pkg/services/extsvcauth/errors.go b/pkg/services/extsvcauth/errors.go index f1af61f4f0c29..c605dd3f7ab9b 100644 --- a/pkg/services/extsvcauth/errors.go +++ b/pkg/services/extsvcauth/errors.go @@ -3,5 +3,6 @@ package extsvcauth import "github.com/grafana/grafana/pkg/util/errutil" var ( - ErrUnknownProvider = errutil.BadRequest("extsvcauth.unknown-provider") + ErrUnknownProvider = errutil.BadRequest("extsvcauth.unknown-provider") + ErrCredentialsNotFound = errutil.NotFound("extsvcauth.credentials-not-found") ) diff --git a/pkg/services/extsvcauth/extsvcaccounts/models.go b/pkg/services/extsvcauth/extsvcaccounts/models.go index 536d6432f32a5..621513e0d210f 100644 --- a/pkg/services/extsvcauth/extsvcaccounts/models.go +++ b/pkg/services/extsvcauth/extsvcaccounts/models.go @@ -5,7 +5,24 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" ) -type saveExtSvcAccountCmd struct { +const ( + kvStoreType = "extsvc-token" + // #nosec G101 - this is not a hardcoded secret + tokenNamePrefix = "extsvc-token" +) + +// Credentials represents the credentials associated to an external service +type Credentials struct { + Secret string +} + +type SaveCredentialsCmd struct { + ExtSvcSlug string + OrgID int64 + Secret string +} + +type saveCmd struct { ExtSvcSlug string OrgID int64 Permissions []ac.Permission diff --git a/pkg/services/extsvcauth/extsvcaccounts/service.go b/pkg/services/extsvcauth/extsvcaccounts/service.go index 6bc60ace2c51b..3041cf653a910 100644 --- a/pkg/services/extsvcauth/extsvcaccounts/service.go +++ b/pkg/services/extsvcauth/extsvcaccounts/service.go @@ -4,24 +4,32 @@ import ( "context" "errors" + "github.com/grafana/grafana/pkg/components/satokengen" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/models/roletype" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/extsvcauth" + "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/secrets/kvstore" sa "github.com/grafana/grafana/pkg/services/serviceaccounts" ) type ExtSvcAccountsService struct { - acSvc ac.Service - logger log.Logger - saSvc sa.Service + acSvc ac.Service + logger log.Logger + saSvc sa.Service + skvStore kvstore.SecretsKVStore } -func ProvideExtSvcAccountsService(acSvc ac.Service, saSvc sa.Service) *ExtSvcAccountsService { +func ProvideExtSvcAccountsService(acSvc ac.Service, saSvc sa.Service, db db.DB, secretsSvc secrets.Service) *ExtSvcAccountsService { + logger := log.New("serviceauth.extsvcaccounts") return &ExtSvcAccountsService{ - acSvc: acSvc, - logger: log.New("serviceauth.extsvcaccounts"), - saSvc: saSvc, + acSvc: acSvc, + logger: logger, + saSvc: saSvc, + skvStore: kvstore.NewSQLSecretsKVStore(db, secretsSvc, logger), // Using SQL store to avoid a cyclic dependency } } @@ -41,6 +49,46 @@ func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, org }, nil } +// SaveExternalService creates, updates or delete a service account (and its token) with the requested permissions. +func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) { + if cmd == nil { + esa.logger.Warn("Received no input") + return nil, nil + } + + slug := slugify.Slugify(cmd.Name) + + if cmd.Impersonation.Enabled { + esa.logger.Warn("Impersonation setup skipped. It is not possible to impersonate with a service account token.", "service", slug) + } + + saID, err := esa.ManageExtSvcAccount(ctx, &extsvcauth.ManageExtSvcAccountCmd{ + ExtSvcSlug: slug, + Enabled: cmd.Self.Enabled, + OrgID: extsvcauth.TmpOrgID, + Permissions: cmd.Self.Permissions, + }) + if err != nil { + return nil, err + } + + // No need for a token if we don't have a service account + if saID <= 0 { + esa.logger.Debug("Skipping service account token creation", "service", slug) + return nil, nil + } + + token, err := esa.getExtSvcAccountToken(ctx, extsvcauth.TmpOrgID, saID, slug) + if err != nil { + esa.logger.Error("Could not get the external svc token", + "service", slug, + "saID", saID, + "error", err.Error()) + return nil, err + } + return &extsvcauth.ExternalService{Name: cmd.Name, ID: slug, Secret: token}, nil +} + // ManageExtSvcAccount creates, updates or deletes the service account associated with an external service func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *extsvcauth.ManageExtSvcAccountCmd) (int64, error) { if cmd == nil { @@ -71,7 +119,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd * return 0, nil } - saID, errSave := esa.saveExtSvcAccount(ctx, &saveExtSvcAccountCmd{ + saID, errSave := esa.saveExtSvcAccount(ctx, &saveCmd{ ExtSvcSlug: cmd.ExtSvcSlug, OrgID: cmd.OrgID, Permissions: cmd.Permissions, @@ -86,7 +134,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd * } // saveExtSvcAccount creates or updates the service account associated with an external service -func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *saveExtSvcAccountCmd) (int64, error) { +func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *saveCmd) (int64, error) { if cmd.SaID <= 0 { // Create a service account esa.logger.Debug("Create service account", "service", cmd.ExtSvcSlug, "orgID", cmd.OrgID) @@ -118,9 +166,75 @@ func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *sa // deleteExtSvcAccount deletes a service account by ID and removes its associated role func (esa *ExtSvcAccountsService) deleteExtSvcAccount(ctx context.Context, orgID int64, slug string, saID int64) error { - esa.logger.Info("Delete service account", "service", slug, "saID", saID) + esa.logger.Info("Delete service account", "service", slug, "orgID", orgID, "saID", saID) if err := esa.saSvc.DeleteServiceAccount(ctx, orgID, saID); err != nil { return err } - return esa.acSvc.DeleteExternalServiceRole(ctx, slug) + if err := esa.acSvc.DeleteExternalServiceRole(ctx, slug); err != nil { + return err + } + return esa.DeleteExtSvcCredentials(ctx, orgID, slug) +} + +// getExtSvcAccountToken get or create the token of an External Service +func (esa *ExtSvcAccountsService) getExtSvcAccountToken(ctx context.Context, orgID, saID int64, extSvcSlug string) (string, error) { + // Get credentials from store + credentials, err := esa.GetExtSvcCredentials(ctx, orgID, extSvcSlug) + if err != nil && !errors.Is(err, extsvcauth.ErrCredentialsNotFound) { + return "", err + } + if credentials != nil { + return credentials.Secret, nil + } + + // Generate token + esa.logger.Info("Generate new service account token", "service", extSvcSlug, "orgID", orgID) + newKeyInfo, err := satokengen.New(extSvcSlug) + if err != nil { + return "", err + } + + esa.logger.Debug("Add service account token", "service", extSvcSlug, "orgID", orgID) + if _, err := esa.saSvc.AddServiceAccountToken(ctx, saID, &sa.AddServiceAccountTokenCommand{ + Name: tokenNamePrefix + "-" + extSvcSlug, + OrgId: orgID, + Key: newKeyInfo.HashedKey, + }); err != nil { + return "", err + } + + if err := esa.SaveExtSvcCredentials(ctx, &SaveCredentialsCmd{ + ExtSvcSlug: extSvcSlug, + OrgID: orgID, + Secret: newKeyInfo.ClientSecret, + }); err != nil { + return "", err + } + + return newKeyInfo.ClientSecret, nil +} + +// GetExtSvcCredentials get the credentials of an External Service from an encrypted storage +func (esa *ExtSvcAccountsService) GetExtSvcCredentials(ctx context.Context, orgID int64, extSvcSlug string) (*Credentials, error) { + esa.logger.Debug("Get service account token from skv", "service", extSvcSlug, "orgID", orgID) + token, ok, err := esa.skvStore.Get(ctx, orgID, extSvcSlug, kvStoreType) + if err != nil { + return nil, err + } + if !ok { + return nil, extsvcauth.ErrCredentialsNotFound.Errorf("No credential found for in store %v", extSvcSlug) + } + return &Credentials{Secret: token}, nil +} + +// SaveExtSvcCredentials stores the credentials of an External Service in an encrypted storage +func (esa *ExtSvcAccountsService) SaveExtSvcCredentials(ctx context.Context, cmd *SaveCredentialsCmd) error { + esa.logger.Debug("Save service account token in skv", "service", cmd.ExtSvcSlug, "orgID", cmd.OrgID) + return esa.skvStore.Set(ctx, cmd.OrgID, cmd.ExtSvcSlug, kvStoreType, cmd.Secret) +} + +// DeleteExtSvcCredentials removes the credentials of an External Service from an encrypted storage +func (esa *ExtSvcAccountsService) DeleteExtSvcCredentials(ctx context.Context, orgID int64, extSvcSlug string) error { + esa.logger.Debug("Delete service account token from skv", "service", extSvcSlug, "orgID", orgID) + return esa.skvStore.Del(ctx, orgID, extSvcSlug, kvStoreType) } diff --git a/pkg/services/extsvcauth/extsvcaccounts/service_test.go b/pkg/services/extsvcauth/extsvcaccounts/service_test.go index 0cb27a557291a..92a152e618c34 100644 --- a/pkg/services/extsvcauth/extsvcaccounts/service_test.go +++ b/pkg/services/extsvcauth/extsvcaccounts/service_test.go @@ -10,8 +10,10 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/secrets/kvstore" sa "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/setting" @@ -20,9 +22,10 @@ import ( ) type TestEnv struct { - S *ExtSvcAccountsService - AcStore *actest.MockStore - SaSvc *tests.MockServiceAccountService + S *ExtSvcAccountsService + AcStore *actest.MockStore + SaSvc *tests.MockServiceAccountService + SkvStore *kvstore.FakeSecretsKVStore } func setupTestEnv(t *testing.T) *TestEnv { @@ -32,13 +35,15 @@ func setupTestEnv(t *testing.T) *TestEnv { fmgt := featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth) env := &TestEnv{ - AcStore: &actest.MockStore{}, - SaSvc: &tests.MockServiceAccountService{}, + AcStore: &actest.MockStore{}, + SaSvc: &tests.MockServiceAccountService{}, + SkvStore: kvstore.NewFakeSecretsKVStore(), } env.S = &ExtSvcAccountsService{ - acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt), - logger: log.New("extsvcaccounts.test"), - saSvc: env.SaSvc, + acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt), + logger: log.New("extsvcaccounts.test"), + saSvc: env.SaSvc, + skvStore: env.SkvStore, } return env } @@ -209,3 +214,196 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) { }) } } + +func TestExtSvcAccountsService_SaveExternalService(t *testing.T) { + extSvcSlug := "grafana-test-app" + tmpOrgID := int64(1) + extSvcAccID := int64(10) + extSvcPerms := []ac.Permission{{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}} + extSvcAccount := &sa.ServiceAccountDTO{ + Id: extSvcAccID, + Name: extSvcSlug, + Login: extSvcSlug, + OrgId: tmpOrgID, + IsDisabled: false, + Role: string(roletype.RoleNone), + } + + tests := []struct { + name string + init func(env *TestEnv) + cmd extsvcauth.ExternalServiceRegistration + checks func(t *testing.T, env *TestEnv) + want *extsvcauth.ExternalService + wantErr bool + }{ + { + name: "should remove service account when disabled", + init: func(env *TestEnv) { + // A previous service account was attached to this slug + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).Return(extSvcAccID, nil) + env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil) + env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + // A token was previously stored in the secret store + _ = env.SkvStore.Set(context.Background(), tmpOrgID, extSvcSlug, kvStoreType, "ExtSvcSecretToken") + }, + cmd: extsvcauth.ExternalServiceRegistration{ + Name: extSvcSlug, + Self: extsvcauth.SelfCfg{ + Enabled: false, + Permissions: extSvcPerms, + }, + }, + checks: func(t *testing.T, env *TestEnv) { + env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, + mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), + mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) + env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, + mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), + mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) + env.AcStore.AssertCalled(t, "DeleteExternalServiceRole", mock.Anything, + mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) + _, ok, _ := env.SkvStore.Get(context.Background(), tmpOrgID, extSvcSlug, kvStoreType) + require.False(t, ok, "secret should have been removed from store") + }, + want: nil, + wantErr: false, + }, + { + name: "should remove service account when no permission", + init: func(env *TestEnv) { + // A previous service account was attached to this slug + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).Return(extSvcAccID, nil) + env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil) + env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + }, + cmd: extsvcauth.ExternalServiceRegistration{ + Name: extSvcSlug, + Self: extsvcauth.SelfCfg{ + Enabled: true, + Permissions: []ac.Permission{}, + }, + }, + checks: func(t *testing.T, env *TestEnv) { + env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, + mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), + mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) + env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything, + mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), + mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID })) + env.AcStore.AssertCalled(t, "DeleteExternalServiceRole", mock.Anything, + mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) + }, + want: nil, + wantErr: false, + }, + { + name: "should create new service account", + init: func(env *TestEnv) { + // No previous service account was attached to this slug + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything). + Return(int64(0), sa.ErrServiceAccountNotFound.Errorf("mock")) + env.SaSvc.On("CreateServiceAccount", mock.Anything, mock.Anything, mock.Anything). + Return(extSvcAccount, nil) + // Api Key was added without problem + env.SaSvc.On("AddServiceAccountToken", mock.Anything, mock.Anything, mock.Anything).Return(&apikey.APIKey{}, nil) + env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + }, + cmd: extsvcauth.ExternalServiceRegistration{ + Name: extSvcSlug, + Self: extsvcauth.SelfCfg{ + Enabled: true, + Permissions: extSvcPerms, + }, + }, + checks: func(t *testing.T, env *TestEnv) { + env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, + mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), + mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) + env.SaSvc.AssertCalled(t, "CreateServiceAccount", mock.Anything, + mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), + mock.MatchedBy(func(cmd *sa.CreateServiceAccountForm) bool { + return cmd.Name == extSvcSlug && *cmd.Role == roletype.RoleNone + }), + ) + env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, + mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { + return cmd.ServiceAccountID == extSvcAccount.Id && cmd.ExternalServiceID == extSvcSlug && + cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && + cmd.Permissions[0] == extSvcPerms[0] + })) + }, + want: &extsvcauth.ExternalService{ + Name: extSvcSlug, + ID: extSvcSlug, + Secret: "not empty", + }, + wantErr: false, + }, + { + name: "should update service account", + init: func(env *TestEnv) { + // A previous service account was attached to this slug + env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything). + Return(int64(11), nil) + env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil) + // This time we don't add a token but rely on the secret store + _ = env.SkvStore.Set(context.Background(), tmpOrgID, extSvcSlug, kvStoreType, "ExtSvcSecretToken") + }, + cmd: extsvcauth.ExternalServiceRegistration{ + Name: extSvcSlug, + Self: extsvcauth.SelfCfg{ + Enabled: true, + Permissions: extSvcPerms, + }, + }, + checks: func(t *testing.T, env *TestEnv) { + env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything, + mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }), + mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug })) + env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything, + mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool { + return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug && + cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 && + cmd.Permissions[0] == extSvcPerms[0] + })) + }, + want: &extsvcauth.ExternalService{ + Name: extSvcSlug, + ID: extSvcSlug, + Secret: "not empty", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + env := setupTestEnv(t) + if tt.init != nil { + tt.init(env) + } + + got, err := env.S.SaveExternalService(ctx, &tt.cmd) + + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + if tt.checks != nil { + tt.checks(t, env) + } + + // Only check that there is a secret, not it's actual value + if tt.want != nil && len(tt.want.Secret) > 0 { + require.NotEmpty(t, got.Secret) + tt.want.Secret = got.Secret + } + + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/services/extsvcauth/models.go b/pkg/services/extsvcauth/models.go index cb82ed08ef7ae..51183a06d4115 100644 --- a/pkg/services/extsvcauth/models.go +++ b/pkg/services/extsvcauth/models.go @@ -9,6 +9,9 @@ import ( const ( OAuth2Server AuthProvider = "OAuth2Server" + + // TmpOrgID is the orgID we use while global service accounts are not supported. + TmpOrgID int64 = 1 ) type AuthProvider string From 7562607319b7453f3b67bcd3a1fad6090bedb6c2 Mon Sep 17 00:00:00 2001 From: Leanna Shippy <11079957+lshippy@users.noreply.github.com> Date: Thu, 12 Oct 2023 07:57:06 -0700 Subject: [PATCH 002/160] [Docs] Annotate visualizations update (#76395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is a sentence in the [Built-in query](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/#built-in-query) section of the page that could use a "the" (emphasis added by me 😄) : > When you copy a dashboard using the Save As feature it will get a new dashboard id, **so annotations created on source dashboard** will no longer be visible on the copy. You can still show them if you add a new Annotation Query and filter by tags. However, this only works if the annotations on the source dashboard had tags to filter by. This PR adds "the" so the phrase reads "so annotations created on the source dashboard" --- .../build-dashboards/annotate-visualizations/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md b/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md index 28aa183baeaa5..6eedd05a43e9e 100644 --- a/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md +++ b/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md @@ -136,7 +136,7 @@ You can stop annotations from being fetched and drawn by taking the following st 1. Find and click the **Annotations & Alerts (Built-in)** query to open it. 1. Click the **Enabled** toggle to turn it off. -When you copy a dashboard using the **Save As** feature it will get a new dashboard id, so annotations created on source dashboard will no longer be visible on the copy. You can still show them if you add a new **Annotation Query** and filter by tags. However, this only works if the annotations on the source dashboard had tags to filter by. +When you copy a dashboard using the **Save As** feature it will get a new dashboard id, so annotations created on the source dashboard will no longer be visible on the copy. You can still show them if you add a new **Annotation Query** and filter by tags. However, this only works if the annotations on the source dashboard had tags to filter by. Following are some query options specific to the built-in annotation query. From 29cf60988b89ab6731c51b965875078d680ac847 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 12 Oct 2023 08:29:06 -0700 Subject: [PATCH 003/160] Playlist: Use a different go struct for sql service vs k8s (#76393) --- .../kinds/core/playlist/schema-reference.md | 117 ----------- kinds/playlist/playlist_kind.cue | 47 ----- packages/grafana-schema/src/index.gen.ts | 9 - .../src/raw/playlist/x/playlist_types.gen.ts | 58 ------ pkg/apis/playlist/v0alpha1/conversions.go | 47 +++++ .../playlist/v0alpha1/conversions_test.go | 64 ++++++ pkg/apis/playlist/v0alpha1/legacy_storage.go | 42 +--- pkg/apis/playlist/v0alpha1/openapi.go | 115 ----------- pkg/apis/playlist/v0alpha1/register.go | 5 - pkg/apis/playlist/v0alpha1/types.go | 51 ++++- pkg/apis/playlist/v0alpha1/types_test.go | 42 ++++ .../v0alpha1/zz_generated.deepcopy.go | 32 ++- .../playlist/v0alpha1/zz_generated.openapi.go | 188 ++++++++++++++++++ pkg/kinds/playlist/playlist_gen.go | 39 ---- pkg/kinds/playlist/playlist_kind_gen.go | 79 -------- pkg/kinds/playlist/playlist_metadata_gen.go | 42 ---- pkg/kinds/playlist/playlist_spec_gen.go | 57 ------ pkg/kinds/playlist/playlist_status_gen.go | 74 ------- pkg/kindsysreport/codegen/report.json | 27 +-- pkg/registry/corekind/base_gen.go | 14 -- pkg/services/playlist/model.go | 53 +++-- .../playlist/playlistimpl/playlist.go | 22 +- pkg/services/store/kind/playlist/summary.go | 68 ------- .../store/kind/playlist/summary_test.go | 40 ---- pkg/services/store/kind/registry.go | 5 - pkg/services/store/kind/registry_test.go | 10 +- public/app/features/playlist/api.ts | 14 +- 27 files changed, 497 insertions(+), 864 deletions(-) delete mode 100644 docs/sources/developers/kinds/core/playlist/schema-reference.md delete mode 100644 kinds/playlist/playlist_kind.cue delete mode 100644 packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts create mode 100644 pkg/apis/playlist/v0alpha1/conversions.go create mode 100644 pkg/apis/playlist/v0alpha1/conversions_test.go delete mode 100644 pkg/apis/playlist/v0alpha1/openapi.go create mode 100644 pkg/apis/playlist/v0alpha1/types_test.go create mode 100644 pkg/apis/playlist/v0alpha1/zz_generated.openapi.go delete mode 100644 pkg/kinds/playlist/playlist_gen.go delete mode 100644 pkg/kinds/playlist/playlist_kind_gen.go delete mode 100644 pkg/kinds/playlist/playlist_metadata_gen.go delete mode 100644 pkg/kinds/playlist/playlist_spec_gen.go delete mode 100644 pkg/kinds/playlist/playlist_status_gen.go delete mode 100644 pkg/services/store/kind/playlist/summary.go delete mode 100644 pkg/services/store/kind/playlist/summary_test.go diff --git a/docs/sources/developers/kinds/core/playlist/schema-reference.md b/docs/sources/developers/kinds/core/playlist/schema-reference.md deleted file mode 100644 index 6864981b00c26..0000000000000 --- a/docs/sources/developers/kinds/core/playlist/schema-reference.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: Playlist kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## Playlist - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - -A playlist is a series of dashboards that is automatically rotated in the browser, on a configurable interval. - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -| Property | Type | Required | Default | Description | -|------------|---------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `interval` | string | **Yes** | `5m` | Interval sets the time between switching views in a playlist.
FIXME: Is this based on a standardized format or what options are available? Can datemath be used? | -| `name` | string | **Yes** | | Name of the playlist. | -| `uid` | string | **Yes** | | Unique playlist identifier. Generated on creation, either by the
creator of the playlist of by the application. | -| `items` | [PlaylistItem](#playlistitem)[] | No | | The ordered list of items that the playlist will iterate over.
FIXME! This should not be optional, but changing it makes the godegen awkward | - -### PlaylistItem - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | Type of the item.
Possible values are: `dashboard_by_uid`, `dashboard_by_id`, `dashboard_by_tag`. | -| `value` | string | **Yes** | | Value depends on type and describes the playlist item.

- dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
is not portable as the numerical identifier is non-deterministic between different instances.
Will be replaced by dashboard_by_uid in the future. (deprecated)
- dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
dashboards behind the tag will be added to the playlist.
- dashboard_by_uid: The value is the dashboard UID | -| `title` | string | No | | Title is an unused property -- it will be removed in the future | - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/kinds/playlist/playlist_kind.cue b/kinds/playlist/playlist_kind.cue deleted file mode 100644 index a87e1c01a1e95..0000000000000 --- a/kinds/playlist/playlist_kind.cue +++ /dev/null @@ -1,47 +0,0 @@ -package kind - -name: "Playlist" -maturity: "merged" -description: "A playlist is a series of dashboards that is automatically rotated in the browser, on a configurable interval." - -lineage: schemas: [{ - version: [0, 0] - schema: { - spec: { - // Unique playlist identifier. Generated on creation, either by the - // creator of the playlist of by the application. - uid: string - - // Name of the playlist. - name: string - - // Interval sets the time between switching views in a playlist. - // FIXME: Is this based on a standardized format or what options are available? Can datemath be used? - interval: string | *"5m" - - // The ordered list of items that the playlist will iterate over. - // FIXME! This should not be optional, but changing it makes the godegen awkward - items?: [...#PlaylistItem] - } @cuetsy(kind="interface") - - /////////////////////////////////////// - // Definitions (referenced above) are declared below - - #PlaylistItem: { - // Type of the item. - type: "dashboard_by_uid" | "dashboard_by_id" | "dashboard_by_tag" - // Value depends on type and describes the playlist item. - // - // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This - // is not portable as the numerical identifier is non-deterministic between different instances. - // Will be replaced by dashboard_by_uid in the future. (deprecated) - // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All - // dashboards behind the tag will be added to the playlist. - // - dashboard_by_uid: The value is the dashboard UID - value: string - - // Title is an unused property -- it will be removed in the future - title?: string - } @cuetsy(kind="interface") - } -}] diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 7d1d3d050681b..6576c1ce63f92 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -124,15 +124,6 @@ export type { // TODO generate code such that tsc enforces type compatibility between raw and veneer decls export type { LibraryPanel } from './veneer/librarypanel.types'; -// Raw generated types from Playlist kind. -export type { - Playlist, - PlaylistItem -} from './raw/playlist/x/playlist_types.gen'; - -// Raw generated enums and default consts from playlist kind. -export { defaultPlaylist } from './raw/playlist/x/playlist_types.gen'; - // Raw generated types from Preferences kind. export type { Preferences, diff --git a/packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts b/packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts deleted file mode 100644 index 4f98abe19bfb0..0000000000000 --- a/packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// TSResourceJenny -// LatestMajorsOrXJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -export interface PlaylistItem { - /** - * Title is an unused property -- it will be removed in the future - */ - title?: string; - /** - * Type of the item. - */ - type: ('dashboard_by_uid' | 'dashboard_by_id' | 'dashboard_by_tag'); - /** - * Value depends on type and describes the playlist item. - * - * - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This - * is not portable as the numerical identifier is non-deterministic between different instances. - * Will be replaced by dashboard_by_uid in the future. (deprecated) - * - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All - * dashboards behind the tag will be added to the playlist. - * - dashboard_by_uid: The value is the dashboard UID - */ - value: string; -} - -export interface Playlist { - /** - * Interval sets the time between switching views in a playlist. - * FIXME: Is this based on a standardized format or what options are available? Can datemath be used? - */ - interval: string; - /** - * The ordered list of items that the playlist will iterate over. - * FIXME! This should not be optional, but changing it makes the godegen awkward - */ - items?: Array; - /** - * Name of the playlist. - */ - name: string; - /** - * Unique playlist identifier. Generated on creation, either by the - * creator of the playlist of by the application. - */ - uid: string; -} - -export const defaultPlaylist: Partial = { - interval: '5m', - items: [], -}; diff --git a/pkg/apis/playlist/v0alpha1/conversions.go b/pkg/apis/playlist/v0alpha1/conversions.go new file mode 100644 index 0000000000000..fc393ffcae312 --- /dev/null +++ b/pkg/apis/playlist/v0alpha1/conversions.go @@ -0,0 +1,47 @@ +package v0alpha1 + +import ( + "fmt" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/grafana/grafana/pkg/services/playlist" +) + +type namespaceMapper = func(orgId int64) string + +func orgNamespaceMapper(orgId int64) string { + if orgId == 1 { + return "default" + } + return fmt.Sprintf("org-%d", orgId) +} + +func convertToK8sResource(v *playlist.PlaylistDTO, namespacer namespaceMapper) *Playlist { + spec := Spec{ + Title: v.Name, + Interval: v.Interval, + } + for _, item := range v.Items { + spec.Items = append(spec.Items, Item{ + Type: ItemType(item.Type), + Value: item.Value, + }) + } + return &Playlist{ + TypeMeta: metav1.TypeMeta{ + Kind: "Playlist", + APIVersion: APIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: v.Uid, + UID: types.UID(v.Uid), + ResourceVersion: fmt.Sprintf("%d", v.UpdatedAt), + CreationTimestamp: metav1.NewTime(time.UnixMilli(v.CreatedAt)), + Namespace: namespacer(v.OrgID), + }, + Spec: spec, + } +} diff --git a/pkg/apis/playlist/v0alpha1/conversions_test.go b/pkg/apis/playlist/v0alpha1/conversions_test.go new file mode 100644 index 0000000000000..48f63dde95ada --- /dev/null +++ b/pkg/apis/playlist/v0alpha1/conversions_test.go @@ -0,0 +1,64 @@ +package v0alpha1 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/playlist" +) + +func TestPlaylistConversion(t *testing.T) { + src := &playlist.PlaylistDTO{ + OrgID: 3, + Uid: "abc", // becomes k8s name + Name: "MyPlaylists", // becomes title + Interval: "10s", + CreatedAt: 12345, + UpdatedAt: 54321, + Items: []playlist.PlaylistItemDTO{ + {Type: "dashboard_by_uid", Value: "UID0"}, + {Type: "dashboard_by_tag", Value: "tagA"}, + {Type: "dashboard_by_id", Value: "123"}, // deprecated + }, + } + dst := convertToK8sResource(src, orgNamespaceMapper) + + require.Equal(t, "abc", src.Uid) + require.Equal(t, "abc", dst.Name) + require.Equal(t, src.Name, dst.Spec.Title) + + out, err := json.MarshalIndent(dst, "", " ") + require.NoError(t, err) + //fmt.Printf("%s", string(out)) + require.JSONEq(t, `{ + "kind": "Playlist", + "apiVersion": "playlist.x.grafana.com/v0alpha1", + "metadata": { + "name": "abc", + "namespace": "org-3", + "uid": "abc", + "resourceVersion": "54321", + "creationTimestamp": "1970-01-01T00:00:12Z" + }, + "spec": { + "title": "MyPlaylists", + "interval": "10s", + "items": [ + { + "type": "dashboard_by_uid", + "value": "UID0" + }, + { + "type": "dashboard_by_tag", + "value": "tagA" + }, + { + "type": "dashboard_by_id", + "value": "123" + } + ] + } + }`, string(out)) +} diff --git a/pkg/apis/playlist/v0alpha1/legacy_storage.go b/pkg/apis/playlist/v0alpha1/legacy_storage.go index 1880499ce4fa2..d476b6a228c94 100644 --- a/pkg/apis/playlist/v0alpha1/legacy_storage.go +++ b/pkg/apis/playlist/v0alpha1/legacy_storage.go @@ -9,7 +9,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - playlistkind "github.com/grafana/grafana/pkg/kinds/playlist" grafanarequest "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/playlist" ) @@ -81,21 +80,14 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO }, } for _, v := range res { - p := Playlist{ - TypeMeta: metav1.TypeMeta{ - Kind: "Playlist", - APIVersion: APIVersion, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: v.UID, - }, - Spec: playlistkind.Spec{ - Name: v.Name, - Uid: v.UID, - Interval: v.Interval, - }, + p, err := s.service.Get(ctx, &playlist.GetPlaylistByUidQuery{ + UID: v.UID, + OrgId: orgId, // required + }) + if err != nil { + return nil, err } - list.Items = append(list.Items, p) + list.Items = append(list.Items, *convertToK8sResource(p, orgNamespaceMapper)) } if len(list.Items) == limit { list.Continue = "" // TODO? @@ -109,30 +101,16 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge orgId = 1 // TODO: default org ID 1 for now } - p, err := s.service.Get(ctx, &playlist.GetPlaylistByUidQuery{ + dto, err := s.service.Get(ctx, &playlist.GetPlaylistByUidQuery{ UID: name, OrgId: orgId, }) if err != nil { return nil, err } - if p == nil { + if dto == nil { return nil, fmt.Errorf("not found?") } - return &Playlist{ - TypeMeta: metav1.TypeMeta{ - Kind: "Playlist", - APIVersion: APIVersion, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: p.Uid, - }, - Spec: playlistkind.Spec{ - Name: p.Name, - Uid: p.Uid, - Interval: p.Interval, - Items: p.Items, - }, - }, nil + return convertToK8sResource(dto, orgNamespaceMapper), nil } diff --git a/pkg/apis/playlist/v0alpha1/openapi.go b/pkg/apis/playlist/v0alpha1/openapi.go deleted file mode 100644 index 6b7123d06fa10..0000000000000 --- a/pkg/apis/playlist/v0alpha1/openapi.go +++ /dev/null @@ -1,115 +0,0 @@ -package v0alpha1 - -import ( - common "k8s.io/kube-openapi/pkg/common" - spec "k8s.io/kube-openapi/pkg/validation/spec" -) - -// NOTE: this must match the golang fully qualifid name! -const kindKey = "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Playlist" - -func getOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - return map[string]common.OpenAPIDefinition{ - kindKey: schema_pkg_Playlist(ref), - kindKey + "List": schema_pkg_PlaylistList(ref), - } -} - -func schema_pkg_Playlist(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Playlist", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - // TODO! add a real spec here - // "spec": { - // SchemaProps: spec.SchemaProps{ - // Default: map[string]interface{}{}, - // Ref: ref("github.com/grafana/google-sheets-datasource/pkg/apis/googlesheets/v1.DatasourceSpec"), - // }, - // }, - // "status": { - // SchemaProps: spec.SchemaProps{ - // Default: map[string]interface{}{}, - // Ref: ref("github.com/grafana/google-sheets-datasource/pkg/apis/googlesheets/v1.DatasourceStatus"), - // }, - // }, - }, - }, - }, - Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta", - }, - } -} - -func schema_pkg_PlaylistList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "PlaylistList", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(kindKey), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - kindKey, - "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} diff --git a/pkg/apis/playlist/v0alpha1/register.go b/pkg/apis/playlist/v0alpha1/register.go index 1c0e59edd4b6a..9a7335f5251e7 100644 --- a/pkg/apis/playlist/v0alpha1/register.go +++ b/pkg/apis/playlist/v0alpha1/register.go @@ -15,11 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/playlist" ) -// GroupName is the group name for this API. -const GroupName = "playlist.x.grafana.com" -const VersionID = "v0alpha1" // -const APIVersion = GroupName + "/" + VersionID - var _ grafanaapiserver.APIGroupBuilder = (*PlaylistAPIBuilder)(nil) // This is used just so wire has something unique to return diff --git a/pkg/apis/playlist/v0alpha1/types.go b/pkg/apis/playlist/v0alpha1/types.go index 2e94bd84f44d8..7a8a147fb2f5d 100644 --- a/pkg/apis/playlist/v0alpha1/types.go +++ b/pkg/apis/playlist/v0alpha1/types.go @@ -2,10 +2,13 @@ package v0alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/kinds/playlist" ) +// GroupName is the group name for this API. +const GroupName = "playlist.x.grafana.com" +const VersionID = "v0alpha1" // +const APIVersion = GroupName + "/" + VersionID + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type Playlist struct { metav1.TypeMeta `json:",inline"` @@ -14,7 +17,7 @@ type Playlist struct { // +optional metav1.ObjectMeta `json:"metadata,omitempty"` - Spec playlist.Spec `json:"spec,omitempty"` + Spec Spec `json:"spec,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -23,5 +26,45 @@ type PlaylistList struct { // +optional metav1.ListMeta `json:"metadata,omitempty"` - Items []Playlist `json:"playlists,omitempty"` + Items []Playlist `json:"items,omitempty"` } + +// Spec defines model for Spec. +type Spec struct { + // Name of the playlist. + Title string `json:"title"` + + // Interval sets the time between switching views in a playlist. + Interval string `json:"interval"` + + // The ordered list of items that the playlist will iterate over. + Items []Item `json:"items,omitempty"` +} + +// Defines values for ItemType. +const ( + ItemTypeDashboardByTag ItemType = "dashboard_by_tag" + ItemTypeDashboardByUid ItemType = "dashboard_by_uid" + + // deprecated -- should use UID + ItemTypeDashboardById ItemType = "dashboard_by_id" +) + +// Item defines model for Item. +type Item struct { + // Type of the item. + Type ItemType `json:"type"` + + // Value depends on type and describes the playlist item. + // + // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This + // is not portable as the numerical identifier is non-deterministic between different instances. + // Will be replaced by dashboard_by_uid in the future. (deprecated) + // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All + // dashboards behind the tag will be added to the playlist. + // - dashboard_by_uid: The value is the dashboard UID + Value string `json:"value"` +} + +// Type of the item. +type ItemType string diff --git a/pkg/apis/playlist/v0alpha1/types_test.go b/pkg/apis/playlist/v0alpha1/types_test.go new file mode 100644 index 0000000000000..8303335987248 --- /dev/null +++ b/pkg/apis/playlist/v0alpha1/types_test.go @@ -0,0 +1,42 @@ +package v0alpha1 + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestPlaylistClone(t *testing.T) { + src := Playlist{ + TypeMeta: metav1.TypeMeta{ + Kind: "Playlist", + APIVersion: APIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "TheUID", + ResourceVersion: "12345", + CreationTimestamp: metav1.NewTime(time.Now()), + Annotations: map[string]string{ + "grafana.com/updatedTime": time.Now().Format(time.RFC3339), + }, + }, + Spec: Spec{ + Title: "A title", + Interval: "20s", + Items: []Item{ + {Type: ItemTypeDashboardByTag, Value: "graph-ng"}, + }, + }, + } + copy := src.DeepCopyObject() + + json0, err := json.Marshal(src) + require.NoError(t, err) + json1, err := json.Marshal(copy) + require.NoError(t, err) + + require.JSONEq(t, string(json0), string(json1)) +} diff --git a/pkg/apis/playlist/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/playlist/v0alpha1/zz_generated.deepcopy.go index 66da954054f8f..74637ae2fb4fc 100644 --- a/pkg/apis/playlist/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/playlist/v0alpha1/zz_generated.deepcopy.go @@ -1,6 +1,8 @@ //go:build !ignore_autogenerated // +build !ignore_autogenerated +// SPDX-License-Identifier: AGPL-3.0-only + // Code generated by deepcopy-gen. DO NOT EDIT. package v0alpha1 @@ -9,11 +11,28 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Item) DeepCopyInto(out *Item) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Item. +func (in *Item) DeepCopy() *Item { + if in == nil { + return nil + } + out := new(Item) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Playlist) DeepCopyInto(out *Playlist) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) return } @@ -69,17 +88,22 @@ func (in *PlaylistList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *legacyStorage) DeepCopyInto(out *legacyStorage) { +func (in *Spec) DeepCopyInto(out *Spec) { *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Item, len(*in)) + copy(*out, *in) + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Storage. -func (in *legacyStorage) DeepCopy() *legacyStorage { +// 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(legacyStorage) + out := new(Spec) in.DeepCopyInto(out) return out } diff --git a/pkg/apis/playlist/v0alpha1/zz_generated.openapi.go b/pkg/apis/playlist/v0alpha1/zz_generated.openapi.go new file mode 100644 index 0000000000000..4be50c3a8f743 --- /dev/null +++ b/pkg/apis/playlist/v0alpha1/zz_generated.openapi.go @@ -0,0 +1,188 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by openapi-gen. DO NOT EDIT. + +// This file was autogenerated by openapi-gen. Do not edit it manually! + +package v0alpha1 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func getOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Item": schema_pkg_apis_playlist_v0alpha1_Item(ref), + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Playlist": schema_pkg_apis_playlist_v0alpha1_Playlist(ref), + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.PlaylistList": schema_pkg_apis_playlist_v0alpha1_PlaylistList(ref), + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Spec": schema_pkg_apis_playlist_v0alpha1_Spec(ref), + } +} + +func schema_pkg_apis_playlist_v0alpha1_Item(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Item defines model for Item.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Type of the item.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Description: "Value depends on type and describes the playlist item.\n\n - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This\n is not portable as the numerical identifier is non-deterministic between different instances.\n Will be replaced by dashboard_by_uid in the future. (deprecated)\n - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All\n dashboards behind the tag will be added to the playlist.\n - dashboard_by_uid: The value is the dashboard UID", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "value"}, + }, + }, + } +} + +func schema_pkg_apis_playlist_v0alpha1_Playlist(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Spec"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Spec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_playlist_v0alpha1_PlaylistList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Playlist"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Playlist", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_playlist_v0alpha1_Spec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Spec defines model for Spec.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Description: "Name of the playlist.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "interval": { + SchemaProps: spec.SchemaProps{ + Description: "Interval sets the time between switching views in a playlist.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Description: "The ordered list of items that the playlist will iterate over.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Item"), + }, + }, + }, + }, + }, + }, + Required: []string{"title", "interval"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Item"}, + } +} diff --git a/pkg/kinds/playlist/playlist_gen.go b/pkg/kinds/playlist/playlist_gen.go deleted file mode 100644 index 289e3cb5785c2..0000000000000 --- a/pkg/kinds/playlist/playlist_gen.go +++ /dev/null @@ -1,39 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoTypesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package playlist - -import ( - "github.com/grafana/grafana/pkg/kinds" -) - -// Resource is the kubernetes style representation of Playlist. (TODO be better) -type K8sResource = kinds.GrafanaResource[Spec, Status] - -// NewResource creates a new instance of the resource with a given name (UID) -func NewK8sResource(name string, s *Spec) K8sResource { - return K8sResource{ - Kind: "Playlist", - APIVersion: "v0-0-alpha", - Metadata: kinds.GrafanaResourceMetadata{ - Name: name, - Annotations: make(map[string]string), - Labels: make(map[string]string), - }, - Spec: s, - } -} - -// Resource is the wire representation of Playlist. -// It currently will soon be merged into the k8s flavor (TODO be better) -type Resource struct { - Metadata Metadata `json:"metadata"` - Spec Spec `json:"spec"` - Status Status `json:"status"` -} diff --git a/pkg/kinds/playlist/playlist_kind_gen.go b/pkg/kinds/playlist/playlist_kind_gen.go deleted file mode 100644 index d2f2a04cf4b65..0000000000000 --- a/pkg/kinds/playlist/playlist_kind_gen.go +++ /dev/null @@ -1,79 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// CoreKindJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package playlist - -import ( - "github.com/grafana/kindsys" - "github.com/grafana/thema" - "github.com/grafana/thema/vmux" - - "github.com/grafana/grafana/pkg/cuectx" -) - -// rootrel is the relative path from the grafana repository root to the -// directory containing the .cue files in which this kind is defined. Necessary -// for runtime errors related to the definition and/or lineage to provide -// a real path to the correct .cue file. -const rootrel string = "kinds/playlist" - -// TODO standard generated docs -type Kind struct { - kindsys.Core - lin thema.ConvergentLineage[*Resource] - jcodec vmux.Codec - valmux vmux.ValueMux[*Resource] -} - -// type guard - ensure generated Kind type satisfies the kindsys.Core interface -var _ kindsys.Core = &Kind{} - -// TODO standard generated docs -func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { - def, err := cuectx.LoadCoreKindDef(rootrel, rt.Context(), nil) - if err != nil { - return nil, err - } - - k := &Kind{} - k.Core, err = kindsys.BindCore(rt, def, opts...) - if err != nil { - return nil, err - } - // Get the thema.Schema that the meta says is in the current version (which - // codegen ensures is always the latest) - cursch := thema.SchemaP(k.Core.Lineage(), def.Properties.CurrentVersion) - tsch, err := thema.BindType(cursch, &Resource{}) - if err != nil { - // Should be unreachable, modulo bugs in the Thema->Go code generator - return nil, err - } - - k.jcodec = vmux.NewJSONCodec("playlist.json") - k.lin = tsch.ConvergentLineage() - k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec) - return k, nil -} - -// ConvergentLineage returns the same [thema.Lineage] as Lineage, but bound (see [thema.BindType]) -// to the the Playlist [Resource] type generated from the current schema, v0.0. -func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*Resource] { - return k.lin -} - -// JSONValueMux is a version multiplexer that maps a []byte containing JSON data -// at any schematized dashboard version to an instance of Playlist [Resource]. -// -// Validation and translation errors emitted from this func will identify the -// input bytes as "dashboard.json". -// -// This is a thin wrapper around Thema's [vmux.ValueMux]. -func (k *Kind) JSONValueMux(b []byte) (*Resource, thema.TranslationLacunas, error) { - return k.valmux(b) -} diff --git a/pkg/kinds/playlist/playlist_metadata_gen.go b/pkg/kinds/playlist/playlist_metadata_gen.go deleted file mode 100644 index f97a4b6eb0123..0000000000000 --- a/pkg/kinds/playlist/playlist_metadata_gen.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoResourceTypes -// -// Run 'make gen-cue' from repository root to regenerate. - -package playlist - -import ( - "time" -) - -// Metadata defines model for Metadata. -type Metadata struct { - CreatedBy string `json:"createdBy"` - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - - // extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - ExtraFields map[string]any `json:"extraFields"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` - UpdateTimestamp time.Time `json:"updateTimestamp"` - UpdatedBy string `json:"updatedBy"` -} - -// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -// It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -// As it is also intended to be generic enough to function with any API Server. -type KubeObjectMetadata struct { - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - Finalizers []string `json:"finalizers"` - Labels map[string]string `json:"labels"` - ResourceVersion string `json:"resourceVersion"` - Uid string `json:"uid"` -} diff --git a/pkg/kinds/playlist/playlist_spec_gen.go b/pkg/kinds/playlist/playlist_spec_gen.go deleted file mode 100644 index 211fb577b7f13..0000000000000 --- a/pkg/kinds/playlist/playlist_spec_gen.go +++ /dev/null @@ -1,57 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoResourceTypes -// -// Run 'make gen-cue' from repository root to regenerate. - -package playlist - -// Defines values for ItemType. -const ( - ItemTypeDashboardById ItemType = "dashboard_by_id" - ItemTypeDashboardByTag ItemType = "dashboard_by_tag" - ItemTypeDashboardByUid ItemType = "dashboard_by_uid" -) - -// Item defines model for Item. -type Item struct { - // Title is an unused property -- it will be removed in the future - Title *string `json:"title,omitempty"` - - // Type of the item. - Type ItemType `json:"type"` - - // Value depends on type and describes the playlist item. - // - // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This - // is not portable as the numerical identifier is non-deterministic between different instances. - // Will be replaced by dashboard_by_uid in the future. (deprecated) - // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All - // dashboards behind the tag will be added to the playlist. - // - dashboard_by_uid: The value is the dashboard UID - Value string `json:"value"` -} - -// Type of the item. -type ItemType string - -// Spec defines model for Spec. -type Spec struct { - // Interval sets the time between switching views in a playlist. - // FIXME: Is this based on a standardized format or what options are available? Can datemath be used? - Interval string `json:"interval"` - - // The ordered list of items that the playlist will iterate over. - // FIXME! This should not be optional, but changing it makes the godegen awkward - Items []Item `json:"items,omitempty"` - - // Name of the playlist. - Name string `json:"name"` - - // Unique playlist identifier. Generated on creation, either by the - // creator of the playlist of by the application. - Uid string `json:"uid"` -} diff --git a/pkg/kinds/playlist/playlist_status_gen.go b/pkg/kinds/playlist/playlist_status_gen.go deleted file mode 100644 index 33644fe4a8702..0000000000000 --- a/pkg/kinds/playlist/playlist_status_gen.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoResourceTypes -// -// Run 'make gen-cue' from repository root to regenerate. - -package playlist - -// Defines values for OperatorStateState. -const ( - OperatorStateStateFailed OperatorStateState = "failed" - OperatorStateStateInProgress OperatorStateState = "in_progress" - OperatorStateStateSuccess OperatorStateState = "success" -) - -// Defines values for StatusOperatorStateState. -const ( - StatusOperatorStateStateFailed StatusOperatorStateState = "failed" - StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" - StatusOperatorStateStateSuccess StatusOperatorStateState = "success" -) - -// OperatorState defines model for OperatorState. -type OperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State OperatorStateState `json:"state"` -} - -// OperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type OperatorStateState string - -// Status defines model for Status. -type Status struct { - // additionalFields is reserved for future use - AdditionalFields map[string]any `json:"additionalFields,omitempty"` - - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"` -} - -// StatusOperatorState defines model for status.#OperatorState. -type StatusOperatorState struct { - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - - // details contains any extra information that is operator-specific - Details map[string]any `json:"details,omitempty"` - - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State StatusOperatorStateState `json:"state"` -} - -// StatusOperatorStateState state describes the state of the lastEvaluation. -// It is limited to three possible states for machine evaluation. -type StatusOperatorStateState string diff --git a/pkg/kindsysreport/codegen/report.json b/pkg/kindsysreport/codegen/report.json index 2aee0908d05c7..dca8c8bd03f92 100644 --- a/pkg/kindsysreport/codegen/report.json +++ b/pkg/kindsysreport/codegen/report.json @@ -1322,31 +1322,26 @@ }, "playlist": { "category": "core", - "codeowners": [ - "grafana/grafana-as-code", - "grafana/grafana-frontend-platform", - "grafana/plugins-platform-frontend" - ], + "codeowners": [], "crd": { "dummySchema": false, - "group": "playlist.core.grafana.com", - "scope": "Namespaced" + "group": "", + "scope": "" }, "currentVersion": [ 0, 0 ], - "description": "A playlist is a series of dashboards that is automatically rotated in the browser, on a configurable interval.", "grafanaMaturityCount": 0, "lineageIsGroup": false, "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/playlist/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/playlist", - "schema": "https://github.com/grafana/grafana/tree/main/kinds/playlist/playlist_kind.cue", - "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts" + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" }, "machineName": "playlist", - "maturity": "merged", + "maturity": "planned", "name": "Playlist", "pluralMachineName": "playlists", "pluralName": "Playlists" @@ -2277,7 +2272,6 @@ "folder", "googlecloudmonitoringdataquery", "heatmappanelcfg", - "playlist", "preferences", "publicdashboard", "role", @@ -2286,7 +2280,7 @@ "timeseriespanelcfg", "trendpanelcfg" ], - "count": 14 + "count": 13 }, "planned": { "name": "planned", @@ -2319,6 +2313,7 @@ "mysqldataquery", "mysqldatasourcecfg", "parcadatasourcecfg", + "playlist", "postgresqldataquery", "postgresqldatasourcecfg", "prometheusdatasourcecfg", @@ -2335,7 +2330,7 @@ "zipkindataquery", "zipkindatasourcecfg" ], - "count": 43 + "count": 44 }, "stable": { "name": "stable", diff --git a/pkg/registry/corekind/base_gen.go b/pkg/registry/corekind/base_gen.go index f969ff245eae4..5f72862b04a0e 100644 --- a/pkg/registry/corekind/base_gen.go +++ b/pkg/registry/corekind/base_gen.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/kinds/folder" "github.com/grafana/grafana/pkg/kinds/librarypanel" - "github.com/grafana/grafana/pkg/kinds/playlist" "github.com/grafana/grafana/pkg/kinds/preferences" "github.com/grafana/grafana/pkg/kinds/publicdashboard" "github.com/grafana/grafana/pkg/kinds/role" @@ -47,7 +46,6 @@ type Base struct { dashboard *dashboard.Kind folder *folder.Kind librarypanel *librarypanel.Kind - playlist *playlist.Kind preferences *preferences.Kind publicdashboard *publicdashboard.Kind role *role.Kind @@ -61,7 +59,6 @@ var ( _ kindsys.Core = &dashboard.Kind{} _ kindsys.Core = &folder.Kind{} _ kindsys.Core = &librarypanel.Kind{} - _ kindsys.Core = &playlist.Kind{} _ kindsys.Core = &preferences.Kind{} _ kindsys.Core = &publicdashboard.Kind{} _ kindsys.Core = &role.Kind{} @@ -89,11 +86,6 @@ func (b *Base) LibraryPanel() *librarypanel.Kind { return b.librarypanel } -// Playlist returns the [kindsys.Interface] implementation for the playlist kind. -func (b *Base) Playlist() *playlist.Kind { - return b.playlist -} - // Preferences returns the [kindsys.Interface] implementation for the preferences kind. func (b *Base) Preferences() *preferences.Kind { return b.preferences @@ -147,12 +139,6 @@ func doNewBase(rt *thema.Runtime) *Base { } reg.all = append(reg.all, reg.librarypanel) - reg.playlist, err = playlist.NewKind(rt) - if err != nil { - panic(fmt.Sprintf("error while initializing the playlist Kind: %s", err)) - } - reg.all = append(reg.all, reg.playlist) - reg.preferences, err = preferences.NewKind(rt) if err != nil { panic(fmt.Sprintf("error while initializing the preferences Kind: %s", err)) diff --git a/pkg/services/playlist/model.go b/pkg/services/playlist/model.go index f0a0b4b4ae784..178bd7c83f803 100644 --- a/pkg/services/playlist/model.go +++ b/pkg/services/playlist/model.go @@ -2,8 +2,6 @@ package playlist import ( "errors" - - "github.com/grafana/grafana/pkg/kinds/playlist" ) // Typed errors @@ -27,9 +25,47 @@ type Playlist struct { UpdatedAt int64 `json:"-" db:"updated_at"` } -type PlaylistDTO = playlist.Spec -type PlaylistItemDTO = playlist.Item -type PlaylistItemType = playlist.ItemType +type PlaylistDTO struct { + // Unique playlist identifier. Generated on creation, either by the + // creator of the playlist of by the application. + Uid string `json:"uid"` + + // Name of the playlist. + Name string `json:"name"` + + // Interval sets the time between switching views in a playlist. + Interval string `json:"interval"` + + // The ordered list of items that the playlist will iterate over. + Items []PlaylistItemDTO `json:"items,omitempty"` + + // Returned for k8s + CreatedAt int64 `json:"-"` + + // Returned for k8s + UpdatedAt int64 `json:"-"` + + // Returned for k8s + OrgID int64 `json:"-"` +} + +type PlaylistItemDTO struct { + // Title is an unused property -- it will be removed in the future + Title *string `json:"title,omitempty"` + + // Type of the item. + Type string `json:"type"` + + // Value depends on type and describes the playlist item. + // + // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This + // is not portable as the numerical identifier is non-deterministic between different instances. + // Will be replaced by dashboard_by_uid in the future. (deprecated) + // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All + // dashboards behind the tag will be added to the playlist. + // - dashboard_by_uid: The value is the dashboard UID + Value string `json:"value"` +} type PlaylistItem struct { Id int64 `db:"id"` @@ -88,10 +124,3 @@ type GetPlaylistItemsByUidQuery struct { PlaylistUID string OrgId int64 } - -func PlaylistToResource(p PlaylistDTO) playlist.K8sResource { - copy := p - r := playlist.NewK8sResource(p.Uid, ©) - copy.Uid = "" // remove it from the payload - return r -} diff --git a/pkg/services/playlist/playlistimpl/playlist.go b/pkg/services/playlist/playlistimpl/playlist.go index 3111d701e5809..ee6a000c62799 100644 --- a/pkg/services/playlist/playlistimpl/playlist.go +++ b/pkg/services/playlist/playlistimpl/playlist.go @@ -4,9 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/playlist" - "github.com/grafana/grafana/pkg/services/store/entity" ) type Service struct { @@ -15,11 +13,10 @@ type Service struct { var _ playlist.Service = &Service{} -func ProvideService(db db.DB, toggles featuremgmt.FeatureToggles, objserver entity.EntityStoreServer) playlist.Service { - sqlstore := &sqlStore{ +func ProvideService(db db.DB) playlist.Service { + return &Service{store: &sqlStore{ db: db, - } - return &Service{store: sqlstore} + }} } func (s *Service) Create(ctx context.Context, cmd *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) { @@ -48,7 +45,7 @@ func (s *Service) Get(ctx context.Context, q *playlist.GetPlaylistByUidQuery) (* } items := make([]playlist.PlaylistItemDTO, len(rawItems)) for i := 0; i < len(rawItems); i++ { - items[i].Type = playlist.PlaylistItemType(rawItems[i].Type) + items[i].Type = rawItems[i].Type items[i].Value = rawItems[i].Value // Add the unused title to the result @@ -58,10 +55,13 @@ func (s *Service) Get(ctx context.Context, q *playlist.GetPlaylistByUidQuery) (* } } return &playlist.PlaylistDTO{ - Uid: v.UID, - Name: v.Name, - Interval: v.Interval, - Items: items, + Uid: v.UID, + Name: v.Name, + Interval: v.Interval, + Items: items, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + OrgID: v.OrgId, }, nil } diff --git a/pkg/services/store/kind/playlist/summary.go b/pkg/services/store/kind/playlist/summary.go deleted file mode 100644 index 8511ae3e511c7..0000000000000 --- a/pkg/services/store/kind/playlist/summary.go +++ /dev/null @@ -1,68 +0,0 @@ -package playlist - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/grafana/grafana/pkg/kinds/playlist" - "github.com/grafana/grafana/pkg/services/store/entity" -) - -func GetEntityKindInfo() entity.EntityKindInfo { - return entity.EntityKindInfo{ - ID: entity.StandardKindPlaylist, - Name: "Playlist", - Description: "Cycle though a collection of dashboards automatically", - } -} - -func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { - return summaryBuilder -} - -func summaryBuilder(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { - obj := &playlist.Spec{} - err := json.Unmarshal(body, obj) - if err != nil { - return nil, nil, err // unable to read object - } - - // TODO: fix model so this is not possible - if obj.Items == nil { - temp := make([]playlist.Item, 0) - obj.Items = temp - } - - obj.Uid = uid // make sure they are consistent - summary := &entity.EntitySummary{ - UID: uid, - Name: obj.Name, - Description: fmt.Sprintf("%d items, refreshed every %s", len(obj.Items), obj.Interval), - } - - for _, item := range obj.Items { - switch item.Type { - case playlist.ItemTypeDashboardByUid: - summary.References = append(summary.References, &entity.EntityExternalReference{ - Family: entity.StandardKindDashboard, - Identifier: item.Value, - }) - - case playlist.ItemTypeDashboardByTag: - if summary.Labels == nil { - summary.Labels = make(map[string]string, 0) - } - summary.Labels[item.Value] = "" - - case playlist.ItemTypeDashboardById: - // obviously insufficient long term... but good to have an example :) - summary.Error = &entity.EntityErrorInfo{ - Message: "Playlist uses deprecated internal id system", - } - } - } - - out, err := json.MarshalIndent(obj, "", " ") - return summary, out, err -} diff --git a/pkg/services/store/kind/playlist/summary_test.go b/pkg/services/store/kind/playlist/summary_test.go deleted file mode 100644 index 7a9186cbc37e1..0000000000000 --- a/pkg/services/store/kind/playlist/summary_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package playlist - -import ( - "context" - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/kinds/playlist" -) - -func TestPlaylistSummary(t *testing.T) { - builder := GetEntitySummaryBuilder() - - // Do not parse invalid input - _, _, err := builder(context.Background(), "abc", []byte("{invalid json")) - require.Error(t, err) - - playlist := playlist.Spec{ - Interval: "30s", - Name: "test", - Items: []playlist.Item{ - {Type: playlist.ItemTypeDashboardByUid, Value: "D1"}, - {Type: playlist.ItemTypeDashboardByTag, Value: "tagA"}, - {Type: playlist.ItemTypeDashboardByUid, Value: "D3"}, - }, - } - out, err := json.Marshal(playlist) - require.NoError(t, err) - require.NotNil(t, out) - - // Do not parse invalid input - summary, body, err := builder(context.Background(), "abc", out) - require.NoError(t, err) - require.Equal(t, "test", summary.Name) - require.Equal(t, 2, len(summary.References)) - require.Equal(t, map[string]string{"tagA": ""}, summary.Labels) - require.True(t, json.Valid(body)) -} diff --git a/pkg/services/store/kind/registry.go b/pkg/services/store/kind/registry.go index a43d7c7a592e3..d6c9f6d27a551 100644 --- a/pkg/services/store/kind/registry.go +++ b/pkg/services/store/kind/registry.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/services/store/kind/folder" "github.com/grafana/grafana/pkg/services/store/kind/geojson" "github.com/grafana/grafana/pkg/services/store/kind/jsonobj" - "github.com/grafana/grafana/pkg/services/store/kind/playlist" "github.com/grafana/grafana/pkg/services/store/kind/png" "github.com/grafana/grafana/pkg/services/store/kind/preferences" "github.com/grafana/grafana/pkg/services/store/kind/snapshot" @@ -30,10 +29,6 @@ type KindRegistry interface { func NewKindRegistry() KindRegistry { kinds := make(map[string]*kindValues) - kinds[entity.StandardKindPlaylist] = &kindValues{ - info: playlist.GetEntityKindInfo(), - builder: playlist.GetEntitySummaryBuilder(), - } kinds[entity.StandardKindDashboard] = &kindValues{ info: dashboard.GetEntityKindInfo(), builder: dashboard.GetEntitySummaryBuilder(), diff --git a/pkg/services/store/kind/registry_test.go b/pkg/services/store/kind/registry_test.go index 9443edc1263aa..a9e501ed5f860 100644 --- a/pkg/services/store/kind/registry_test.go +++ b/pkg/services/store/kind/registry_test.go @@ -5,7 +5,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/kind/dummy" ) @@ -24,21 +23,14 @@ func TestKindRegistry(t *testing.T) { "frame", "geojson", "jsonobj", - "playlist", "png", "preferences", "snapshot", "test", }, ids) - // Check playlist exists - info, err := registry.GetInfo(entity.StandardKindPlaylist) - require.NoError(t, err) - require.Equal(t, "Playlist", info.Name) - require.False(t, info.IsRaw) - // Check that we registered a test item - info, err = registry.GetInfo("test") + info, err := registry.GetInfo("test") require.NoError(t, err) require.Equal(t, "test", info.Name) require.True(t, info.IsRaw) diff --git a/public/app/features/playlist/api.ts b/public/app/features/playlist/api.ts index f5987ad5badcb..a24152ada1189 100644 --- a/public/app/features/playlist/api.ts +++ b/public/app/features/playlist/api.ts @@ -46,7 +46,7 @@ interface K8sPlaylist { name: string; }; spec: { - name: string; + title: string; interval: string; items: PlaylistItem[]; }; @@ -58,10 +58,7 @@ class K8sAPI implements PlaylistAPI { async getAllPlaylist(): Promise { const result = await getBackendSrv().get(this.url); - console.log('getAllPlaylist', result); - const v = result.playlists.map(k8sResourceAsPlaylist); - console.log('after', v); - return v; + return result.playlists.map(k8sResourceAsPlaylist); } async getPlaylist(uid: string): Promise { @@ -118,9 +115,12 @@ class K8sAPI implements PlaylistAPI { // the main difference is that k8s uses metdata.name as the uid // to avoid future confusion, the display name is now called "title" function k8sResourceAsPlaylist(r: K8sPlaylist): Playlist { + const { spec, metadata } = r; return { - ...r.spec, - uid: r.metadata.name, // replace the uid from the k8s name + uid: metadata.name, // use the k8s name as uid + name: spec.title, + interval: spec.interval, + items: spec.items, }; } From bdeb829cf6fdafbe2b6ba6dcaa1ce6c5bc637cdb Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Thu, 12 Oct 2023 18:31:49 +0300 Subject: [PATCH 004/160] Revert "Nested Folders: Fix fetching a folder by title" (#76469) Revert "Nested Folders: Fix fetching a folder by title (#74725)" This reverts commit 0eac9aff7f82a6953f82415f757ad05aac266654. --- pkg/services/folder/folderimpl/sqlstore.go | 10 +---- .../folder/folderimpl/sqlstore_test.go | 38 +++---------------- pkg/services/folder/model.go | 14 +++---- pkg/services/folder/service.go | 5 +-- 4 files changed, 14 insertions(+), 53 deletions(-) diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index dee0ceedf8d8f..7bac08fe0d74c 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -166,15 +166,7 @@ func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.F case q.ID != nil: exists, err = sess.SQL("SELECT * FROM folder WHERE id = ?", q.ID).Get(foldr) case q.Title != nil: - args := []any{*q.Title, q.OrgID} - s := "SELECT * FROM folder WHERE title = ? AND org_id = ?" - if q.ParentUID != nil { - s = s + " AND parent_uid = ?" - args = append(args, *q.ParentUID) - } else { - s = s + " AND parent_uid IS NULL" - } - exists, err = sess.SQL(s, args...).Get(foldr) + exists, err = sess.SQL("SELECT * FROM folder WHERE title = ? AND org_id = ?", q.Title, q.OrgID).Get(foldr) default: return folder.ErrBadRequest.Errorf("one of ID, UID, or Title must be included in the command") } diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go index 82d32cd325923..21f7ba880099d 100644 --- a/pkg/services/folder/folderimpl/sqlstore_test.go +++ b/pkg/services/folder/folderimpl/sqlstore_test.go @@ -384,17 +384,6 @@ func TestIntegrationGet(t *testing.T) { }) require.NoError(t, err) - // create subfolder with same title - subfolderUID := util.GenerateShortUID() - subfolder, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ - Title: folderTitle, - Description: folderDsc, - OrgID: orgID, - UID: subfolderUID, - ParentUID: f.UID, - }) - require.NoError(t, err) - t.Cleanup(func() { err := folderStore.Delete(context.Background(), f.UID, orgID) require.NoError(t, err) @@ -416,14 +405,15 @@ func TestIntegrationGet(t *testing.T) { assert.Equal(t, f.OrgID, ff.OrgID) assert.Equal(t, f.Title, ff.Title) assert.Equal(t, f.Description, ff.Description) + //assert.Equal(t, folder.GeneralFolderUID, ff.ParentUID) assert.NotEmpty(t, ff.Created) assert.NotEmpty(t, ff.Updated) assert.NotEmpty(t, ff.URL) }) - t.Run("get folder by title should return folder under root", func(t *testing.T) { + t.Run("get folder by title should succeed", func(t *testing.T) { ff, err := folderStore.Get(context.Background(), folder.GetFolderQuery{ - Title: &folderTitle, + Title: &f.Title, OrgID: orgID, }) require.NoError(t, err) @@ -432,30 +422,13 @@ func TestIntegrationGet(t *testing.T) { assert.Equal(t, f.OrgID, ff.OrgID) assert.Equal(t, f.Title, ff.Title) assert.Equal(t, f.Description, ff.Description) + //assert.Equal(t, folder.GeneralFolderUID, ff.ParentUID) assert.NotEmpty(t, ff.Created) assert.NotEmpty(t, ff.Updated) assert.NotEmpty(t, ff.URL) }) - t.Run("get folder by title and parent UID should return subfolder", func(t *testing.T) { - ff, err := folderStore.Get(context.Background(), folder.GetFolderQuery{ - Title: &folderTitle, - OrgID: orgID, - ParentUID: &f.UID, - }) - require.NoError(t, err) - assert.Equal(t, subfolder.ID, ff.ID) - assert.Equal(t, subfolder.UID, ff.UID) - assert.Equal(t, subfolder.OrgID, ff.OrgID) - assert.Equal(t, subfolder.Title, ff.Title) - assert.Equal(t, subfolder.Description, ff.Description) - assert.Equal(t, subfolder.ParentUID, ff.ParentUID) - assert.NotEmpty(t, subfolder.Created) - assert.NotEmpty(t, subfolder.Updated) - assert.NotEmpty(t, subfolder.URL) - }) - - t.Run("get folder by ID should succeed", func(t *testing.T) { + t.Run("get folder by title should succeed", func(t *testing.T) { ff, err := folderStore.Get(context.Background(), folder.GetFolderQuery{ ID: &f.ID, }) @@ -465,6 +438,7 @@ func TestIntegrationGet(t *testing.T) { assert.Equal(t, f.OrgID, ff.OrgID) assert.Equal(t, f.Title, ff.Title) assert.Equal(t, f.Description, ff.Description) + //assert.Equal(t, folder.GeneralFolderUID, ff.ParentUID) assert.NotEmpty(t, ff.Created) assert.NotEmpty(t, ff.Updated) assert.NotEmpty(t, ff.URL) diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 13e62c196cac2..b2674134bc392 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -125,15 +125,13 @@ type DeleteFolderCommand struct { // GetFolderQuery is used for all folder Get requests. Only one of UID, ID, or // Title should be set; if multiple fields are set by the caller the dashboard -// service will select the field with the most specificity, in order: UID, ID -// Title. If Title is set, it will fetch the folder in the root folder. -// Callers can additionally set the ParentUID field to fetch a folder by title under a specific folder. +// service will select the field with the most specificity, in order: ID, UID, +// Title. type GetFolderQuery struct { - UID *string - ParentUID *string - ID *int64 - Title *string - OrgID int64 + UID *string + ID *int64 + Title *string + OrgID int64 SignedInUser identity.Requester `json:"-"` } diff --git a/pkg/services/folder/service.go b/pkg/services/folder/service.go index f3dda822d652e..0d7a8218651f4 100644 --- a/pkg/services/folder/service.go +++ b/pkg/services/folder/service.go @@ -15,10 +15,7 @@ type Service interface { // GetFolder takes a GetFolderCommand and returns a folder matching the // request. One of ID, UID, or Title must be included. If multiple values // are included in the request, Grafana will select one in order of - // specificity (UID, ID,Title). - // If Title is set, it will fetch the folder in the root folder. - // If nested folders are enabled, callers can additionally set the ParentUID field - // to fetch a folder by title under a specific folder. + // specificity (ID, UID, Title). Get(ctx context.Context, cmd *GetFolderQuery) (*Folder, error) // Update is used to update a folder's UID, Title and Description. To change From c4ac4eb41b0917f1b7c6fb233ecd45d9b81ac23a Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 12 Oct 2023 17:10:08 +0100 Subject: [PATCH 005/160] Alerting: Export of notification policies to HCL (#76411) --- pkg/services/ngalert/api/api_provisioning.go | 15 ++--- .../ngalert/api/api_provisioning_test.go | 15 +++++ pkg/services/ngalert/api/compat.go | 60 +++++++++++++++---- .../definitions/provisioning_policies.go | 30 ++++++---- .../export/GrafanaPoliciesExporter.tsx | 4 +- 5 files changed, 92 insertions(+), 32 deletions(-) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index bd825349c2863..af8feda9eb7d1 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -557,13 +557,14 @@ func exportHcl(download bool, body definitions.AlertingFileExport) response.Resp // Body: &cp, // }) // } - // for idx, cp := range ex.Policies { - // resources = append(resources, resourceBlock{ - // Type: "grafana_notification_policy", - // Name: fmt.Sprintf("notification_policy_%d", idx), - // Body: &cp, - // }) - // + for idx, cp := range body.Policies { + policy := cp.Policy + resources = append(resources, hcl.Resource{ + Type: "grafana_notification_policy", + Name: fmt.Sprintf("notification_policy_%d", idx+1), + Body: policy, + }) + } hclBody, err := hcl.Encode(resources...) if err != nil { diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 5128072c0396f..e067861b5d38c 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -1134,6 +1134,21 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, 200, response.Status()) require.Equal(t, expectedResponse, string(response.Body())) }) + + t.Run("hcl body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + sut.policies = createFakeNotificationPolicyService() + rc := createTestRequestCtx() + + rc.Context.Req.Form.Add("format", "hcl") + expectedResponse := "resource \"grafana_notification_policy\" \"notification_policy_1\" {\n contact_point = \"default-receiver\"\n group_by = [\"g1\", \"g2\"]\n\n policy {\n contact_point = \"nested-receiver\"\n group_by = [\"g3\", \"g4\"]\n\n matcher {\n label = \"foo\"\n match = \"=\"\n value = \"bar\"\n }\n\n mute_timings = [\"interval\"]\n continue = true\n group_wait = \"5m\"\n group_interval = \"5m\"\n repeat_interval = \"5m\"\n }\n\n group_wait = \"30s\"\n group_interval = \"5m\"\n repeat_interval = \"1h\"\n}\n" + + response := sut.RouteGetPolicyTreeExport(&rc) + + t.Log(string(response.Body())) + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) }) }) } diff --git a/pkg/services/ngalert/api/compat.go b/pkg/services/ngalert/api/compat.go index 899609a7dc3a1..8a901c6de1a1c 100644 --- a/pkg/services/ngalert/api/compat.go +++ b/pkg/services/ngalert/api/compat.go @@ -281,18 +281,36 @@ func AlertingFileExportFromRoute(orgID int64, route definitions.Route) (definiti // RouteExportFromRoute creates a definitions.RouteExport DTO from definitions.Route. func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport { + toStringIfNotNil := func(d *model.Duration) *string { + if d == nil { + return nil + } + s := d.String() + return &s + } + + matchers := make([]*definitions.MatcherExport, 0, len(route.ObjectMatchers)) + for _, matcher := range route.ObjectMatchers { + matchers = append(matchers, &definitions.MatcherExport{ + Label: matcher.Name, + Match: matcher.Type.String(), + Value: matcher.Value, + }) + } + export := definitions.RouteExport{ - Receiver: route.Receiver, - GroupByStr: route.GroupByStr, - Match: route.Match, - MatchRE: route.MatchRE, - Matchers: route.Matchers, - ObjectMatchers: route.ObjectMatchers, - MuteTimeIntervals: route.MuteTimeIntervals, - Continue: route.Continue, - GroupWait: route.GroupWait, - GroupInterval: route.GroupInterval, - RepeatInterval: route.RepeatInterval, + Receiver: route.Receiver, + GroupByStr: NilIfEmpty(util.Pointer(route.GroupByStr)), + Match: route.Match, + MatchRE: route.MatchRE, + Matchers: route.Matchers, + ObjectMatchers: route.ObjectMatchers, + ObjectMatchersSlice: matchers, + MuteTimeIntervals: NilIfEmpty(util.Pointer(route.MuteTimeIntervals)), + Continue: OmitDefault(util.Pointer(route.Continue)), + GroupWait: toStringIfNotNil(route.GroupWait), + GroupInterval: toStringIfNotNil(route.GroupInterval), + RepeatInterval: toStringIfNotNil(route.RepeatInterval), } if len(route.Routes) > 0 { @@ -304,3 +322,23 @@ func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport { return &export } + +// OmitDefault returns nil if the value is the default. +func OmitDefault[T comparable](v *T) *T { + var def T + if v == nil { + return v + } + if *v == def { + return nil + } + return v +} + +// NilIfEmpty returns nil if pointer to slice points to the empty slice. +func NilIfEmpty[T any](v *[]T) *[]T { + if v == nil || len(*v) == 0 { + return nil + } + return v +} diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go index 697fa1937a252..480339895ee44 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go @@ -2,7 +2,6 @@ package definitions import ( "github.com/prometheus/alertmanager/config" - "github.com/prometheus/common/model" ) // swagger:route GET /api/v1/provisioning/policies provisioning stable RouteGetPolicyTree @@ -58,20 +57,27 @@ type NotificationPolicyExport struct { // RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in // provisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them. type RouteExport struct { - Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` + Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"` - GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"` + GroupByStr *[]string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"` // Deprecated. Remove before v1.0 release. Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` // Deprecated. Remove before v1.0 release. - MatchRE config.MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` - Matchers config.Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` - ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"` - MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` - Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` // Added omitempty to yaml for a cleaner export. - Routes []*RouteExport `yaml:"routes,omitempty" json:"routes,omitempty"` + MatchRE config.MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` + Matchers config.Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` + ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"` + ObjectMatchersSlice []*MatcherExport `yaml:"-" json:"-" hcl:"matcher,block"` + MuteTimeIntervals *[]string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings"` + Continue *bool `yaml:"continue,omitempty" json:"continue,omitempty" hcl:"continue,optional"` // Added omitempty to yaml for a cleaner export. + Routes []*RouteExport `yaml:"routes,omitempty" json:"routes,omitempty" hcl:"policy,block"` - GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` - GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` - RepeatInterval *model.Duration `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"` + GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"` + GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"` + RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"` +} + +type MatcherExport struct { + Label string `yaml:"-" json:"-" hcl:"label"` + Match string `yaml:"-" json:"-" hcl:"match"` + Value string `yaml:"-" json:"-" hcl:"value"` } diff --git a/public/app/features/alerting/unified/components/export/GrafanaPoliciesExporter.tsx b/public/app/features/alerting/unified/components/export/GrafanaPoliciesExporter.tsx index 7cf5d82dd35bf..c0cf2e0083b11 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaPoliciesExporter.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaPoliciesExporter.tsx @@ -6,7 +6,7 @@ import { alertRuleApi } from '../../api/alertRuleApi'; import { FileExportPreview } from './FileExportPreview'; import { GrafanaExportDrawer } from './GrafanaExportDrawer'; -import { ExportFormats, jsonAndYamlGrafanaExportProviders } from './providers'; +import { allGrafanaExportProviders, ExportFormats } from './providers'; interface GrafanaPoliciesPreviewProps { exportFormat: ExportFormats; onClose: () => void; @@ -45,7 +45,7 @@ export const GrafanaPoliciesExporter = ({ onClose }: GrafanaPoliciesExporterProp activeTab={activeTab} onTabChange={setActiveTab} onClose={onClose} - formatProviders={jsonAndYamlGrafanaExportProviders} + formatProviders={Object.values(allGrafanaExportProviders)} > From 9d8be05cce419f2d7eb9771bb5fc123adc0aea60 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Thu, 12 Oct 2023 18:33:18 +0200 Subject: [PATCH 006/160] Test data source: Add random node radius for nodegraph (#75381) --- .../datasource/grafana-testdata-datasource/nodeGraphUtils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts b/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts index c0630dd1c4b19..cd74289ba7fed 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts @@ -139,6 +139,8 @@ export function generateRandomNodes(count = 10) { nodeFields.arc__errors.values.push(node.error); const rnd = Math.random(); nodeFields[NodeGraphDataFrameFieldNames.icon].values.push(rnd > 0.9 ? 'database' : rnd < 0.1 ? 'cloud' : ''); + nodeFields[NodeGraphDataFrameFieldNames.nodeRadius].values.push(rnd > 0.5 ? 30 : 40); + for (const edge of node.edges) { const id = `${node.id}--${edge}`; // We can have duplicate edges when we added some more by random From 67e3e576d0c0999933435f8d50517cd0061617a7 Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Thu, 12 Oct 2023 12:46:49 -0400 Subject: [PATCH 007/160] Prometheus: Fix config bug for sigv4 auth (#76390) fix config bug for selecting sigv4 --- .../DataSourceHttpSettingsOverhaul.tsx | 22 +++++++++---------- .../plugins/datasource/prometheus/types.ts | 1 + 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/configuration/DataSourceHttpSettingsOverhaul.tsx b/public/app/plugins/datasource/prometheus/configuration/DataSourceHttpSettingsOverhaul.tsx index 6f14514444119..51b84c12f0e74 100644 --- a/public/app/plugins/datasource/prometheus/configuration/DataSourceHttpSettingsOverhaul.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/DataSourceHttpSettingsOverhaul.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { DataSourceSettings } from '@grafana/data'; -import { Auth, ConnectionSettings, convertLegacyAuthProps } from '@grafana/experimental'; +import { Auth, ConnectionSettings, convertLegacyAuthProps, AuthMethod } from '@grafana/experimental'; import { SecureSocksProxySettings, useTheme2 } from '@grafana/ui'; import { AzureAuthSettings } from '@grafana/ui/src/components/DataSourceSettings/types'; @@ -127,21 +127,12 @@ export const DataSourcehttpSettingsOverhaul = (props: Props) => { />
{ - // handle selecting of custom methods // sigV4Id if (sigV4AuthToggleEnabled) { setSigV4Selected(method === sigV4Id); - // mutate jsonData here to store the selected option because of auth component issue with onOptionsChange being overridden - options.jsonData.sigV4Auth = method === sigV4Id; } // Azure @@ -150,7 +141,16 @@ export const DataSourcehttpSettingsOverhaul = (props: Props) => { azureAuthSettings.setAzureAuthEnabled(options, method === azureAuthId); } - newAuthProps.onAuthMethodSelect(method); + onOptionsChange({ + ...options, + basicAuth: method === AuthMethod.BasicAuth, + withCredentials: method === AuthMethod.CrossSiteCredentials, + jsonData: { + ...options.jsonData, + sigV4Auth: method === sigV4Id, + oauthPassThru: method === AuthMethod.OAuthForward, + }, + }); }} // If your method is selected pass its id to `selectedMethod`, // otherwise pass the id from converted legacy data diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index 266a0f089a087..5c5c4a6ac402a 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -46,6 +46,7 @@ export interface PromOptions extends DataSourceJsonData { incrementalQueryOverlapWindow?: string; disableRecordingRules?: boolean; sigV4Auth?: boolean; + oauthPassThru?: boolean; } export type ExemplarTraceIdDestination = { From 5a79e70d20ced58c9a503ee27cde51d730640751 Mon Sep 17 00:00:00 2001 From: Isabel <76437239+imatwawana@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:51:19 -0400 Subject: [PATCH 008/160] Add date parameter (#76479) --- docs/sources/whatsnew/whats-new-next/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sources/whatsnew/whats-new-next/README.md b/docs/sources/whatsnew/whats-new-next/README.md index f0bbaa6ee75b3..2fbaf560b8e74 100644 --- a/docs/sources/whatsnew/whats-new-next/README.md +++ b/docs/sources/whatsnew/whats-new-next/README.md @@ -10,6 +10,8 @@ Use the following template, replace any `` with the appropriate text ( + + _Available in in Grafana _ @@ -31,6 +33,10 @@ Intended availability of the feature when released outside of Grafana Cloud. The information is intentionally commented out so that it isn't displayed in the published page. If the feature is not going to be released outside of Grafana Cloud, omit the HTML comment entirely. +## _`DATE`_ + +The release date of the feature, fully written out. For example: September 12, 2023. + ## _`CLOUD AVAILABILITY`_ One of the following [release life cycle stages](https://grafana.com/docs/release-life-cycle/): @@ -72,6 +78,8 @@ For example, using the partial URL `/docs/grafana/next/explore/` to link to the +September 12, 2023 + _Available in public preview in Grafana Cloud Pro and Advanced_ The navigation in Grafana Cloud has been updated in the following ways... From 94ce87571ddfcede0fb7a229a65502b385d5bca3 Mon Sep 17 00:00:00 2001 From: Michael Mandrus <41969079+mmandrus@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:56:49 -0400 Subject: [PATCH 009/160] Caching: Add feature toggle for memory efficient cache payload serialization (#76145) * add feature toggle for smart cache serialization * re-add toggle after merge conflict * switch feature toggle stage to experimental * incorporate PR feedback --- .../feature-toggles/index.md | 195 +++++++++--------- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 13 +- pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 10 +- 5 files changed, 117 insertions(+), 103 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index a2631cd9f0494..372f5ee8754a4 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -19,38 +19,38 @@ This page contains a list of available feature toggles. To learn how to turn on Some features are enabled by default. You can disable these feature by setting the feature flag to "false" in the configuration. -| Feature toggle name | Description | Enabled by default | -| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -| `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | -| `featureHighlights` | Highlight Grafana Enterprise features | | -| `dataConnectionsConsole` | Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins. | Yes | -| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | -| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | Yes | -| `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | Yes | -| `cloudwatchNewRegionsHandler` | Refactor of /regions endpoint, no user-facing changes | Yes | -| `nestedFolderPicker` | Enables the new folder picker to work with nested folders. Requires the nestedFolders feature flag | Yes | -| `accessTokenExpirationCheck` | Enable OAuth access_token expiration check and token refresh using the refresh_token | | -| `emptyDashboardPage` | Enable the redesigned user interface of a dashboard page that includes no panels | Yes | -| `disablePrometheusExemplarSampling` | Disable Prometheus exemplar sampling | | -| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | -| `gcomOnlyExternalOrgRoleSync` | Prohibits a user from changing organization roles synced with Grafana Cloud auth provider | | -| `prometheusMetricEncyclopedia` | Adds the metrics explorer component to the Prometheus query builder as an option in metric select | Yes | -| `prometheusDataplane` | Changes responses to from Prometheus to be compliant with the dataplane specification. In particular it sets the numeric Field.Name from 'Value' to the value of the `__name__` label when present. | Yes | -| `lokiMetricDataplane` | Changes metric responses from Loki to be compliant with the dataplane specification. | Yes | -| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | -| `alertingNotificationsPoliciesMatchingInstances` | Enables the preview of matching instances for notification policies | Yes | -| `useCachingService` | When turned on, the new query and resource caching implementation using a wire service inject will be used in place of the previous middleware implementation | | -| `enableElasticsearchBackendQuerying` | Enable the processing of queries and responses in the Elasticsearch data source through backend | Yes | -| `advancedDataSourcePicker` | Enable a new data source picker with contextual information, recently used order and advanced mode | Yes | -| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | Yes | -| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | -| `transformationsRedesign` | Enables the transformations redesign | Yes | -| `toggleLabelsInLogsUI` | Enable toggleable filters in log details view | Yes | -| `azureMonitorDataplane` | Adds dataplane compliant frame metadata in the Azure Monitor datasource | Yes | -| `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component | Yes | -| `newBrowseDashboards` | New browse/manage dashboards UI | Yes | -| `alertingInsights` | Show the new alerting insights landing page | Yes | -| `cloudWatchWildCardDimensionValues` | Fetches dimension values from CloudWatch to correctly label wildcard dimensions | Yes | +| Feature toggle name | Description | Enabled by default | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | +| `featureHighlights` | Highlight Grafana Enterprise features | | +| `dataConnectionsConsole` | Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins. | Yes | +| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | +| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | Yes | +| `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | Yes | +| `cloudwatchNewRegionsHandler` | Refactor of /regions endpoint, no user-facing changes | Yes | +| `nestedFolderPicker` | Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle | Yes | +| `accessTokenExpirationCheck` | Enable OAuth access_token expiration check and token refresh using the refresh_token | | +| `emptyDashboardPage` | Enable the redesigned user interface of a dashboard page that includes no panels | Yes | +| `disablePrometheusExemplarSampling` | Disable Prometheus exemplar sampling | | +| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | +| `gcomOnlyExternalOrgRoleSync` | Prohibits a user from changing organization roles synced with Grafana Cloud auth provider | | +| `prometheusMetricEncyclopedia` | Adds the metrics explorer component to the Prometheus query builder as an option in metric select | Yes | +| `prometheusDataplane` | Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label. | Yes | +| `lokiMetricDataplane` | Changes metric responses from Loki to be compliant with the dataplane specification. | Yes | +| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | +| `alertingNotificationsPoliciesMatchingInstances` | Enables the preview of matching instances for notification policies | Yes | +| `useCachingService` | When active, the new query and resource caching implementation using a wire service inject replaces the previous middleware implementation. | | +| `enableElasticsearchBackendQuerying` | Enable the processing of queries and responses in the Elasticsearch data source through backend | Yes | +| `advancedDataSourcePicker` | Enable a new data source picker with contextual information, recently used order and advanced mode | Yes | +| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | Yes | +| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | +| `transformationsRedesign` | Enables the transformations redesign | Yes | +| `toggleLabelsInLogsUI` | Enable toggleable filters in log details view | Yes | +| `azureMonitorDataplane` | Adds dataplane compliant frame metadata in the Azure Monitor datasource | Yes | +| `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component | Yes | +| `newBrowseDashboards` | New browse/manage dashboards UI | Yes | +| `alertingInsights` | Show the new alerting insights landing page | Yes | +| `cloudWatchWildCardDimensionValues` | Fetches dimension values from CloudWatch to correctly label wildcard dimensions | Yes | ## Preview feature toggles @@ -84,71 +84,72 @@ Some features are enabled by default. You can disable these feature by setting t These features are early in their development lifecycle and so are not yet supported in Grafana Cloud. Experimental features might be changed or removed without prior notice. -| Feature toggle name | Description | -| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | -| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | -| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | -| `storage` | Configurable storage for dashboards, datasources, and resources | -| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query | -| `traceToMetrics` | Enable trace to metrics links | -| `canvasPanelNesting` | Allow elements nesting | -| `scenes` | Experimental framework to build interactive dashboards | -| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | -| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | -| `dockedMegaMenu` | Enable support for a persistent (docked) navigation menu | -| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | -| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | -| `alertingBacktesting` | Rule backtesting API for alerting | -| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | -| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | -| `individualCookiePreferences` | Support overriding cookie preferences per user | -| `timeSeriesTable` | Enable time series table transformer & sparkline cell type | -| `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation | -| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | -| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | -| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | -| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | -| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | -| `unifiedRequestLog` | Writes error logs to the request logger | -| `extraThemes` | Enables extra themes | -| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | -| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | -| `dashboardEmbed` | Allow embedding dashboard for external use in Code editors | -| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | -| `lokiFormatQuery` | Enables the ability to format Loki queries | -| `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore | -| `pluginsDynamicAngularDetectionPatterns` | Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones | -| `vizAndWidgetSplit` | Split panels between visualizations and widgets | -| `prometheusIncrementalQueryInstrumentation` | Adds RudderStack events to incremental queries | -| `logsExploreTableVisualisation` | A table visualisation for logs in Explore | -| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | -| `mlExpressions` | Enable support for Machine Learning in server-side expressions | -| `traceQLStreaming` | Enables response streaming of TraceQL queries of the Tempo data source | -| `metricsSummary` | Enables metrics summary queries in the Tempo data source | -| `grafanaAPIServer` | Enable Kubernetes API Server for Grafana resources | -| `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server | -| `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end | -| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | -| `influxdbSqlSupport` | Enable InfluxDB SQL query language support with new querying UI | -| `angularDeprecationUI` | Display new Angular deprecation-related UI features | -| `dashgpt` | Enable AI powered features in dashboards | -| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions | -| `requestInstrumentationStatusSource` | Include a status source label for request metrics and logs | -| `libraryPanelRBAC` | Enables RBAC support for library panels | -| `wargamesTesting` | Placeholder feature flag for internal testing | -| `externalCorePlugins` | Allow core plugins to be loaded as external | -| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | -| `httpSLOLevels` | Adds SLO level to http request metrics | -| `alertingModifiedExport` | Enables using UI for provisioned rules modification and export | -| `panelMonitoring` | Enables panel monitoring through logs and measurements | -| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | -| `transformationsVariableSupport` | Allows using variables in transformations | -| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists | -| `navAdminSubsections` | Splits the administration section of the nav tree into subsections | -| `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | -| `awsDatasourcesNewFormStyling` | Applies new form styling for configuration and query editors in AWS plugins | +| Feature toggle name | Description | +| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | +| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | +| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | +| `storage` | Configurable storage for dashboards, datasources, and resources | +| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query | +| `traceToMetrics` | Enable trace to metrics links | +| `canvasPanelNesting` | Allow elements nesting | +| `scenes` | Experimental framework to build interactive dashboards | +| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | +| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | +| `dockedMegaMenu` | Enable support for a persistent (docked) navigation menu | +| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | +| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | +| `alertingBacktesting` | Rule backtesting API for alerting | +| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | +| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | +| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | +| `individualCookiePreferences` | Support overriding cookie preferences per user | +| `timeSeriesTable` | Enable time series table transformer & sparkline cell type | +| `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation | +| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | +| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | +| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | +| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | +| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | +| `unifiedRequestLog` | Writes error logs to the request logger | +| `extraThemes` | Enables extra themes | +| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | +| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | +| `dashboardEmbed` | Allow embedding dashboard for external use in Code editors | +| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | +| `lokiFormatQuery` | Enables the ability to format Loki queries | +| `exploreScrollableLogsContainer` | Improves the scrolling behavior of logs in Explore | +| `pluginsDynamicAngularDetectionPatterns` | Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones | +| `vizAndWidgetSplit` | Split panels between visualizations and widgets | +| `prometheusIncrementalQueryInstrumentation` | Adds RudderStack events to incremental queries | +| `logsExploreTableVisualisation` | A table visualisation for logs in Explore | +| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | +| `mlExpressions` | Enable support for Machine Learning in server-side expressions | +| `traceQLStreaming` | Enables response streaming of TraceQL queries of the Tempo data source | +| `metricsSummary` | Enables metrics summary queries in the Tempo data source | +| `grafanaAPIServer` | Enable Kubernetes API Server for Grafana resources | +| `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server | +| `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end | +| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | +| `influxdbSqlSupport` | Enable InfluxDB SQL query language support with new querying UI | +| `angularDeprecationUI` | Display new Angular deprecation-related UI features | +| `dashgpt` | Enable AI powered features in dashboards | +| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions | +| `requestInstrumentationStatusSource` | Include a status source label for request metrics and logs | +| `libraryPanelRBAC` | Enables RBAC support for library panels | +| `wargamesTesting` | Placeholder feature flag for internal testing | +| `externalCorePlugins` | Allow core plugins to be loaded as external | +| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | +| `httpSLOLevels` | Adds SLO level to http request metrics | +| `alertingModifiedExport` | Enables using UI for provisioned rules modification and export | +| `panelMonitoring` | Enables panel monitoring through logs and measurements | +| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | +| `transformationsVariableSupport` | Allows using variables in transformations | +| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists | +| `navAdminSubsections` | Splits the administration section of the nav tree into subsections | +| `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | +| `awsDatasourcesNewFormStyling` | Applies new form styling for configuration and query editors in AWS plugins | +| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 345e9fbd3fde1..b099c5c8d1f0a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -142,4 +142,5 @@ export interface FeatureToggles { navAdminSubsections?: boolean; recoveryThreshold?: boolean; awsDatasourcesNewFormStyling?: boolean; + cachingOptimizeSerializationMemoryUsage?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5810fdb840aa4..9f31c513c0598 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -236,7 +236,7 @@ var ( }, { Name: "nestedFolderPicker", - Description: "Enables the new folder picker to work with nested folders. Requires the nestedFolders feature flag", + Description: "Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle", Stage: FeatureStageGeneralAvailability, Owner: grafanaFrontendPlatformSquad, FrontendOnly: true, @@ -346,7 +346,7 @@ var ( }, { Name: "prometheusDataplane", - Description: "Changes responses to from Prometheus to be compliant with the dataplane specification. In particular it sets the numeric Field.Name from 'Value' to the value of the `__name__` label when present.", + Description: "Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label.", Expression: "true", Stage: FeatureStageGeneralAvailability, Owner: grafanaObservabilityMetricsSquad, @@ -431,7 +431,7 @@ var ( }, { Name: "useCachingService", - Description: "When turned on, the new query and resource caching implementation using a wire service inject will be used in place of the previous middleware implementation", + Description: "When active, the new query and resource caching implementation using a wire service inject replaces the previous middleware implementation.", Stage: FeatureStageGeneralAvailability, Owner: grafanaOperatorExperienceSquad, RequiresRestart: true, @@ -867,5 +867,12 @@ var ( FrontendOnly: true, Owner: awsDatasourcesSquad, }, + { + Name: "cachingOptimizeSerializationMemoryUsage", + Description: "If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + FrontendOnly: false, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 3c928a69c9907..d0366d7b2fa06 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -123,3 +123,4 @@ kubernetesPlaylists,experimental,@grafana/grafana-app-platform-squad,false,false navAdminSubsections,experimental,@grafana/grafana-frontend-platform,false,false,false,false recoveryThreshold,experimental,@grafana/alerting-squad,false,false,true,false awsDatasourcesNewFormStyling,experimental,@grafana/aws-datasources,false,false,false,true +cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 3240b12583354..40e731ea05d61 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -144,7 +144,7 @@ const ( FlagNestedFolders = "nestedFolders" // FlagNestedFolderPicker - // Enables the new folder picker to work with nested folders. Requires the nestedFolders feature flag + // Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle FlagNestedFolderPicker = "nestedFolderPicker" // FlagAccessTokenExpirationCheck @@ -208,7 +208,7 @@ const ( FlagClientTokenRotation = "clientTokenRotation" // FlagPrometheusDataplane - // Changes responses to from Prometheus to be compliant with the dataplane specification. In particular it sets the numeric Field.Name from 'Value' to the value of the `__name__` label when present. + // Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label. FlagPrometheusDataplane = "prometheusDataplane" // FlagLokiMetricDataplane @@ -260,7 +260,7 @@ const ( FlagRefactorVariablesTimeRange = "refactorVariablesTimeRange" // FlagUseCachingService - // When turned on, the new query and resource caching implementation using a wire service inject will be used in place of the previous middleware implementation + // When active, the new query and resource caching implementation using a wire service inject replaces the previous middleware implementation. FlagUseCachingService = "useCachingService" // FlagEnableElasticsearchBackendQuerying @@ -502,4 +502,8 @@ const ( // FlagAwsDatasourcesNewFormStyling // Applies new form styling for configuration and query editors in AWS plugins FlagAwsDatasourcesNewFormStyling = "awsDatasourcesNewFormStyling" + + // FlagCachingOptimizeSerializationMemoryUsage + // If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. + FlagCachingOptimizeSerializationMemoryUsage = "cachingOptimizeSerializationMemoryUsage" ) From c21e2bee1df1c9a0e3b01921508fae2e21e72311 Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Thu, 12 Oct 2023 13:03:33 -0400 Subject: [PATCH 010/160] Prometheus: Variable query, allow for label values query type with label, label filters and no metric (#76472) allow for label values query type with label, label filters and no metric --- .../components/VariableQueryEditor.test.tsx | 21 +++++++++++++++++++ .../components/VariableQueryEditor.tsx | 2 +- .../migrations/variableMigration.ts | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx index ae76f2a2dcc22..7d0af5aa56de1 100644 --- a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx +++ b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx @@ -108,6 +108,27 @@ describe('PromVariableQueryEditor', () => { expect(migration).toEqual(expected); }); + test('Migrates a query object with no metric and only label filters to an expression correctly', () => { + const query: PromVariableQuery = { + qryType: PromVariableQueryType.LabelValues, + label: 'name', + labelFilters: [ + { + label: 'label', + op: '=', + value: 'value', + }, + ], + refId: 'PrometheusDatasource-VariableQuery', + }; + + const migration: string = migrateVariableEditorBackToVariableSupport(query); + + const expected = 'label_values({label="value"},name)'; + + expect(migration).toEqual(expected); + }); + beforeEach(() => { props = { datasource: { diff --git a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx index 165236b0ba0b4..43f4badad79e5 100644 --- a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx @@ -50,7 +50,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props) // seriesQuery is only a whole const [seriesQuery, setSeriesQuery] = useState(''); - // the original variable query implementation + // the original variable query implementation, e.g. label_value(metric, label_name) const [classicQuery, setClassicQuery] = useState(''); // list of label names for label_values(), /api/v1/labels, contains the same results as label_names() function diff --git a/public/app/plugins/datasource/prometheus/migrations/variableMigration.ts b/public/app/plugins/datasource/prometheus/migrations/variableMigration.ts index 8cf87585d5d2a..302191f98fbe7 100644 --- a/public/app/plugins/datasource/prometheus/migrations/variableMigration.ts +++ b/public/app/plugins/datasource/prometheus/migrations/variableMigration.ts @@ -105,7 +105,7 @@ export function migrateVariableEditorBackToVariableSupport(QueryVariable: PromVa } return 'label_names()'; case QueryType.LabelValues: - if (QueryVariable.metric) { + if (QueryVariable.metric || (QueryVariable.labelFilters && QueryVariable.labelFilters.length !== 0)) { const visualQueryQuery = { metric: QueryVariable.metric, labels: QueryVariable.labelFilters ?? [], From 42f4244026f436d960cb04c043d57e610ca3af9e Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 12 Oct 2023 19:17:32 +0200 Subject: [PATCH 011/160] Alerting: Add rules export on a folder level (#76016) --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 7 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - public/app/features/alerting/routes.tsx | 15 +- .../unified/MoreActionsRuleButtons.tsx | 14 +- .../features/alerting/unified/RuleEditor.tsx | 3 + .../alerting/unified/RuleList.test.tsx | 47 +++--- .../features/alerting/unified/RuleList.tsx | 11 +- .../features/alerting/unified/RuleViewer.tsx | 25 +--- .../alerting/unified/api/alertRuleApi.ts | 29 ++-- .../export/GrafanaModifyExport.test.tsx | 138 ++++++++++++++++++ .../components/export/GrafanaRuleExporter.tsx | 4 +- .../export/GrafanaRuleFolderExporter.tsx | 59 ++++++++ .../export/GrafanaRuleGroupExporter.tsx | 5 +- .../export/GrafanaRulesExporter.tsx | 2 +- .../rule-viewer/RuleViewer.v1.test.tsx | 27 +++- .../components/rules/RuleActionsButtons.tsx | 115 +++++++-------- .../rules/RuleDetailsActionButtons.tsx | 86 ++++++++--- .../components/rules/RulesGroup.test.tsx | 6 +- .../unified/components/rules/RulesGroup.tsx | 78 ++++++---- .../components/rules/RulesTable.test.tsx | 5 +- .../app/features/alerting/unified/mockApi.ts | 58 ++++++++ 24 files changed, 511 insertions(+), 230 deletions(-) create mode 100644 public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx create mode 100644 public/app/features/alerting/unified/components/export/GrafanaRuleFolderExporter.tsx diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 372f5ee8754a4..8e54ac4c55736 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -141,7 +141,6 @@ Experimental features might be changed or removed without prior notice. | `externalCorePlugins` | Allow core plugins to be loaded as external | | `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | | `httpSLOLevels` | Adds SLO level to http request metrics | -| `alertingModifiedExport` | Enables using UI for provisioned rules modification and export | | `panelMonitoring` | Enables panel monitoring through logs and measurements | | `enableNativeHTTPHistogram` | Enables native HTTP Histograms | | `transformationsVariableSupport` | Allows using variables in transformations | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b099c5c8d1f0a..912bae6fb3299 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -134,7 +134,6 @@ export interface FeatureToggles { idForwarding?: boolean; cloudWatchWildCardDimensionValues?: boolean; externalServiceAccounts?: boolean; - alertingModifiedExport?: boolean; panelMonitoring?: boolean; enableNativeHTTPHistogram?: boolean; transformationsVariableSupport?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9f31c513c0598..76203671901be 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -810,13 +810,6 @@ var ( RequiresDevMode: true, Owner: grafanaAuthnzSquad, }, - { - Name: "alertingModifiedExport", - Description: "Enables using UI for provisioned rules modification and export", - Stage: FeatureStageExperimental, - FrontendOnly: false, - Owner: grafanaAlertingSquad, - }, { Name: "panelMonitoring", Description: "Enables panel monitoring through logs and measurements", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d0366d7b2fa06..2a03ae6389438 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -115,7 +115,6 @@ httpSLOLevels,experimental,@grafana/hosted-grafana-team,false,false,true,false idForwarding,experimental,@grafana/grafana-authnz-team,true,false,false,false cloudWatchWildCardDimensionValues,GA,@grafana/aws-datasources,false,false,false,false externalServiceAccounts,experimental,@grafana/grafana-authnz-team,true,false,false,false -alertingModifiedExport,experimental,@grafana/alerting-squad,false,false,false,false panelMonitoring,experimental,@grafana/dataviz-squad,false,false,false,true enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false,false transformationsVariableSupport,experimental,@grafana/grafana-bi-squad,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 40e731ea05d61..2b93a1f731074 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -471,10 +471,6 @@ const ( // Automatic service account and token setup for plugins FlagExternalServiceAccounts = "externalServiceAccounts" - // FlagAlertingModifiedExport - // Enables using UI for provisioned rules modification and export - FlagAlertingModifiedExport = "alertingModifiedExport" - // FlagPanelMonitoring // Enables panel monitoring through logs and measurements FlagPanelMonitoring = "panelMonitoring" diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index c1d39295cd062..5e67be4822df3 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -1,6 +1,5 @@ import { uniq } from 'lodash'; import React from 'react'; -import { Redirect } from 'react-router-dom'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { NavLandingPage } from 'app/core/components/NavLandingPage/NavLandingPage'; @@ -247,15 +246,13 @@ const unifiedRoutes: RouteDescriptor[] = [ { path: '/alerting/:id/modify-export', pageClass: 'page-alerting', - roles: evaluateAccess([AccessControlAction.AlertingRuleUpdate]), - component: config.featureToggles.alertingModifiedExport - ? SafeDynamicImport( - () => - import( - /* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/components/export/GrafanaModifyExport' - ) + roles: evaluateAccess([AccessControlAction.AlertingRuleRead]), + component: SafeDynamicImport( + () => + import( + /* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/components/export/GrafanaModifyExport' ) - : () => , + ), }, { path: '/alerting/:sourceName/:id/view', diff --git a/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx b/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx index ba1d61121ec12..fcf0f5e6b48d1 100644 --- a/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx +++ b/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx @@ -7,12 +7,20 @@ import { Button, Dropdown, Icon, LinkButton, Menu, MenuItem } from '@grafana/ui' import { logInfo, LogMessages } from './Analytics'; import { GrafanaRulesExporter } from './components/export/GrafanaRulesExporter'; -import { useRulesAccess } from './utils/accessControlHooks'; +import { AlertSourceAction, useAlertSourceAbility } from './hooks/useAbilities'; interface Props {} export function MoreActionsRuleButtons({}: Props) { - const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess(); + const [_, viewRuleAllowed] = useAlertSourceAbility(AlertSourceAction.ViewAlertRule); + const [createRuleSupported, createRuleAllowed] = useAlertSourceAbility(AlertSourceAction.CreateAlertRule); + const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertSourceAbility( + AlertSourceAction.CreateExternalAlertRule + ); + + const canCreateGrafanaRules = createRuleSupported && createRuleAllowed; + const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed; + const location = useLocation(); const [showExportDrawer, toggleShowExportDrawer] = useToggle(false); const newMenu = ( @@ -25,7 +33,7 @@ export function MoreActionsRuleButtons({}: Props) { label="New recording rule" /> )} - {canReadProvisioning && } + {viewRuleAllowed && } ); diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 23487beea4db1..4928fc27cb569 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -57,6 +57,9 @@ const RuleEditor = ({ match }: RuleEditorProps) => { if (identifier) { await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: identifier.ruleSourceName })); } + if (copyFromIdentifier) { + await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: copyFromIdentifier.ruleSourceName })); + } }, [dispatch]); const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess(); diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 07de53cb27a6e..ff1eddc5d3837 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -684,25 +684,27 @@ describe('RuleList', () => { describe('RBAC Enabled', () => { describe('Export button', () => { - it('Export button should be visible when the user has alert provisioning read permissions', async () => { - grantUserPermissions([AccessControlAction.AlertingProvisioningRead]); + it('Export button should be visible when the user has alert read permissions', async () => { + grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.FoldersRead]); mocks.getAllDataSourcesMock.mockReturnValue([]); setDataSourceSrv(new MockDataSourceSrv({})); - mocks.api.fetchRules.mockResolvedValue([]); - mocks.api.fetchRulerRules.mockResolvedValue({}); - - renderRuleList(); - - await userEvent.click(ui.moreButton.get()); - expect(ui.exportButton.get()).toBeInTheDocument(); - }); - it('Export button should be visible when the user has alert provisioning read secrets permissions', async () => { - grantUserPermissions([AccessControlAction.AlertingProvisioningReadSecrets]); - - mocks.getAllDataSourcesMock.mockReturnValue([]); - setDataSourceSrv(new MockDataSourceSrv({})); - mocks.api.fetchRules.mockResolvedValue([]); + mocks.api.fetchRules.mockResolvedValue([ + mockPromRuleNamespace({ + name: 'foofolder', + dataSourceName: GRAFANA_RULES_SOURCE_NAME, + groups: [ + mockPromRuleGroup({ + name: 'grafana-group', + rules: [ + mockPromAlertingRule({ + query: '[]', + }), + ], + }), + ], + }), + ]); mocks.api.fetchRulerRules.mockResolvedValue({}); renderRuleList(); @@ -710,19 +712,6 @@ describe('RuleList', () => { await userEvent.click(ui.moreButton.get()); expect(ui.exportButton.get()).toBeInTheDocument(); }); - it('Export button should not be visible when the user has no alert provisioning read permissions', async () => { - grantUserPermissions([AccessControlAction.AlertingRuleCreate, AccessControlAction.FoldersRead]); - - mocks.getAllDataSourcesMock.mockReturnValue([]); - setDataSourceSrv(new MockDataSourceSrv({})); - mocks.api.fetchRules.mockResolvedValue([]); - mocks.api.fetchRulerRules.mockResolvedValue({}); - - renderRuleList(); - - await userEvent.click(ui.moreButton.get()); - expect(ui.exportButton.query()).not.toBeInTheDocument(); - }); }); describe('Grafana Managed Alerts', () => { it('New alert button should be visible when the user has alert rule create and folder read permissions and no rules exists', async () => { diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index 6aaaec90e1d4e..655b52a9f3d28 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -24,7 +24,6 @@ import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces'; import { useFilteredRules, useRulesFilter } from './hooks/useFilteredRules'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchAllPromAndRulerRulesAction } from './state/actions'; -import { useRulesAccess } from './utils/accessControlHooks'; import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants'; import { getAllRulesSourceNames } from './utils/datasource'; @@ -91,8 +90,6 @@ const RuleList = withErrorBoundary( const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces(); const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState); - const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess(); - return ( // We don't want to show the Loading... indicator for the whole page. // We show separate indicators for Grafana-managed and Cloud rules @@ -116,11 +113,9 @@ const RuleList = withErrorBoundary( )} - {(canCreateGrafanaRules || canCreateCloudRules || canReadProvisioning) && ( - - - - )} + + + )} diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 05f844372882e..864d68d104e32 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -1,16 +1,12 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Disable, Enable } from 'react-enable'; -import { useParams } from 'react-router-dom'; -import { Button, HorizontalGroup, withErrorBoundary } from '@grafana/ui'; -import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; +import { withErrorBoundary } from '@grafana/ui'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; -import { GrafanaRuleExporter } from './components/export/GrafanaRuleExporter'; import { AlertingFeature } from './features'; -import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; const DetailViewV1 = SafeDynamicImport(() => import('./components/rule-viewer/RuleViewer.v1')); const DetailViewV2 = SafeDynamicImport(() => import('./components/rule-viewer/v2/RuleViewer.v2')); @@ -21,25 +17,8 @@ type RuleViewerProps = GrafanaRouteComponentProps<{ }>; const RuleViewer = (props: RuleViewerProps): JSX.Element => { - const routeParams = useParams<{ type: string; id: string }>(); - const uidFromParams = routeParams.id; - - const sourceName = props.match.params.sourceName; - - const [showYaml, setShowYaml] = useState(false); - const actionButtons = - sourceName === GRAFANA_RULES_SOURCE_NAME ? ( - - - - ) : null; - return ( - - {showYaml && setShowYaml(false)} />} diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index bee885c2e0d3f..85c07792765a1 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -43,10 +43,6 @@ export interface Datasource { export const PREVIEW_URL = '/api/v1/rule/test/grafana'; export const PROM_RULES_URL = 'api/prometheus/grafana/api/v1/rules'; -function getProvisioningExportUrl(ruleUid: string, format: 'yaml' | 'json' | 'hcl' = 'yaml') { - return `/api/v1/provisioning/alert-rules/${ruleUid}/export?format=${format}`; -} - export interface Data { refId: string; relativeTimeRange: RelativeTimeRange; @@ -71,6 +67,13 @@ export interface Rule { export type AlertInstances = Record; +interface ExportRulesParams { + format: ExportFormats; + folderUid?: string; + group?: string; + ruleUid?: string; +} + export interface ModifyExportPayload { rules: Array; name: string; @@ -192,20 +195,10 @@ export const alertRuleApi = alertingApi.injectEndpoints({ }, }), - exportRule: build.query({ - query: ({ uid, format }) => ({ url: getProvisioningExportUrl(uid, format), responseType: 'text' }), - }), - exportRuleGroup: build.query({ - query: ({ folderUid, groupName, format }) => ({ - url: `/api/v1/provisioning/folder/${folderUid}/rule-groups/${groupName}/export`, - params: { format: format }, - responseType: 'text', - }), - }), - exportRules: build.query({ - query: ({ format }) => ({ - url: `/api/v1/provisioning/alert-rules/export`, - params: { format: format }, + exportRules: build.query({ + query: ({ format, folderUid, group, ruleUid }) => ({ + url: `/api/ruler/grafana/api/v1/export/rules`, + params: { format: format, folderUid: folderUid, group: group, ruleUid: ruleUid }, responseType: 'text', }), }), diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx new file mode 100644 index 0000000000000..0f44e23c14fdf --- /dev/null +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx @@ -0,0 +1,138 @@ +import { render, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import { AutoSizerProps } from 'react-virtualized-auto-sizer'; +import { byRole, byTestId, byText } from 'testing-library-selector'; + +import { selectors } from '@grafana/e2e-selectors'; +import { locationService } from '@grafana/runtime'; + +import { TestProvider } from '../../../../../../test/helpers/TestProvider'; +import { AlertmanagerChoice } from '../../../../../plugins/datasource/alertmanager/types'; +import { DashboardSearchItemType } from '../../../../search/types'; +import { mockAlertRuleApi, mockApi, mockExportApi, mockSearchApi, setupMswServer } from '../../mockApi'; +import { getGrafanaRule, mockDataSource } from '../../mocks'; +import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi'; +import { setupDataSources } from '../../testSetup/datasources'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; + +import GrafanaModifyExport from './GrafanaModifyExport'; + +jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ + AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, +})); + +jest.mock('react-virtualized-auto-sizer', () => { + return ({ children }: AutoSizerProps) => children({ height: 600, width: 1 }); +}); +jest.mock('@grafana/ui', () => ({ + ...jest.requireActual('@grafana/ui'), + CodeEditor: ({ value }: { value: string }) =>