From 5897024dfe0af9d98813601ee02ea9dea2d357ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 20 Feb 2025 12:34:52 +0100 Subject: [PATCH 01/27] feat(unified-storage): enhance gRPC client with dskit (#101035) --- go.mod | 4 +-- pkg/storage/unified/client.go | 67 ++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 06cd809c8e6..2b11e0fabe6 100644 --- a/go.mod +++ b/go.mod @@ -447,9 +447,9 @@ require ( github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e // indirect + github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e // @grafana/grafana-search-and-storage github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect - github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // @grafana/grafana-search-and-storage github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index 7b34b95ab92..b4200f31dee 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -5,7 +5,10 @@ import ( "fmt" "path/filepath" + otgrpc "github.com/opentracing-contrib/go-grpc" + "github.com/opentracing/opentracing-go" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "gocloud.dev/blob/fileblob" "google.golang.org/grpc" @@ -13,6 +16,9 @@ import ( authnlib "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" + "github.com/grafana/dskit/flagext" + "github.com/grafana/dskit/grpcclient" + "github.com/grafana/dskit/middleware" infraDB "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" @@ -39,6 +45,10 @@ type Options struct { Docs resource.DocumentBuilderSupplier } +type clientMetrics struct { + requestDuration *prometheus.HistogramVec +} + // This adds a UnifiedStorage client into the wire dependency tree func ProvideUnifiedStorageClient(opts *Options) (resource.ResourceClient, error) { // See: apiserver.ApplyGrafanaConfig(cfg, features, o) @@ -104,11 +114,8 @@ func newClient(opts options.StorageOptions, return nil, fmt.Errorf("expecting address for storage_type: %s", opts.StorageType) } - // Create a connection to the gRPC server - conn, err := grpc.NewClient(opts.Address, - grpc.WithStatsHandler(otelgrpc.NewClientHandler()), - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) + // Create a connection to the gRPC server. + conn, err := grpcConn(opts.Address, reg) if err != nil { return nil, err } @@ -153,3 +160,53 @@ func newResourceClient(conn *grpc.ClientConn, cfg *setting.Cfg, features feature } return resource.NewRemoteResourceClient(tracer, conn, clientCfgMapping(grpcutils.ReadGrpcClientConfig(cfg)), cfg.Env == setting.Dev) } + +// grpcConn creates a new gRPC connection to the provided address. +func grpcConn(address string, reg prometheus.Registerer) (*grpc.ClientConn, error) { + // This works for now as the Provide function is only called once during startup. + // We might eventually want to tight this factory to a struct for more runtime control. + metrics := clientMetrics{ + requestDuration: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ + Name: "resource_server_client_request_duration_seconds", + Help: "Time spent executing requests to the resource server.", + Buckets: prometheus.ExponentialBuckets(0.008, 4, 7), + }, []string{"operation", "status_code"}), + } + + // Report gRPC status code errors as labels. + unary, stream := instrument(metrics.requestDuration, middleware.ReportGRPCStatusOption) + + cfg := grpcclient.Config{} + // Set the defaults that are normally set by Config.RegisterFlags. + flagext.DefaultValues(&cfg) + + opts, err := cfg.DialOption(unary, stream) + if err != nil { + return nil, fmt.Errorf("could not instrument grpc client: %w", err) + } + + opts = append(opts, grpc.WithStatsHandler(otelgrpc.NewClientHandler())) + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + + // Use round_robin to balances requests more evenly over the available Storage server. + opts = append(opts, grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`)) + + // Disable looking up service config from TXT DNS records. + // This reduces the number of requests made to the DNS servers. + opts = append(opts, grpc.WithDisableServiceConfig()) + + // Create a connection to the gRPC server + return grpc.NewClient(address, opts...) +} + +// instrument is the same as grpcclient.Instrument but without the middleware.ClientUserHeaderInterceptor +// and middleware.StreamClientUserHeaderInterceptor as we don't need them. +func instrument(requestDuration *prometheus.HistogramVec, instrumentationLabelOptions ...middleware.InstrumentationOption) ([]grpc.UnaryClientInterceptor, []grpc.StreamClientInterceptor) { + return []grpc.UnaryClientInterceptor{ + otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer()), + middleware.UnaryClientInstrumentInterceptor(requestDuration, instrumentationLabelOptions...), + }, []grpc.StreamClientInterceptor{ + otgrpc.OpenTracingStreamClientInterceptor(opentracing.GlobalTracer()), + middleware.StreamClientInstrumentInterceptor(requestDuration, instrumentationLabelOptions...), + } +} From be81314e21186521a6f80e006b61aacbdb7004c9 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Thu, 20 Feb 2025 12:42:59 +0100 Subject: [PATCH 02/27] Replace v0 by k8s v1 when `kubernetesDashboards` is enabled (#101033) Use v1 by default when kubernetesDashboards is on --- .betterer.results | 2 +- .../features/dashboard/api/ResponseTransformers.ts | 4 ++-- .../app/features/dashboard/api/dashboard_api.test.ts | 8 ++++---- public/app/features/dashboard/api/dashboard_api.ts | 7 +++---- public/app/features/dashboard/api/utils.test.ts | 10 +++++----- public/app/features/dashboard/api/utils.ts | 12 ++++++------ .../dashboard/api/{v0.test.ts => v1.test.ts} | 8 ++++---- public/app/features/dashboard/api/{v0.ts => v1.ts} | 2 +- 8 files changed, 26 insertions(+), 27 deletions(-) rename public/app/features/dashboard/api/{v0.test.ts => v1.test.ts} (97%) rename public/app/features/dashboard/api/{v0.ts => v1.ts} (99%) diff --git a/.betterer.results b/.betterer.results index 6ca30a27198..42f02a2db9f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3667,7 +3667,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"] ], - "public/app/features/dashboard/api/v0.ts:5381": [ + "public/app/features/dashboard/api/v1.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/dashboard/components/AddLibraryPanelWidget/AddLibraryPanelWidget.tsx:5381": [ diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 4c25f8d44a7..b57465de211 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -190,13 +190,13 @@ export function ensureV2Response( export function ensureV1Response( dashboard: DashboardDTO | DashboardWithAccessInfo | DashboardWithAccessInfo ): DashboardDTO { - // if dashboard is not on v0 schema or v2 schema, return as is + // if dashboard is not on v1 schema or v2 schema, return as is if (!isDashboardResource(dashboard)) { return dashboard; } const spec = dashboard.spec; - // if dashboard is on v0 schema + // if dashboard is on v1 schema if (isDashboardV0Spec(spec)) { return { meta: { diff --git a/public/app/features/dashboard/api/dashboard_api.test.ts b/public/app/features/dashboard/api/dashboard_api.test.ts index ce234f47ae6..113158bb057 100644 --- a/public/app/features/dashboard/api/dashboard_api.test.ts +++ b/public/app/features/dashboard/api/dashboard_api.test.ts @@ -2,7 +2,7 @@ import { config } from '@grafana/runtime'; import { getDashboardAPI, setDashboardAPI } from './dashboard_api'; import { LegacyDashboardAPI } from './legacy'; -import { K8sDashboardAPI } from './v0'; +import { K8sDashboardAPI } from './v1'; import { K8sDashboardV2API } from './v2'; describe('DashboardApi', () => { @@ -29,7 +29,7 @@ describe('DashboardApi', () => { expect(getDashboardAPI()).toBeInstanceOf(LegacyDashboardAPI); }); - it('should use v0 api when and kubernetesDashboards toggle is enabled', () => { + it('should use v1 api when and kubernetesDashboards toggle is enabled', () => { config.featureToggles.kubernetesDashboards = true; expect(getDashboardAPI()).toBeInstanceOf(K8sDashboardAPI); }); @@ -55,12 +55,12 @@ describe('DashboardApi', () => { expect(getDashboardAPI()).toBeInstanceOf(LegacyDashboardAPI); }); - it('should use v0 api when kubernetesDashboards toggle is enabled', () => { + it('should use v1 api when kubernetesDashboards toggle is enabled', () => { config.featureToggles.kubernetesDashboards = true; expect(getDashboardAPI()).toBeInstanceOf(K8sDashboardAPI); }); - it('should use v0 api when kubernetesDashboards and useV2DashboardsAPI toggle is enabled', () => { + it('should use v1 api when kubernetesDashboards and useV2DashboardsAPI toggle is enabled', () => { config.featureToggles.useV2DashboardsAPI = true; config.featureToggles.kubernetesDashboards = true; expect(getDashboardAPI()).toBeInstanceOf(K8sDashboardAPI); diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index 390df6fb9aa..1867be90306 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -5,13 +5,12 @@ import { DashboardDTO } from 'app/types'; import { LegacyDashboardAPI } from './legacy'; import { DashboardAPI, DashboardWithAccessInfo } from './types'; import { getDashboardsApiVersion } from './utils'; -import { K8sDashboardAPI } from './v0'; +import { K8sDashboardAPI } from './v1'; import { K8sDashboardV2API } from './v2'; type DashboardAPIClients = { legacy: DashboardAPI; - v0: DashboardAPI; - // v1: DashboardDTO; TODO[schema]: enable v1 when available + v1: DashboardAPI; v2: DashboardAPI, DashboardV2Spec>; }; @@ -40,7 +39,7 @@ export function getDashboardAPI( if (!clients) { clients = { legacy: new LegacyDashboardAPI(), - v0: new K8sDashboardAPI(), + v1: new K8sDashboardAPI(), v2: new K8sDashboardV2API(isConvertingToV1), }; } diff --git a/public/app/features/dashboard/api/utils.test.ts b/public/app/features/dashboard/api/utils.test.ts index 0dc97fa87c8..65499339f68 100644 --- a/public/app/features/dashboard/api/utils.test.ts +++ b/public/app/features/dashboard/api/utils.test.ts @@ -7,12 +7,12 @@ describe('getDashboardsApiVersion', () => { jest.resetModules(); }); - it('should return v0 when dashboardScene is disabled and kubernetesDashboards is enabled', () => { + it('should return v1 when dashboardScene is disabled and kubernetesDashboards is enabled', () => { config.featureToggles = { dashboardScene: false, kubernetesDashboards: true, }; - expect(getDashboardsApiVersion()).toBe('v0'); + expect(getDashboardsApiVersion()).toBe('v1'); }); it('should return legacy when dashboardScene is disabled and kubernetesDashboards is disabled', () => { @@ -31,13 +31,13 @@ describe('getDashboardsApiVersion', () => { expect(getDashboardsApiVersion()).toBe('v2'); }); - it('should return v0 when dashboardScene is enabled, useV2DashboardsAPI is disabled, and kubernetesDashboards is enabled', () => { + it('should return v1 when dashboardScene is enabled, useV2DashboardsAPI is disabled, and kubernetesDashboards is enabled', () => { config.featureToggles = { dashboardScene: true, useV2DashboardsAPI: false, kubernetesDashboards: true, }; - expect(getDashboardsApiVersion()).toBe('v0'); + expect(getDashboardsApiVersion()).toBe('v1'); }); it('should return legacy when dashboardScene is enabled and both useV2DashboardsAPI and kubernetesDashboards are disabled', () => { @@ -71,7 +71,7 @@ describe('getDashboardsApiVersion', () => { kubernetesDashboards: true, }; - expect(getDashboardsApiVersion()).toBe('v0'); + expect(getDashboardsApiVersion()).toBe('v1'); }); }); }); diff --git a/public/app/features/dashboard/api/utils.ts b/public/app/features/dashboard/api/utils.ts index 89d0c9219d3..d65c35f2ff0 100644 --- a/public/app/features/dashboard/api/utils.ts +++ b/public/app/features/dashboard/api/utils.ts @@ -14,9 +14,9 @@ export function getDashboardsApiVersion() { // if dashboard scene is disabled, use legacy API response for the old architecture if (!config.featureToggles.dashboardScene || forcingOldDashboardArch) { - // for old architecture, use v0 API for k8s dashboards + // for old architecture, use v1 API for k8s dashboards if (config.featureToggles.kubernetesDashboards) { - return 'v0'; + return 'v1'; } return 'legacy'; @@ -27,20 +27,20 @@ export function getDashboardsApiVersion() { } if (config.featureToggles.kubernetesDashboards) { - return 'v0'; + return 'v1'; } return 'legacy'; } -// This function is used to determine if the dashboard is in v2 format or also v0 format +// This function is used to determine if the dashboard is in v2 format or also v1 format export function isDashboardResource( obj?: DashboardDTO | DashboardWithAccessInfo | DashboardWithAccessInfo | null ): obj is DashboardWithAccessInfo | DashboardWithAccessInfo { if (!obj) { return false; } - // is v0 or v2 format? + // is v1 or v2 format? const isK8sDashboard = 'kind' in obj && obj.kind === 'DashboardWithAccessInfo'; return isK8sDashboard; } @@ -50,7 +50,7 @@ export function isDashboardV2Spec(obj: Dashboard | DashboardDataDTO | DashboardV } export function isDashboardV0Spec(obj: DashboardDataDTO | DashboardV2Spec): obj is DashboardDataDTO { - return !isDashboardV2Spec(obj); // not v2 spec means it's v0 spec + return !isDashboardV2Spec(obj); // not v2 spec means it's v1 spec } export function isDashboardV2Resource( diff --git a/public/app/features/dashboard/api/v0.test.ts b/public/app/features/dashboard/api/v1.test.ts similarity index 97% rename from public/app/features/dashboard/api/v0.test.ts rename to public/app/features/dashboard/api/v1.test.ts index 67ca83adc8e..0f2eadedc5f 100644 --- a/public/app/features/dashboard/api/v0.test.ts +++ b/public/app/features/dashboard/api/v1.test.ts @@ -4,11 +4,11 @@ import { AnnoKeyFolder } from 'app/features/apiserver/types'; import { DashboardDataDTO } from 'app/types'; import { DashboardWithAccessInfo } from './types'; -import { K8sDashboardAPI } from './v0'; +import { K8sDashboardAPI } from './v1'; const mockDashboardDto: DashboardWithAccessInfo = { kind: 'DashboardWithAccessInfo', - apiVersion: 'v0alpha1', + apiVersion: 'v1alpha1', metadata: { name: 'dash-uid', @@ -28,7 +28,7 @@ const mockDashboardDto: DashboardWithAccessInfo = { const saveDashboardResponse = { kind: 'Dashboard', - apiVersion: 'dashboard.grafana.app/v0alpha1', + apiVersion: 'dashboard.grafana.app/v1alpha1', metadata: { name: 'adh59cn', namespace: 'default', @@ -106,7 +106,7 @@ jest.mock('app/features/live/dashboard/dashboardWatcher', () => ({ ignoreNextSave: jest.fn(), })); -describe('v0 dashboard API', () => { +describe('v1 dashboard API', () => { it('should provide folder annotations', async () => { jest.spyOn(backendSrv, 'getFolderByUid').mockResolvedValue({ id: 1, diff --git a/public/app/features/dashboard/api/v0.ts b/public/app/features/dashboard/api/v1.ts similarity index 99% rename from public/app/features/dashboard/api/v0.ts rename to public/app/features/dashboard/api/v1.ts index f4d90551a66..b3383928bc9 100644 --- a/public/app/features/dashboard/api/v0.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -26,7 +26,7 @@ export class K8sDashboardAPI implements DashboardAPI { constructor() { this.client = new ScopedResourceClient({ group: 'dashboard.grafana.app', - version: 'v0alpha1', + version: 'v1alpha1', resource: 'dashboards', }); } From 16fda6f68609ff3e472fcfbb08187fb38f68b2a2 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:54:47 +0100 Subject: [PATCH 03/27] Authz: Setup access claims for service identity (#100986) * Setup access claims for service identity and add them to identityes without any claims --- pkg/apimachinery/identity/context.go | 67 ++++++++++++------- pkg/apimachinery/identity/static.go | 15 +++-- pkg/services/authn/authnimpl/registration.go | 2 + .../authn/authnimpl/sync/access_claims.go | 32 +++++++++ .../authn/grpcutils/inproc_exchanger.go | 5 +- 5 files changed, 89 insertions(+), 32 deletions(-) create mode 100644 pkg/services/authn/authnimpl/sync/access_claims.go diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 7600f000e7e..8699b3a3ad9 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" + "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" ) @@ -30,27 +31,34 @@ func checkNilRequester(r Requester) bool { return r == nil || (reflect.ValueOf(r).Kind() == reflect.Ptr && reflect.ValueOf(r).IsNil()) } -const serviceName = "service" -const serviceNameForProvisioning = "provisioning" +const ( + serviceName = "service" + serviceNameForProvisioning = "provisioning" +) -// WithServiceIdentity sets an identity representing the service itself in provided org and store it in context. -// This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with -// static permissions so it can be used in legacy code paths. -func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) { - r := &StaticRequester{ +func newInternalIdentity(name string, namespace string, orgID int64) Requester { + return &StaticRequester{ Type: types.TypeAccessPolicy, - Name: serviceName, - UserUID: serviceName, - AuthID: serviceName, - Login: serviceName, + Name: name, + UserUID: name, + AuthID: name, + Login: name, OrgRole: RoleAdmin, + Namespace: namespace, IsGrafanaAdmin: true, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: serviceIdentityPermissions, }, + AccessTokenClaims: ServiceIdentityClaims, } +} +// WithServiceIdentity sets an identity representing the service itself in provided org and store it in context. +// This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with +// static permissions so it can be used in legacy code paths. +func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) { + r := newInternalIdentity(serviceName, "", orgID) return WithRequester(ctx, r), r } @@ -60,21 +68,7 @@ func WithProvisioningIdentitiy(ctx context.Context, namespace string) (context.C return nil, nil, err } - r := &StaticRequester{ - Type: types.TypeAccessPolicy, - Name: serviceNameForProvisioning, - UserUID: serviceNameForProvisioning, - AuthID: serviceNameForProvisioning, - Login: serviceNameForProvisioning, - OrgRole: RoleAdmin, - IsGrafanaAdmin: true, - Namespace: namespace, - OrgID: ns.OrgID, - Permissions: map[int64]map[string][]string{ - ns.OrgID: serviceIdentityPermissions, - }, - } - + r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID) return WithRequester(ctx, r), r, nil } @@ -97,6 +91,14 @@ func getWildcardPermissions(actions ...string) map[string][]string { return permissions } +func getTokenPermissions(groups ...string) []string { + out := make([]string, 0, len(groups)) + for _, group := range groups { + out = append(out, group+":*") + } + return out +} + // serviceIdentityPermissions is a list of wildcard permissions for provided actions. // We should add every action required "internally" here. var serviceIdentityPermissions = getWildcardPermissions( @@ -118,6 +120,19 @@ var serviceIdentityPermissions = getWildcardPermissions( "teams:read", // accesscontrol.ActionTeamsRead, ) +var serviceIdentityTokenPermissions = getTokenPermissions( + "folder.grafana.app", + "dashboard.grafana.app", + "secret.grafana.app", +) + +var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{ + Rest: authn.AccessTokenClaims{ + Permissions: serviceIdentityTokenPermissions, + DelegatedPermissions: serviceIdentityTokenPermissions, + }, +} + func IsServiceIdentity(ctx context.Context) bool { ident, ok := types.AuthInfoFrom(ctx) if !ok { diff --git a/pkg/apimachinery/identity/static.go b/pkg/apimachinery/identity/static.go index 05678db4568..e948f2c1e65 100644 --- a/pkg/apimachinery/identity/static.go +++ b/pkg/apimachinery/identity/static.go @@ -30,10 +30,11 @@ type StaticRequester struct { Namespace string IsGrafanaAdmin bool // Permissions grouped by orgID and actions - Permissions map[int64]map[string][]string - IDToken string - IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims] - CacheKey string + Permissions map[int64]map[string][]string + IDToken string + IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims] + AccessTokenClaims *authnlib.Claims[authnlib.AccessTokenClaims] + CacheKey string } // GetID returns typed id for the entity @@ -62,10 +63,16 @@ func (u *StaticRequester) GetAudience() []string { } func (u *StaticRequester) GetTokenPermissions() []string { + if u.AccessTokenClaims != nil { + return u.AccessTokenClaims.Rest.Permissions + } return []string{} } func (u *StaticRequester) GetTokenDelegatedPermissions() []string { + if u.AccessTokenClaims != nil { + return u.AccessTokenClaims.Rest.DelegatedPermissions + } return []string{} } diff --git a/pkg/services/authn/authnimpl/registration.go b/pkg/services/authn/authnimpl/registration.go index 593a0d823fc..c5af37a3098 100644 --- a/pkg/services/authn/authnimpl/registration.go +++ b/pkg/services/authn/authnimpl/registration.go @@ -145,5 +145,7 @@ func ProvideRegistration( nsSync := sync.ProvideNamespaceSync(cfg) authnSvc.RegisterPostAuthHook(nsSync.SyncNamespace, 150) + authnSvc.RegisterPostAuthHook(sync.AccessClaimsHook, 160) + return Registration{} } diff --git a/pkg/services/authn/authnimpl/sync/access_claims.go b/pkg/services/authn/authnimpl/sync/access_claims.go new file mode 100644 index 00000000000..e66bc440721 --- /dev/null +++ b/pkg/services/authn/authnimpl/sync/access_claims.go @@ -0,0 +1,32 @@ +package sync + +import ( + "context" + + authnlib "github.com/grafana/authlib/authn" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/authn" +) + +func NewAccessClaimsSync() AccessClaimsSync { + return AccessClaimsSync{} +} + +type AccessClaimsSync struct{} + +func AccessClaimsHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error { + if id.AccessTokenClaims == nil { + // When normal authencation flows are used withint grafana we don't have any access token e.g. using user + // session. This makes it impossible to authorize using AccessClient because we don't have any access claims + // with deletegated permissions. To get around this we use the hardcoded delegated + // permissions. + id.AccessTokenClaims = &authnlib.Claims[authnlib.AccessTokenClaims]{ + Rest: authnlib.AccessTokenClaims{ + DelegatedPermissions: identity.ServiceIdentityClaims.Rest.DelegatedPermissions, + }, + } + } + + return nil +} diff --git a/pkg/services/authn/grpcutils/inproc_exchanger.go b/pkg/services/authn/grpcutils/inproc_exchanger.go index f7e24ffed7b..af252ca2fb1 100644 --- a/pkg/services/authn/grpcutils/inproc_exchanger.go +++ b/pkg/services/authn/grpcutils/inproc_exchanger.go @@ -9,6 +9,7 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" ) type inProcExchanger struct { @@ -37,8 +38,8 @@ func createInProcToken() (*authn.TokenExchangeResponse, error) { }, Rest: authn.AccessTokenClaims{ Namespace: "*", - Permissions: []string{"folder.grafana.app:*", "dashboard.grafana.app:*"}, - DelegatedPermissions: []string{"folder.grafana.app:*", "dashboard.grafana.app:*"}, + Permissions: identity.ServiceIdentityClaims.Rest.Permissions, + DelegatedPermissions: identity.ServiceIdentityClaims.Rest.DelegatedPermissions, }, } From 2e32a3d19f73bff62b1825c85ce02c88647aa56e Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Thu, 20 Feb 2025 14:28:03 +0100 Subject: [PATCH 04/27] unistore: fix dashboard conversion events (#101053) fix dashboard conversion events --- pkg/storage/unified/apistore/store.go | 2 +- pkg/storage/unified/apistore/stream.go | 34 +++++++++++++-------- pkg/storage/unified/apistore/stream_test.go | 34 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 pkg/storage/unified/apistore/stream_test.go diff --git a/pkg/storage/unified/apistore/store.go b/pkg/storage/unified/apistore/store.go index 8d3da4b7c33..115443ad099 100644 --- a/pkg/storage/unified/apistore/store.go +++ b/pkg/storage/unified/apistore/store.go @@ -294,7 +294,7 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption } reporter := apierrors.NewClientErrorReporter(500, "WATCH", "") - decoder := newStreamDecoder(client, s.newFunc, predicate, s.codec, cancelWatch) + decoder := newStreamDecoder(client, s.newFunc, predicate, s.codec, cancelWatch, s.opts.InternalConversion) return watch.NewStreamWatcher(decoder, reporter), nil } diff --git a/pkg/storage/unified/apistore/stream.go b/pkg/storage/unified/apistore/stream.go index 9546e3e8b64..e38e1a76d16 100644 --- a/pkg/storage/unified/apistore/stream.go +++ b/pkg/storage/unified/apistore/stream.go @@ -19,25 +19,33 @@ import ( ) type streamDecoder struct { - client resource.ResourceStore_WatchClient - newFunc func() runtime.Object - predicate storage.SelectionPredicate - codec runtime.Codec - cancelWatch context.CancelFunc - done sync.WaitGroup + client resource.ResourceStore_WatchClient + newFunc func() runtime.Object + predicate storage.SelectionPredicate + codec runtime.Codec + cancelWatch context.CancelFunc + done sync.WaitGroup + internalConversion func([]byte, runtime.Object) (runtime.Object, error) } -func newStreamDecoder(client resource.ResourceStore_WatchClient, newFunc func() runtime.Object, predicate storage.SelectionPredicate, codec runtime.Codec, cancelWatch context.CancelFunc) *streamDecoder { +func newStreamDecoder(client resource.ResourceStore_WatchClient, newFunc func() runtime.Object, predicate storage.SelectionPredicate, codec runtime.Codec, cancelWatch context.CancelFunc, internalConversion func([]byte, runtime.Object) (runtime.Object, error)) *streamDecoder { return &streamDecoder{ - client: client, - newFunc: newFunc, - predicate: predicate, - codec: codec, - cancelWatch: cancelWatch, + client: client, + newFunc: newFunc, + predicate: predicate, + codec: codec, + cancelWatch: cancelWatch, + internalConversion: internalConversion, } } func (d *streamDecoder) toObject(w *resource.WatchEvent_Resource) (runtime.Object, error) { - obj, _, err := d.codec.Decode(w.Value, nil, d.newFunc()) + var obj runtime.Object + var err error + if d.internalConversion != nil { + obj, err = d.internalConversion(w.Value, d.newFunc()) + } else { + obj, _, err = d.codec.Decode(w.Value, nil, d.newFunc()) + } if err == nil { accessor, err := utils.MetaAccessor(obj) if err != nil { diff --git a/pkg/storage/unified/apistore/stream_test.go b/pkg/storage/unified/apistore/stream_test.go new file mode 100644 index 00000000000..e707fbda5d8 --- /dev/null +++ b/pkg/storage/unified/apistore/stream_test.go @@ -0,0 +1,34 @@ +package apistore + +import ( + "testing" + + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestStreamDecoder(t *testing.T) { + t.Run("toObject should handle internal conversion", func(t *testing.T) { + called := false + internalConversion := func(data []byte, obj runtime.Object) (runtime.Object, error) { + called = true + return obj, nil + } + + decoder := &streamDecoder{ + newFunc: func() runtime.Object { return &unstructured.Unstructured{} }, + internalConversion: internalConversion, + } + + event := &resource.WatchEvent_Resource{ + Value: []byte("test"), + } + + obj, err := decoder.toObject(event) + require.NoError(t, err) + require.NotNil(t, obj) + require.True(t, called, "internal conversion function should have been called") + }) +} From 40e7646348f0157a9fd5d29d4f942475ef30b51f Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Thu, 20 Feb 2025 14:38:28 +0100 Subject: [PATCH 05/27] Bugfix: Fixing issue where dataSource property got froozen (#101058) Cloning datasource before passing it to extensions. --- public/app/features/datasources/components/EditDataSource.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/datasources/components/EditDataSource.tsx b/public/app/features/datasources/components/EditDataSource.tsx index 64cbc49ddfd..e948bad2013 100644 --- a/public/app/features/datasources/components/EditDataSource.tsx +++ b/public/app/features/datasources/components/EditDataSource.tsx @@ -1,4 +1,5 @@ import { AnyAction } from '@reduxjs/toolkit'; +import { cloneDeep } from 'lodash'; import { useMemo } from 'react'; import * as React from 'react'; @@ -210,7 +211,7 @@ export function EditDataSourceView({
From 92304646e61a6f08de5f4901460f6ff430283770 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 20 Feb 2025 14:53:04 +0100 Subject: [PATCH 06/27] Advisor: Add navigation link (#101049) --- pkg/services/navtree/navtreeimpl/applinks.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index cf78f67e1d4..1adc19e0d57 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -313,6 +313,10 @@ func (s *ServiceImpl) readNavigationSettings() { s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Metrics"} } + if s.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAdvisor) { + s.navigationAppConfig["grafana-advisor-app"] = NavigationAppConfig{SectionID: navtree.NavIDCfg, Text: "Advisor"} + } + s.navigationAppPathConfig = map[string]NavigationAppConfig{ "/a/grafana-auth-app": {SectionID: navtree.NavIDCfgAccess, SortWeight: 2}, } From 4a8b1ba61bc0b596af2f9fe2412024a87d10dcd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 20 Feb 2025 15:03:08 +0100 Subject: [PATCH 07/27] tests(unified-storage): use unified storage grpc client (#101063) --- pkg/storage/unified/client.go | 6 +++--- pkg/storage/unified/sql/test/integration_test.go | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index b4200f31dee..305606bafef 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -115,7 +115,7 @@ func newClient(opts options.StorageOptions, } // Create a connection to the gRPC server. - conn, err := grpcConn(opts.Address, reg) + conn, err := GrpcConn(opts.Address, reg) if err != nil { return nil, err } @@ -161,8 +161,8 @@ func newResourceClient(conn *grpc.ClientConn, cfg *setting.Cfg, features feature return resource.NewRemoteResourceClient(tracer, conn, clientCfgMapping(grpcutils.ReadGrpcClientConfig(cfg)), cfg.Env == setting.Dev) } -// grpcConn creates a new gRPC connection to the provided address. -func grpcConn(address string, reg prometheus.Registerer) (*grpc.ClientConn, error) { +// GrpcConn creates a new gRPC connection to the provided address. +func GrpcConn(address string, reg prometheus.Registerer) (*grpc.ClientConn, error) { // This works for now as the Provide function is only called once during startup. // We might eventually want to tight this factory to a struct for more runtime control. metrics := clientMetrics{ diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 3d75ed1da3f..c47a2700f1f 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -8,8 +8,6 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/grafana/authlib/authn" @@ -20,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" @@ -510,7 +509,7 @@ func TestClientServer(t *testing.T) { }) t.Run("Create a client", func(t *testing.T) { - conn, err := grpc.NewClient(svc.GetAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := unified.GrpcConn(svc.GetAddress(), prometheus.NewPedanticRegistry()) require.NoError(t, err) client, err = resource.NewRemoteResourceClient(tracing.NewNoopTracerService(), conn, authn.GrpcClientConfig{}, true) require.NoError(t, err) From 68ce949fffec5f45071b60721975e2b2d7bc83b4 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Thu, 20 Feb 2025 09:17:52 -0500 Subject: [PATCH 08/27] Alerting: Fix token-based Slack image upload to work with channel names (#100988) Upgrades grafana/alerting to 00d6703 which includes the fix. --- go.mod | 2 +- go.sum | 4 ++-- pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/apistore/go.sum | 4 ++-- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/go.sum | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 2b11e0fabe6..b0880354e19 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20250218192337-92d5f29b5664 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index d8fdfe5fe39..a48dd64155d 100644 --- a/go.sum +++ b/go.sum @@ -1511,8 +1511,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250218192337-92d5f29b5664 h1:avtd90OVg2wMHz5pUsTWypu4rEt5bn5H0kOLXa7xgqc= -github.com/grafana/alerting v0.0.0-20250218192337-92d5f29b5664/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= +github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 h1:GCgtfJ0G/uHdmZl9oTz2ok31zFT1HJEhVcsmVJFpSBg= +github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 14c3d477026..0d3f27f6f58 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -192,7 +192,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/grafana/alerting v0.0.0-20250218192337-92d5f29b5664 // indirect + github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 // indirect github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 42ff04a48e9..0aaa26d90ec 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -566,8 +566,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250218192337-92d5f29b5664 h1:avtd90OVg2wMHz5pUsTWypu4rEt5bn5H0kOLXa7xgqc= -github.com/grafana/alerting v0.0.0-20250218192337-92d5f29b5664/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= +github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 h1:GCgtfJ0G/uHdmZl9oTz2ok31zFT1HJEhVcsmVJFpSBg= +github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index b245031350a..1e999c1bba3 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -117,7 +117,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250218192337-92d5f29b5664 // indirect + github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 666d232f374..56e82e0a61a 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -397,8 +397,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/alerting v0.0.0-20250218192337-92d5f29b5664 h1:avtd90OVg2wMHz5pUsTWypu4rEt5bn5H0kOLXa7xgqc= -github.com/grafana/alerting v0.0.0-20250218192337-92d5f29b5664/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= +github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 h1:GCgtfJ0G/uHdmZl9oTz2ok31zFT1HJEhVcsmVJFpSBg= +github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= From c969a7251e4d9d60bb2db7ee0e871c3159aefb35 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Thu, 20 Feb 2025 15:22:27 +0100 Subject: [PATCH 09/27] PluginExtensions: Fixed so we expose the proper types for usePluginComponents (#100949) * Fixed usage of incorrect type in the 'usePluginComponents' hook. * improved betterer. * Changed import path of usePluginLinks types. * improved betterer result. --- .betterer.results | 6 +-- .../grafana-runtime/src/services/index.ts | 30 ++++++++++--- .../pluginExtensions/getPluginExtensions.ts | 44 +------------------ .../pluginExtensions/usePluginComponent.ts | 11 +++-- .../pluginExtensions/usePluginComponents.ts | 18 ++++++-- .../pluginExtensions/usePluginFunctions.ts | 12 ++++- .../pluginExtensions/usePluginLinks.ts | 13 +++++- .../extensions/usePluginComponents.tsx | 7 +-- .../plugins/extensions/usePluginLinks.tsx | 5 +-- 9 files changed, 72 insertions(+), 74 deletions(-) diff --git a/.betterer.results b/.betterer.results index 42f02a2db9f..106ccbad80f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5599,15 +5599,11 @@ exports[`better eslint`] = { [0, 0, 0, "\'@grafana/data/src/types/pluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], "public/app/features/plugins/extensions/usePluginComponents.tsx:5381": [ - [0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/plugins/extensions/usePluginFunctions.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/plugins/extensions/usePluginLinks.tsx:5381": [ - [0, 0, 0, "\'@grafana/runtime/src/services/pluginExtensions/getPluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/plugins/extensions/validators.ts:5381": [ [0, 0, 0, "\'@grafana/data/src/types/pluginExtensions\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index 5e8892c5cc7..3ceef02923a 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -21,9 +21,6 @@ export { type GetPluginExtensionsResult, type UsePluginExtensions, type UsePluginExtensionsResult, - type UsePluginComponentResult, - type UsePluginFunctionsOptions, - type UsePluginFunctionsResult, } from './pluginExtensions/getPluginExtensions'; export { setPluginExtensionsHook, @@ -32,10 +29,29 @@ export { usePluginComponentExtensions, } from './pluginExtensions/usePluginExtensions'; -export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent'; -export { setPluginComponentsHook, usePluginComponents } from './pluginExtensions/usePluginComponents'; -export { setPluginLinksHook, usePluginLinks } from './pluginExtensions/usePluginLinks'; -export { setPluginFunctionsHook, usePluginFunctions } from './pluginExtensions/usePluginFunctions'; +export { + setPluginComponentHook, + usePluginComponent, + type UsePluginComponentResult, +} from './pluginExtensions/usePluginComponent'; +export { + setPluginComponentsHook, + usePluginComponents, + type UsePluginComponentsResult, + type UsePluginComponentsOptions, +} from './pluginExtensions/usePluginComponents'; +export { + setPluginLinksHook, + usePluginLinks, + type UsePluginLinksOptions, + type UsePluginLinksResult, +} from './pluginExtensions/usePluginLinks'; +export { + setPluginFunctionsHook, + usePluginFunctions, + type UsePluginFunctionsOptions, + type UsePluginFunctionsResult, +} from './pluginExtensions/usePluginFunctions'; export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils'; export { setCurrentUser } from './user'; diff --git a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts index 3d5de8ab3dc..edc5a48db6b 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts @@ -1,10 +1,4 @@ -import type { - PluginExtension, - PluginExtensionLink, - PluginExtensionComponent, - PluginExtensionFunction, - PluginExtensionComponentMeta, -} from '@grafana/data'; +import type { PluginExtension, PluginExtensionLink, PluginExtensionComponent } from '@grafana/data'; import { isPluginExtensionComponent, isPluginExtensionLink } from './utils'; @@ -23,11 +17,6 @@ export type GetPluginExtensionsOptions = { limitPerPlugin?: number; }; -export type UsePluginComponentOptions = { - extensionPointId: string; - limitPerPlugin?: number; -}; - export type GetPluginExtensionsResult = { extensions: T[]; }; @@ -37,37 +26,6 @@ export type UsePluginExtensionsResult = { isLoading: boolean; }; -export type UsePluginComponentResult = { - component: React.ComponentType | undefined | null; - isLoading: boolean; -}; - -export type UsePluginComponentsResult = { - components: Array & { meta: PluginExtensionComponentMeta }>; - isLoading: boolean; -}; - -export type UsePluginLinksOptions = { - extensionPointId: string; - context?: object | Record; - limitPerPlugin?: number; -}; - -export type UsePluginLinksResult = { - isLoading: boolean; - links: PluginExtensionLink[]; -}; - -export type UsePluginFunctionsOptions = { - extensionPointId: string; - limitPerPlugin?: number; -}; - -export type UsePluginFunctionsResult = { - isLoading: boolean; - functions: Array>; -}; - let singleton: GetPluginExtensions | undefined; export function setPluginExtensionGetter(instance: GetPluginExtensions): void { diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginComponent.ts b/packages/grafana-runtime/src/services/pluginExtensions/usePluginComponent.ts index a11a37389c3..40fb6b24880 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/usePluginComponent.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/usePluginComponent.ts @@ -1,6 +1,9 @@ -import { UsePluginComponentResult } from './getPluginExtensions'; +export type UsePluginComponent = (componentId: string) => UsePluginComponentResult; -export type UsePluginComponent = (id: string) => UsePluginComponentResult; +export type UsePluginComponentResult = { + component: React.ComponentType | undefined | null; + isLoading: boolean; +}; let singleton: UsePluginComponent | undefined; @@ -12,9 +15,9 @@ export function setPluginComponentHook(hook: UsePluginComponent): void { singleton = hook; } -export function usePluginComponent(id: string): UsePluginComponentResult { +export function usePluginComponent(componentId: string): UsePluginComponentResult { if (!singleton) { throw new Error('setPluginComponentHook(options) can only be used after the Grafana instance has started.'); } - return singleton(id) as UsePluginComponentResult; + return singleton(componentId) as UsePluginComponentResult; } diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginComponents.ts b/packages/grafana-runtime/src/services/pluginExtensions/usePluginComponents.ts index aafe2bb8aac..649c54d0414 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/usePluginComponents.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/usePluginComponents.ts @@ -1,13 +1,23 @@ -import { GetPluginExtensionsOptions, UsePluginComponentsResult } from './getPluginExtensions'; +import { PluginExtensionComponentMeta } from '@grafana/data'; + +export type UsePluginComponentsOptions = { + extensionPointId: string; + limitPerPlugin?: number; +}; + +export type UsePluginComponentsResult = { + components: Array & { meta: PluginExtensionComponentMeta }>; + isLoading: boolean; +}; export type UsePluginComponents = ( - options: GetPluginExtensionsOptions + options: UsePluginComponentsOptions ) => UsePluginComponentsResult; let singleton: UsePluginComponents | undefined; export function setPluginComponentsHook(hook: UsePluginComponents): void { - // We allow overriding the registry in tests + // We allow overriding the hook in tests if (singleton && process.env.NODE_ENV !== 'test') { throw new Error('setPluginComponentsHook() function should only be called once, when Grafana is starting.'); } @@ -15,7 +25,7 @@ export function setPluginComponentsHook(hook: UsePluginComponents): void { } export function usePluginComponents( - options: GetPluginExtensionsOptions + options: UsePluginComponentsOptions ): UsePluginComponentsResult { if (!singleton) { throw new Error('setPluginComponentsHook(options) can only be used after the Grafana instance has started.'); diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts b/packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts index 1eb86b70e14..2e480855b38 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/usePluginFunctions.ts @@ -1,4 +1,14 @@ -import { UsePluginFunctionsOptions, UsePluginFunctionsResult } from './getPluginExtensions'; +import { PluginExtensionFunction } from '@grafana/data'; + +export type UsePluginFunctionsOptions = { + extensionPointId: string; + limitPerPlugin?: number; +}; + +export type UsePluginFunctionsResult = { + isLoading: boolean; + functions: Array>; +}; export type UsePluginFunctions = (options: UsePluginFunctionsOptions) => UsePluginFunctionsResult; diff --git a/packages/grafana-runtime/src/services/pluginExtensions/usePluginLinks.ts b/packages/grafana-runtime/src/services/pluginExtensions/usePluginLinks.ts index d90d88cf4f7..d4940127f61 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/usePluginLinks.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/usePluginLinks.ts @@ -1,4 +1,15 @@ -import { UsePluginLinksOptions, UsePluginLinksResult } from './getPluginExtensions'; +import { PluginExtensionLink } from '@grafana/data'; + +export type UsePluginLinksOptions = { + extensionPointId: string; + context?: object | Record; + limitPerPlugin?: number; +}; + +export type UsePluginLinksResult = { + isLoading: boolean; + links: PluginExtensionLink[]; +}; export type UsePluginLinks = (options: UsePluginLinksOptions) => UsePluginLinksResult; diff --git a/public/app/features/plugins/extensions/usePluginComponents.tsx b/public/app/features/plugins/extensions/usePluginComponents.tsx index 0e59834db94..d5259a85c41 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.tsx @@ -2,10 +2,7 @@ import { useMemo } from 'react'; import { useObservable } from 'react-use'; import { PluginExtensionComponentMeta, PluginExtensionTypes, usePluginContext } from '@grafana/data'; -import { - UsePluginComponentOptions, - UsePluginComponentsResult, -} from '@grafana/runtime/src/services/pluginExtensions/getPluginExtensions'; +import { UsePluginComponentsOptions, UsePluginComponentsResult } from '@grafana/runtime'; import { useAddedComponentsRegistry } from './ExtensionRegistriesContext'; import * as errors from './errors'; @@ -19,7 +16,7 @@ import { isExtensionPointIdValid, isExtensionPointMetaInfoMissing } from './vali export function usePluginComponents({ limitPerPlugin, extensionPointId, -}: UsePluginComponentOptions): UsePluginComponentsResult { +}: UsePluginComponentsOptions): UsePluginComponentsResult { const registry = useAddedComponentsRegistry(); const registryState = useObservable(registry.asObservable()); const pluginContext = usePluginContext(); diff --git a/public/app/features/plugins/extensions/usePluginLinks.tsx b/public/app/features/plugins/extensions/usePluginLinks.tsx index 58efcc4e3e6..6ecaf6b71b9 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.tsx @@ -3,10 +3,7 @@ import { useMemo } from 'react'; import { useObservable } from 'react-use'; import { PluginExtensionLink, PluginExtensionTypes, usePluginContext } from '@grafana/data'; -import { - UsePluginLinksOptions, - UsePluginLinksResult, -} from '@grafana/runtime/src/services/pluginExtensions/getPluginExtensions'; +import { UsePluginLinksOptions, UsePluginLinksResult } from '@grafana/runtime'; import { useAddedLinksRegistry } from './ExtensionRegistriesContext'; import * as errors from './errors'; From e36c9220f9acee6d16b8cec9bb4b7408e0eaf545 Mon Sep 17 00:00:00 2001 From: xavi <114113189+volcanonoodle@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:39:03 +0100 Subject: [PATCH 10/27] IAM: fix grafana_com OAuth connector config overriding (#101066) --- pkg/login/social/connectors/grafana_com_oauth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/login/social/connectors/grafana_com_oauth.go b/pkg/login/social/connectors/grafana_com_oauth.go index 84ea01632d1..a4efd5cd116 100644 --- a/pkg/login/social/connectors/grafana_com_oauth.go +++ b/pkg/login/social/connectors/grafana_com_oauth.go @@ -39,13 +39,13 @@ type OrgRecord struct { } func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGrafanaCom { - s := newSocialBase(social.GrafanaComProviderName, orgRoleMapper, info, features, cfg) - // Override necessary settings info.AuthUrl = cfg.GrafanaComURL + "/oauth2/authorize" info.TokenUrl = cfg.GrafanaComURL + "/api/oauth2/token" info.AuthStyle = "inheader" + s := newSocialBase(social.GrafanaComProviderName, orgRoleMapper, info, features, cfg) + allowedOrganizations, err := util.SplitStringWithError(info.Extra[allowedOrganizationsKey]) if err != nil { s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GrafanaComProviderName, "error", err) From 74e621f37793755d9bf44f254ad8299de8b2cf03 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:53:06 -0300 Subject: [PATCH 11/27] fix(unified-storage): "sharedwithme" search in legacy modes returns everything (#101054) * fix "sharedwithme" search returning everything when user does not have any shared dashboards --- pkg/registry/apis/dashboard/search.go | 10 +- pkg/registry/apis/dashboard/search_test.go | 162 ++++++++++++++++++++- 2 files changed, 167 insertions(+), 5 deletions(-) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 3060972a32d..fe97d4cabb1 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -333,10 +333,14 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { return } - // hijacks the "name" query param to only search for shared dashboard UIDs - if len(dashboardUIDs) > 0 { - names = append(names, dashboardUIDs...) + if len(dashboardUIDs) == 0 { + s.write(w, dashboardv0alpha1.SearchResults{ + Hits: []dashboardv0alpha1.DashboardHit{}, + }) + return } + // hijacks the "name" query param to only search for shared dashboard UIDs + names = append(names, dashboardUIDs...) } else if folder != "" { if folder == rootFolder { folder = "" // root folder is empty in the search index diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index 3d8967359c9..de2658c28c1 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -357,7 +357,131 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { searchHandler.DoSearch(rr, req) - assert.Equal(t, mockClient.CallCount, 1) + assert.Equal(t, mockClient.CallCount, 0) + }) + + t.Run("should return empty result without searching if user does not have shared dashboards", func(t *testing.T) { + mockClient := &MockClient{} + + features := featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering) + searchHandler := SearchHandler{ + log: log.New("test", "test"), + client: mockClient, + tracer: tracing.NewNoopTracerService(), + features: features, + } + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search?folder=sharedwithme", nil) + req.Header.Add("content-type", "application/json") + // "Permissions" prop in "SignedInUser" is where we store the uid of dashboards shared with the user + // doesn't exist here, which represents a user without any shared dashboards + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + assert.Equal(t, mockClient.CallCount, 0) + + resp := rr.Result() + defer func() { + if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } + }() + + p := &v0alpha1.SearchResults{} + err := json.NewDecoder(resp.Body).Decode(p) + require.NoError(t, err) + assert.Equal(t, 0, len(p.Hits)) + }) + + t.Run("should return empty result if user has access to folder of all shared dashboards", func(t *testing.T) { + // dashboardSearchRequest + mockResponse1 := &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "folder", + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "dashboardinroot", + Resource: "dashboard", + }, + Cells: [][]byte{[]byte("")}, // root folder doesn't have uid + }, + { + Key: &resource.ResourceKey{ + Name: "dashboardinpublicfolder", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("publicfolder"), // folder uid + }, + }, + }, + }, + } + + // folderSearchRequest + mockResponse2 := &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "folder", + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "publicfolder", + Resource: "folder", + }, + Cells: [][]byte{ + []byte(""), // root folder uid + }, + }, + }, + }, + } + + mockClient := &MockClient{ + MockResponses: []*resource.ResourceSearchResponse{mockResponse1, mockResponse2}, + } + + features := featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering) + searchHandler := SearchHandler{ + log: log.New("test", "test"), + client: mockClient, + tracer: tracing.NewNoopTracerService(), + features: features, + } + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search?folder=sharedwithme", nil) + req.Header.Add("content-type", "application/json") + allPermissions := make(map[int64]map[string][]string) + permissions := make(map[string][]string) + permissions[dashboards.ActionDashboardsRead] = []string{"dashboards:uid:dashboardinroot", "dashboards:uid:dashboardinpublicfolder"} + allPermissions[1] = permissions + // "Permissions" is where we store the uid of dashboards shared with the user + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test", OrgID: 1, Permissions: allPermissions})) + + searchHandler.DoSearch(rr, req) + + assert.Equal(t, mockClient.CallCount, 2) + + resp := rr.Result() + defer func() { + if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } + }() + + p := &v0alpha1.SearchResults{} + err := json.NewDecoder(resp.Body).Decode(p) + require.NoError(t, err) + assert.Equal(t, 0, len(p.Hits)) }) t.Run("should return the dashboards shared with the user", func(t *testing.T) { @@ -421,8 +545,29 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { }, } + mockResponse3 := &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "folder", + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "dashboardinprivatefolder", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("privatefolder"), // folder uid + }, + }, + }, + }, + } + mockClient := &MockClient{ - MockResponses: []*resource.ResourceSearchResponse{mockResponse1, mockResponse2}, + MockResponses: []*resource.ResourceSearchResponse{mockResponse1, mockResponse2, mockResponse3}, } features := featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering) @@ -439,6 +584,7 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { permissions := make(map[string][]string) permissions[dashboards.ActionDashboardsRead] = []string{"dashboards:uid:dashboardinroot", "dashboards:uid:dashboardinprivatefolder", "dashboards:uid:dashboardinpublicfolder"} allPermissions[1] = permissions + // "Permissions" is where we store the uid of dashboards shared with the user req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test", OrgID: 1, Permissions: allPermissions})) searchHandler.DoSearch(rr, req) @@ -455,6 +601,18 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { // permission to read thirdCall := mockClient.MockCalls[2] assert.Equal(t, thirdCall.Options.Fields[0].Values, []string{"dashboardinprivatefolder"}) + + resp := rr.Result() + defer func() { + if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } + }() + + p := &v0alpha1.SearchResults{} + err := json.NewDecoder(resp.Body).Decode(p) + require.NoError(t, err) + assert.Equal(t, len(mockResponse3.Results.Rows), len(p.Hits)) }) } From 14886410d6ffc3e36ec9925985f00d3d324a09a9 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:07:06 +0100 Subject: [PATCH 12/27] Zanzana: Use shared auth interceptor for zanzana and pass tracer (#100968) * Use shared auth interceptor for zanzana and pass tracer --- .../authn/grpcutils/grpc_authenticator.go | 7 ++- pkg/services/authz/zanzana.go | 60 ++++++++----------- pkg/services/authz/zanzana/server.go | 5 +- .../authz/zanzana/server/openfga_server.go | 3 - pkg/services/authz/zanzana/server/server.go | 11 ++-- .../zanzana/server/server_batch_check.go | 2 +- .../authz/zanzana/server/server_check.go | 2 +- .../authz/zanzana/server/server_list.go | 6 +- .../authz/zanzana/server/server_read.go | 2 +- .../authz/zanzana/server/server_test.go | 3 +- .../authz/zanzana/server/server_write.go | 2 +- 11 files changed, 48 insertions(+), 55 deletions(-) diff --git a/pkg/services/authn/grpcutils/grpc_authenticator.go b/pkg/services/authn/grpcutils/grpc_authenticator.go index b85457dfaf9..542a5268eee 100644 --- a/pkg/services/authn/grpcutils/grpc_authenticator.go +++ b/pkg/services/authn/grpcutils/grpc_authenticator.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/authlib/types" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" @@ -22,7 +23,7 @@ import ( ) func NewInProcGrpcAuthenticator() interceptors.Authenticator { - return newAuthenticator( + return NewAuthenticatorInterceptor( authn.NewDefaultAuthenticator( authn.NewUnsafeAccessTokenVerifier(authn.VerifierConfig{}), authn.NewUnsafeIDTokenVerifier(authn.VerifierConfig{}), @@ -46,7 +47,7 @@ func NewAuthenticator(cfg *GrpcServerConfig, tracer tracing.Tracer) interceptors authn.NewIDTokenVerifier(authn.VerifierConfig{}, kr), ) - return newAuthenticator(auth, tracer) + return NewAuthenticatorInterceptor(auth, tracer) } func NewAuthenticatorWithFallback(cfg *setting.Cfg, reg prometheus.Registerer, tracer tracing.Tracer, fallback interceptors.Authenticator) interceptors.Authenticator { @@ -64,7 +65,7 @@ func NewAuthenticatorWithFallback(cfg *setting.Cfg, reg prometheus.Registerer, t } } -func newAuthenticator(auth authn.Authenticator, tracer tracing.Tracer) interceptors.Authenticator { +func NewAuthenticatorInterceptor(auth authn.Authenticator, tracer trace.Tracer) interceptors.Authenticator { return interceptors.AuthenticatorFunc(func(ctx context.Context) (context.Context, error) { ctx, span := tracer.Start(ctx, "grpcutils.Authenticate") defer span.End() diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 60bea71c2ee..8d644167528 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -12,27 +12,26 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" healthv1pb "google.golang.org/grpc/health/grpc_health_v1" - "google.golang.org/grpc/metadata" authnlib "github.com/grafana/authlib/authn" authzv1 "github.com/grafana/authlib/authz/proto/v1" - claims "github.com/grafana/authlib/types" + "github.com/grafana/authlib/types" "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/authn/grpcutils" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" - "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" "github.com/grafana/grafana/pkg/setting" ) // ProvideZanzana used to register ZanzanaClient. // It will also start an embedded ZanzanaSever if mode is set to "embedded". -func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureToggles) (zanzana.Client, error) { +func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features featuremgmt.FeatureToggles) (zanzana.Client, error) { if !features.IsEnabledGlobally(featuremgmt.FlagZanzana) { return zanzana.NewNoopClient(), nil } @@ -82,7 +81,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg return nil, fmt.Errorf("failed to start zanzana: %w", err) } - srv, err := zanzana.NewServer(cfg.ZanzanaServer, openfga, logger) + srv, err := zanzana.NewServer(cfg.ZanzanaServer, openfga, logger, tracer) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } @@ -90,7 +89,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg channel := &inprocgrpc.Channel{} // Put * as a namespace so we can properly authorize request with in-proc mode channel.WithServerUnaryInterceptor(grpcAuth.UnaryServerInterceptor(func(ctx context.Context) (context.Context, error) { - ctx = claims.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{ + ctx = types.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{ Rest: authnlib.AccessTokenClaims{ Namespace: "*", }, @@ -144,6 +143,18 @@ type Zanzana struct { } func (z *Zanzana) start(ctx context.Context) error { + tracingCfg, err := tracing.ProvideTracingConfig(z.cfg) + if err != nil { + return err + } + + tracingCfg.ServiceName = "zanzana" + + tracer, err := tracing.ProvideService(tracingCfg) + if err != nil { + return err + } + store, err := zanzana.NewStore(z.cfg, z.logger) if err != nil { return fmt.Errorf("failed to initilize zanana store: %w", err) @@ -154,46 +165,27 @@ func (z *Zanzana) start(ctx context.Context) error { return fmt.Errorf("failed to start zanzana: %w", err) } - zanzanaServer, err := zanzana.NewServer(z.cfg.ZanzanaServer, openfgaServer, z.logger) + zanzanaServer, err := zanzana.NewServer(z.cfg.ZanzanaServer, openfgaServer, z.logger, tracer) if err != nil { return fmt.Errorf("failed to start zanzana: %w", err) } - tracingCfg, err := tracing.ProvideTracingConfig(z.cfg) - if err != nil { - return err - } - tracingCfg.ServiceName = "zanzana" - - tracer, err := tracing.ProvideService(tracingCfg) - if err != nil { - return err - } - authenticator := authnlib.NewAccessTokenAuthenticator( authnlib.NewAccessTokenVerifier( - authnlib.VerifierConfig{ - AllowedAudiences: []string{authzServiceAudience}, - }, + authnlib.VerifierConfig{AllowedAudiences: []string{authzServiceAudience}}, authnlib.NewKeyRetriever(authnlib.KeyRetrieverConfig{ SigningKeysURL: z.cfg.ZanzanaServer.SigningKeysURL, }), ), ) - authfn := interceptors.AuthenticatorFunc(func(ctx context.Context) (context.Context, error) { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return nil, fmt.Errorf("missing metadata") - } - c, err := authenticator.Authenticate(ctx, authnlib.NewGRPCTokenProvider(md)) - if err != nil { - return nil, err - } - return claims.WithAuthInfo(ctx, c), nil - }) - - z.handle, err = grpcserver.ProvideService(z.cfg, z.features, authfn, tracer, prometheus.DefaultRegisterer) + z.handle, err = grpcserver.ProvideService( + z.cfg, + z.features, + grpcutils.NewAuthenticatorInterceptor(authenticator, tracer), + tracer, + prometheus.DefaultRegisterer, + ) if err != nil { return fmt.Errorf("failed to create zanzana grpc server: %w", err) } diff --git a/pkg/services/authz/zanzana/server.go b/pkg/services/authz/zanzana/server.go index b1b79cc179f..859e5d9a9bc 100644 --- a/pkg/services/authz/zanzana/server.go +++ b/pkg/services/authz/zanzana/server.go @@ -7,13 +7,14 @@ import ( openfgastorage "github.com/openfga/openfga/pkg/storage" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authz/zanzana/server" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/setting" ) -func NewServer(cfg setting.ZanzanaServerSettings, openfga server.OpenFGAServer, logger log.Logger) (*server.Server, error) { - return server.NewServer(cfg, openfga, logger) +func NewServer(cfg setting.ZanzanaServerSettings, openfga server.OpenFGAServer, logger log.Logger, tracer tracing.Tracer) (*server.Server, error) { + return server.NewServer(cfg, openfga, logger, tracer) } func NewHealthServer(target server.DiagnosticServer) *server.HealthServer { diff --git a/pkg/services/authz/zanzana/server/openfga_server.go b/pkg/services/authz/zanzana/server/openfga_server.go index ba994a0cab8..2eb854a81c1 100644 --- a/pkg/services/authz/zanzana/server/openfga_server.go +++ b/pkg/services/authz/zanzana/server/openfga_server.go @@ -36,9 +36,6 @@ func NewOpenFGAServer(cfg setting.ZanzanaServerSettings, store storage.OpenFGADa server.WithListObjectsDeadline(cfg.ListObjectsDeadline), } - // FIXME(kalleep): Interceptors - // We probably need to at least need to add store id interceptor also - // would be nice to inject our own requestid? srv, err := server.NewServerWithOpts(opts...) if err != nil { return nil, err diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index 822a7c2828b..9ec9a79c735 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -9,12 +9,12 @@ import ( "github.com/fullstorydev/grpchan/inprocgrpc" authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" - "go.opentelemetry.io/otel" "google.golang.org/protobuf/types/known/wrapperspb" dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" "github.com/grafana/grafana/pkg/setting" @@ -25,8 +25,6 @@ const cacheCleanInterval = 2 * time.Minute var _ authzv1.AuthzServiceServer = (*Server)(nil) var _ authzextv1.AuthzExtentionServiceServer = (*Server)(nil) -var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/authz/zanzana/server") - type OpenFGAServer interface { openfgav1.OpenFGAServiceServer IsReady(ctx context.Context) (bool, error) @@ -40,10 +38,12 @@ type Server struct { openfgaClient openfgav1.OpenFGAServiceClient cfg setting.ZanzanaServerSettings - logger log.Logger stores map[string]storeInfo storesMU *sync.Mutex cache *localcache.CacheService + + logger log.Logger + tracer tracing.Tracer } type storeInfo struct { @@ -51,7 +51,7 @@ type storeInfo struct { ModelID string } -func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger log.Logger) (*Server, error) { +func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger log.Logger, tracer tracing.Tracer) (*Server, error) { channel := &inprocgrpc.Channel{} openfgav1.RegisterOpenFGAServiceServer(channel, openfga) openFGAClient := openfgav1.NewOpenFGAServiceClient(channel) @@ -64,6 +64,7 @@ func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger cfg: cfg, cache: localcache.New(cfg.CheckQueryCacheTTL, cacheCleanInterval), logger: logger, + tracer: tracer, } return s, nil diff --git a/pkg/services/authz/zanzana/server/server_batch_check.go b/pkg/services/authz/zanzana/server/server_batch_check.go index dcfade5d73b..8b9195b01c8 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check.go +++ b/pkg/services/authz/zanzana/server/server_batch_check.go @@ -11,7 +11,7 @@ import ( ) func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) { - ctx, span := tracer.Start(ctx, "server.BatchCheck") + ctx, span := s.tracer.Start(ctx, "server.BatchCheck") defer span.End() if err := authorize(ctx, r.GetNamespace()); err != nil { diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index 6cd41a57b5b..838a39aba83 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -10,7 +10,7 @@ import ( ) func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) { - ctx, span := tracer.Start(ctx, "server.Check") + ctx, span := s.tracer.Start(ctx, "server.Check") defer span.End() if err := authorize(ctx, r.GetNamespace()); err != nil { diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index 85b1e961f46..c435cbc90cb 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -15,7 +15,7 @@ import ( ) func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.ListResponse, error) { - ctx, span := tracer.Start(ctx, "server.List") + ctx, span := s.tracer.Start(ctx, "server.List") defer span.End() if err := authorize(ctx, r.GetNamespace()); err != nil { @@ -141,7 +141,7 @@ func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequ type listFn func(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) func (s *Server) listObjectCached(ctx context.Context, req *openfgav1.ListObjectsRequest, fn listFn) (*openfgav1.ListObjectsResponse, error) { - ctx, span := tracer.Start(ctx, "server.listObjectCached") + ctx, span := s.tracer.Start(ctx, "server.listObjectCached") defer span.End() key, err := getRequestHash(req) @@ -163,7 +163,7 @@ func (s *Server) listObjectCached(ctx context.Context, req *openfgav1.ListObject } func (s *Server) streamedListObjects(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) { - ctx, span := tracer.Start(ctx, "server.streamedListObjects") + ctx, span := s.tracer.Start(ctx, "server.streamedListObjects") defer span.End() r := &openfgav1.StreamedListObjectsRequest{ diff --git a/pkg/services/authz/zanzana/server/server_read.go b/pkg/services/authz/zanzana/server/server_read.go index e9c36727dbb..755f769c11b 100644 --- a/pkg/services/authz/zanzana/server/server_read.go +++ b/pkg/services/authz/zanzana/server/server_read.go @@ -10,7 +10,7 @@ import ( ) func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) { - ctx, span := tracer.Start(ctx, "server.Read") + ctx, span := s.tracer.Start(ctx, "server.Read") defer span.End() if err := authorize(ctx, req.GetNamespace()); err != nil { diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index a300be92204..8a1fd1786ea 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" "github.com/grafana/grafana/pkg/services/authz/zanzana/store" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" @@ -75,7 +76,7 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { openfga, err := NewOpenFGAServer(cfg.ZanzanaServer, store, log.NewNopLogger()) require.NoError(t, err) - srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger()) + srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger(), tracing.NewNoopTracerService()) require.NoError(t, err) storeInf, err := srv.getStoreInfo(context.Background(), namespace) diff --git a/pkg/services/authz/zanzana/server/server_write.go b/pkg/services/authz/zanzana/server/server_write.go index 9ae5b70f9d1..0aa932bc3f8 100644 --- a/pkg/services/authz/zanzana/server/server_write.go +++ b/pkg/services/authz/zanzana/server/server_write.go @@ -10,7 +10,7 @@ import ( ) func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*authzextv1.WriteResponse, error) { - ctx, span := tracer.Start(ctx, "server.Write") + ctx, span := s.tracer.Start(ctx, "server.Write") defer span.End() if err := authorize(ctx, req.GetNamespace()); err != nil { From 522e75c750697e4385ad82d31f7f858ae9494bf8 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Thu, 20 Feb 2025 16:11:16 +0100 Subject: [PATCH 13/27] build: runs codegen on enterprise apis as well (#101057) Signed-off-by: bergquist --- hack/update-codegen.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 4df77aec6c3..57841bbbeb2 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -87,4 +87,8 @@ grafana::codegen:run pkg grafana::codegen:run pkg/apimachinery grafana::codegen:run pkg/aggregator +if [ -d "pkg/extensions/apis" ]; then + grafana::codegen:run pkg/extensions +fi + echo "done." From e83f4fca1e7ad18380078eb0f70434c865ddd315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 20 Feb 2025 16:17:50 +0100 Subject: [PATCH 14/27] feat(unified-storage): add some basic retry logic for the grpc client (#101001) --- go.mod | 2 +- pkg/storage/unified/client.go | 16 ++++++++++ pkg/storage/unified/client_retry.go | 46 +++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 pkg/storage/unified/client_retry.go diff --git a/go.mod b/go.mod index b0880354e19..8e30f7555b4 100644 --- a/go.mod +++ b/go.mod @@ -367,7 +367,7 @@ require ( github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grafana/sqlds/v4 v4.1.3 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/grafana-search-and-storage github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/hashicorp/consul/api v1.30.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index 305606bafef..396ce141c06 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "path/filepath" + "time" otgrpc "github.com/opentracing-contrib/go-grpc" "github.com/opentracing/opentracing-go" @@ -47,6 +48,7 @@ type Options struct { type clientMetrics struct { requestDuration *prometheus.HistogramVec + requestRetries *prometheus.CounterVec } // This adds a UnifiedStorage client into the wire dependency tree @@ -171,11 +173,25 @@ func GrpcConn(address string, reg prometheus.Registerer) (*grpc.ClientConn, erro Help: "Time spent executing requests to the resource server.", Buckets: prometheus.ExponentialBuckets(0.008, 4, 7), }, []string{"operation", "status_code"}), + requestRetries: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ + Name: "resource_server_client_request_retries_total", + Help: "Total number of retries for requests to the resource server.", + }, []string{"operation"}), } // Report gRPC status code errors as labels. unary, stream := instrument(metrics.requestDuration, middleware.ReportGRPCStatusOption) + // Add middleware to retry on transient connection issues. Note that + // we do not implement it for streams, as we don't currently use streams. + retryCfg := retryConfig{ + Max: 3, + Backoff: time.Second, + BackoffJitter: 0.5, + } + unary = append(unary, unaryRetryInterceptor(retryCfg)) + unary = append(unary, unaryRetryInstrument(metrics.requestRetries)) + cfg := grpcclient.Config{} // Set the defaults that are normally set by Config.RegisterFlags. flagext.DefaultValues(&cfg) diff --git a/pkg/storage/unified/client_retry.go b/pkg/storage/unified/client_retry.go new file mode 100644 index 00000000000..47b899e4198 --- /dev/null +++ b/pkg/storage/unified/client_retry.go @@ -0,0 +1,46 @@ +package unified + +import ( + "context" + "strconv" + "time" + + grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" + "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" +) + +type retryConfig struct { + Max uint + Backoff time.Duration + BackoffJitter float64 +} + +// unaryRetryInterceptor creates an interceptor to perform retries for unary methods. +// +// Note: Retry codes are the same as the default codes. +// +// From go-grpc-middleware/interceptors/retry/options.go: +// `ResourceExhausted` means that the user quota, e.g. per-RPC limits, have been reached. +// `Unavailable` means that system is currently unavailable and the client should retry again. +func unaryRetryInterceptor(cfg retryConfig) grpc.UnaryClientInterceptor { + return grpc_retry.UnaryClientInterceptor( + grpc_retry.WithMax(cfg.Max), + grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(cfg.Backoff, cfg.BackoffJitter)), + grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable), + ) +} + +// unaryRetryInstrument creates an interceptor to count and log retry attempts. +func unaryRetryInstrument(metric *prometheus.CounterVec) grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, resp interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + // We can tell if a call is a retry by checking the retry attempt metadata. + attempt, err := strconv.Atoi(metautils.ExtractOutgoing(ctx).Get(grpc_retry.AttemptMetadataKey)) + if err == nil && attempt > 0 { + metric.WithLabelValues(method).Inc() + } + return invoker(ctx, method, req, resp, cc, opts...) + } +} From 53bc06ef41e59269371638c8e2e43a312c937ef8 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Thu, 20 Feb 2025 15:36:12 +0000 Subject: [PATCH 15/27] LBAC: Remove reference for feature toggle (#101074) missed reference for feature toggle --- .../teamlbac/configure-teamlbac-for-loki/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/index.md b/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/index.md index 0e5875eb26c..18a21b24ee8 100644 --- a/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/index.md +++ b/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-loki/index.md @@ -19,8 +19,6 @@ You cannot configure LBAC rules for Grafana-provisioned data sources from the UI ## Before you begin -To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you. - - Be sure that you have the permission setup to create a Loki tenant in Grafana Cloud - Be sure that you have admin data source permissions for Grafana. From 6e5de80ef295c4ab81b85fca31eb1b652f8ad035 Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Thu, 20 Feb 2025 16:43:26 +0100 Subject: [PATCH 16/27] unistore: refactor the storage backend test suite (#101061) * refactor the storage backend test suite * move testing * pass a newBackendFunc --- .../unified/sql/test/integration_test.go | 476 +--------------- .../unified/testing/storage_backend.go | 516 ++++++++++++++++++ 2 files changed, 528 insertions(+), 464 deletions(-) create mode 100644 pkg/storage/unified/testing/storage_backend.go diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index c47a2700f1f..92c644d48aa 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -8,12 +8,10 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" "github.com/grafana/dskit/services" - "github.com/grafana/grafana/pkg/apimachinery/utils" infraDB "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -22,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" + unitest "github.com/grafana/grafana/pkg/storage/unified/testing" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util/testutil" ) @@ -30,451 +29,22 @@ func TestMain(m *testing.M) { testsuite.Run(m) } -func newServer(t *testing.T, cfg *setting.Cfg) (sql.Backend, resource.ResourceServer) { - t.Helper() - if cfg == nil { - cfg = setting.NewCfg() - } - - dbstore := infraDB.InitTestDB(t) - - eDB, err := dbimpl.ProvideResourceDB(dbstore, cfg, nil) - require.NoError(t, err) - require.NotNil(t, eDB) - - ret, err := sql.NewBackend(sql.BackendOptions{ - DBProvider: eDB, - }) - require.NoError(t, err) - require.NotNil(t, ret) - - err = ret.Init(testutil.NewDefaultTestContext(t)) - require.NoError(t, err) - - server, err := resource.NewResourceServer(resource.ResourceServerOptions{ - Backend: ret, - Diagnostics: ret, - Lifecycle: ret, - }) - require.NoError(t, err) - require.NotNil(t, server) - - return ret, server -} - -func TestIntegrationBackendHappyPath(t *testing.T) { - // if infraDB.IsTestDbSQLite() { - // t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") - // } - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := types.WithAuthInfo(context.Background(), authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{ - Claims: jwt.Claims{ - Subject: "testuser", - }, - Rest: authn.AccessTokenClaims{}, - })) - - backend, server := newServer(t, nil) - - stream, err := backend.WatchWriteEvents(context.Background()) // Using a different context to avoid canceling the stream after the DefaultContextTimeout - require.NoError(t, err) - var rv1, rv2, rv3, rv4, rv5 int64 - - t.Run("Add 3 resources", func(t *testing.T) { - rv1, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) +// TestStorageBackend is a test for the StorageBackend interface. +func TestIntegrationSQLStorageBackend(t *testing.T) { + unitest.RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { + dbstore := infraDB.InitTestDB(t) + eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) require.NoError(t, err) - require.Greater(t, rv1, int64(0)) + require.NotNil(t, eDB) - rv2, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) - require.NoError(t, err) - require.Greater(t, rv2, rv1) - - rv3, err = writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) - require.NoError(t, err) - require.Greater(t, rv3, rv2) - - stats, err := backend.GetResourceStats(ctx, "", 0) - require.NoError(t, err) - require.Len(t, stats, 1) - require.Equal(t, int64(3), stats[0].Count) - require.Equal(t, rv3, stats[0].ResourceVersion) - }) - - t.Run("Update item2", func(t *testing.T) { - rv4, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) - require.NoError(t, err) - require.Greater(t, rv4, rv3) - }) - - t.Run("Delete item1", func(t *testing.T) { - rv5, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_DELETED) - require.NoError(t, err) - require.Greater(t, rv5, rv4) - }) - - t.Run("Read latest item 2", func(t *testing.T) { - resp := backend.ReadResource(ctx, &resource.ReadRequest{Key: resourceKey("item2")}) - require.Nil(t, resp.Error) - require.Equal(t, rv4, resp.ResourceVersion) - require.Equal(t, "item2 MODIFIED", string(resp.Value)) - require.Equal(t, "folderuid", resp.Folder) - }) - - t.Run("Read early version of item2", func(t *testing.T) { - resp := backend.ReadResource(ctx, &resource.ReadRequest{ - Key: resourceKey("item2"), - ResourceVersion: rv3, // item2 was created at rv2 and updated at rv4 - }) - require.Nil(t, resp.Error) - require.Equal(t, rv2, resp.ResourceVersion) - require.Equal(t, "item2 ADDED", string(resp.Value)) - }) - - t.Run("PrepareList latest", func(t *testing.T) { - resp, err := server.List(ctx, &resource.ListRequest{ - Options: &resource.ListOptions{ - Key: &resource.ResourceKey{ - Namespace: "namespace", - Group: "group", - Resource: "resource", - }, - }, + backend, err := sql.NewBackend(sql.BackendOptions{ + DBProvider: eDB, }) require.NoError(t, err) - require.Nil(t, resp.Error) - require.Len(t, resp.Items, 2) - require.Equal(t, "item2 MODIFIED", string(resp.Items[0].Value)) - require.Equal(t, "item3 ADDED", string(resp.Items[1].Value)) - require.Equal(t, rv5, resp.ResourceVersion) - }) - - t.Run("Watch events", func(t *testing.T) { - event := <-stream - require.Equal(t, "item1", event.Key.Name) - require.Equal(t, rv1, event.ResourceVersion) - require.Equal(t, resource.WatchEvent_ADDED, event.Type) - require.Equal(t, "folderuid", event.Folder) - - event = <-stream - require.Equal(t, "item2", event.Key.Name) - require.Equal(t, rv2, event.ResourceVersion) - require.Equal(t, resource.WatchEvent_ADDED, event.Type) - require.Equal(t, "folderuid", event.Folder) - - event = <-stream - require.Equal(t, "item3", event.Key.Name) - require.Equal(t, rv3, event.ResourceVersion) - require.Equal(t, resource.WatchEvent_ADDED, event.Type) - - event = <-stream - require.Equal(t, "item2", event.Key.Name) - require.Equal(t, rv4, event.ResourceVersion) - require.Equal(t, resource.WatchEvent_MODIFIED, event.Type) - - event = <-stream - require.Equal(t, "item1", event.Key.Name) - require.Equal(t, rv5, event.ResourceVersion) - require.Equal(t, resource.WatchEvent_DELETED, event.Type) - }) -} - -func TestIntegrationBackendWatchWriteEventsFromLastest(t *testing.T) { - if infraDB.IsTestDbSQLite() { - t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") - } - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) - backend, _ := newServer(t, nil) - - // Create a few resources before initing the watch - _, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) - require.NoError(t, err) - - // Start the watch - stream, err := backend.WatchWriteEvents(ctx) - require.NoError(t, err) - - // Create one more event - _, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) - require.NoError(t, err) - require.Equal(t, "item2", (<-stream).Key.Name) -} - -func TestIntegrationBackendList(t *testing.T) { - if infraDB.IsTestDbSQLite() { - t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") - } - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) - backend, server := newServer(t, nil) - - // Create a few resources before starting the watch - rv1, _ := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) - require.Greater(t, rv1, int64(0)) - rv2, _ := writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) // rv=2 - will be modified at rv=6 - require.Greater(t, rv2, rv1) - rv3, _ := writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) // rv=3 - will be deleted at rv=7 - require.Greater(t, rv3, rv2) - rv4, _ := writeEvent(ctx, backend, "item4", resource.WatchEvent_ADDED) - require.Greater(t, rv4, rv3) - rv5, _ := writeEvent(ctx, backend, "item5", resource.WatchEvent_ADDED) - require.Greater(t, rv5, rv4) - rv6, _ := writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) - require.Greater(t, rv6, rv5) - rv7, _ := writeEvent(ctx, backend, "item3", resource.WatchEvent_DELETED) - require.Greater(t, rv7, rv6) - rv8, _ := writeEvent(ctx, backend, "item6", resource.WatchEvent_ADDED) - require.Greater(t, rv8, rv7) - - t.Run("fetch all latest", func(t *testing.T) { - res, err := server.List(ctx, &resource.ListRequest{ - Options: &resource.ListOptions{ - Key: &resource.ResourceKey{ - Group: "group", - Resource: "resource", - }, - }, - }) + require.NotNil(t, backend) + err = backend.Init(testutil.NewDefaultTestContext(t)) require.NoError(t, err) - require.Nil(t, res.Error) - require.Len(t, res.Items, 5) - // should be sorted by key ASC - require.Equal(t, "item1 ADDED", string(res.Items[0].Value)) - require.Equal(t, "item2 MODIFIED", string(res.Items[1].Value)) - require.Equal(t, "item4 ADDED", string(res.Items[2].Value)) - require.Equal(t, "item5 ADDED", string(res.Items[3].Value)) - require.Equal(t, "item6 ADDED", string(res.Items[4].Value)) - - require.Empty(t, res.NextPageToken) - }) - - t.Run("list latest first page ", func(t *testing.T) { - res, err := server.List(ctx, &resource.ListRequest{ - Limit: 3, - Options: &resource.ListOptions{ - Key: &resource.ResourceKey{ - Group: "group", - Resource: "resource", - }, - }, - }) - require.NoError(t, err) - require.Nil(t, res.Error) - require.Len(t, res.Items, 3) - continueToken, err := sql.GetContinueToken(res.NextPageToken) - require.NoError(t, err) - require.Equal(t, "item1 ADDED", string(res.Items[0].Value)) - require.Equal(t, "item2 MODIFIED", string(res.Items[1].Value)) - require.Equal(t, "item4 ADDED", string(res.Items[2].Value)) - require.Equal(t, rv8, continueToken.ResourceVersion) - }) - - t.Run("list at revision", func(t *testing.T) { - res, err := server.List(ctx, &resource.ListRequest{ - ResourceVersion: rv4, - Options: &resource.ListOptions{ - Key: &resource.ResourceKey{ - Group: "group", - Resource: "resource", - }, - }, - }) - require.NoError(t, err) - require.Nil(t, res.Error) - require.Len(t, res.Items, 4) - require.Equal(t, "item1 ADDED", string(res.Items[0].Value)) - require.Equal(t, "item2 ADDED", string(res.Items[1].Value)) - require.Equal(t, "item3 ADDED", string(res.Items[2].Value)) - require.Equal(t, "item4 ADDED", string(res.Items[3].Value)) - require.Empty(t, res.NextPageToken) - }) - - t.Run("fetch first page at revision with limit", func(t *testing.T) { - res, err := server.List(ctx, &resource.ListRequest{ - Limit: 3, - ResourceVersion: rv7, - Options: &resource.ListOptions{ - Key: &resource.ResourceKey{ - Group: "group", - Resource: "resource", - }, - }, - }) - require.NoError(t, err) - require.NoError(t, err) - require.Nil(t, res.Error) - require.Len(t, res.Items, 3) - t.Log(res.Items) - require.Equal(t, "item1 ADDED", string(res.Items[0].Value)) - require.Equal(t, "item2 MODIFIED", string(res.Items[1].Value)) - require.Equal(t, "item4 ADDED", string(res.Items[2].Value)) - - continueToken, err := sql.GetContinueToken(res.NextPageToken) - require.NoError(t, err) - require.Equal(t, rv7, continueToken.ResourceVersion) - }) - - t.Run("fetch second page at revision", func(t *testing.T) { - continueToken := &sql.ContinueToken{ - ResourceVersion: rv8, - StartOffset: 2, - } - res, err := server.List(ctx, &resource.ListRequest{ - NextPageToken: continueToken.String(), - Limit: 2, - Options: &resource.ListOptions{ - Key: &resource.ResourceKey{ - Group: "group", - Resource: "resource", - }, - }, - }) - require.NoError(t, err) - require.Nil(t, res.Error) - require.Len(t, res.Items, 2) - t.Log(res.Items) - require.Equal(t, "item4 ADDED", string(res.Items[0].Value)) - require.Equal(t, "item5 ADDED", string(res.Items[1].Value)) - - continueToken, err = sql.GetContinueToken(res.NextPageToken) - require.NoError(t, err) - require.Equal(t, rv8, continueToken.ResourceVersion) - require.Equal(t, int64(4), continueToken.StartOffset) - }) - - // add 5 events for item1 - should be saved to history - rvHistory1, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) - require.NoError(t, err) - require.Greater(t, rvHistory1, rv1) - rvHistory2, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) - require.NoError(t, err) - require.Greater(t, rvHistory2, rvHistory1) - rvHistory3, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) - require.NoError(t, err) - require.Greater(t, rvHistory3, rvHistory2) - rvHistory4, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) - require.NoError(t, err) - require.Greater(t, rvHistory4, rvHistory3) - rvHistory5, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) - require.NoError(t, err) - require.Greater(t, rvHistory5, rvHistory4) - - t.Run("fetch first history page at revision with limit", func(t *testing.T) { - res, err := server.List(ctx, &resource.ListRequest{ - Limit: 3, - Source: resource.ListRequest_HISTORY, - Options: &resource.ListOptions{ - Key: &resource.ResourceKey{ - Namespace: "namespace", - Group: "group", - Resource: "resource", - Name: "item1", - }, - }, - }) - require.NoError(t, err) - require.NoError(t, err) - require.Nil(t, res.Error) - require.Len(t, res.Items, 3) - t.Log(res.Items) - // should be in desc order, so the newest RVs are returned first - require.Equal(t, "item1 MODIFIED", string(res.Items[0].Value)) - require.Equal(t, rvHistory5, res.Items[0].ResourceVersion) - require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) - require.Equal(t, rvHistory4, res.Items[1].ResourceVersion) - require.Equal(t, "item1 MODIFIED", string(res.Items[2].Value)) - require.Equal(t, rvHistory3, res.Items[2].ResourceVersion) - - continueToken, err := sql.GetContinueToken(res.NextPageToken) - require.NoError(t, err) - // should return the furthest back RV as the next page token - require.Equal(t, rvHistory3, continueToken.ResourceVersion) - }) - - t.Run("fetch second page of history at revision", func(t *testing.T) { - continueToken := &sql.ContinueToken{ - ResourceVersion: rvHistory3, - StartOffset: 2, - } - res, err := server.List(ctx, &resource.ListRequest{ - NextPageToken: continueToken.String(), - Limit: 2, - Source: resource.ListRequest_HISTORY, - Options: &resource.ListOptions{ - Key: &resource.ResourceKey{ - Namespace: "namespace", - Group: "group", - Resource: "resource", - Name: "item1", - }, - }, - }) - require.NoError(t, err) - require.Nil(t, res.Error) - require.Len(t, res.Items, 2) - t.Log(res.Items) - require.Equal(t, "item1 MODIFIED", string(res.Items[0].Value)) - require.Equal(t, rvHistory2, res.Items[0].ResourceVersion) - require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) - require.Equal(t, rvHistory1, res.Items[1].ResourceVersion) - }) -} - -func TestIntegrationBlobSupport(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) - backend, server := newServer(t, nil) - store, ok := backend.(resource.BlobSupport) - require.True(t, ok) - - t.Run("put and fetch blob", func(t *testing.T) { - key := &resource.ResourceKey{ - Namespace: "ns", - Group: "g", - Resource: "r", - Name: "n", - } - - b1, err := server.PutBlob(ctx, &resource.PutBlobRequest{ - Resource: key, - Method: resource.PutBlobRequest_GRPC, - ContentType: "plain/text", - Value: []byte("hello 11111"), - }) - require.NoError(t, err) - require.Nil(t, b1.Error) - require.Equal(t, "c894ae57bd227b8f8c63f38a2ddf458b", b1.Hash) - - b2, err := server.PutBlob(ctx, &resource.PutBlobRequest{ - Resource: key, - Method: resource.PutBlobRequest_GRPC, - ContentType: "plain/text", - Value: []byte("hello 22222"), // the most recent - }) - require.NoError(t, err) - require.Nil(t, b2.Error) - require.Equal(t, "b0da48de4ff92e0ad0d836de4d746937", b2.Hash) - - // Check that we can still access both values - found, err := store.GetResourceBlob(ctx, key, &utils.BlobInfo{UID: b1.Uid}, true) - require.NoError(t, err) - require.Equal(t, []byte("hello 11111"), found.Value) - - found, err = store.GetResourceBlob(ctx, key, &utils.BlobInfo{UID: b2.Uid}, true) - require.NoError(t, err) - require.Equal(t, []byte("hello 22222"), found.Value) + return backend }) } @@ -549,28 +119,6 @@ func TestClientServer(t *testing.T) { }) } -func writeEvent(ctx context.Context, store sql.Backend, name string, action resource.WatchEvent_Type) (int64, error) { - res := &unstructured.Unstructured{ - Object: map[string]any{}, - } - meta, err := utils.MetaAccessor(res) - if err != nil { - return 0, err - } - meta.SetFolder("folderuid") - return store.WriteEvent(ctx, resource.WriteEvent{ - Type: action, - Value: []byte(name + " " + resource.WatchEvent_Type_name[int32(action)]), - Key: &resource.ResourceKey{ - Namespace: "namespace", - Group: "group", - Resource: "resource", - Name: name, - }, - Object: meta, - }) -} - func resourceKey(name string) *resource.ResourceKey { return &resource.ResourceKey{ Namespace: "namespace", diff --git a/pkg/storage/unified/testing/storage_backend.go b/pkg/storage/unified/testing/storage_backend.go new file mode 100644 index 00000000000..25a422aaede --- /dev/null +++ b/pkg/storage/unified/testing/storage_backend.go @@ -0,0 +1,516 @@ +package test + +import ( + "context" + "testing" + "time" + + "github.com/go-jose/go-jose/v3/jwt" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/authlib/authn" + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/utils" + infraDB "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/sql" + "github.com/grafana/grafana/pkg/util/testutil" +) + +type NewBackendFunc func(ctx context.Context) resource.StorageBackend + +// TestCase defines a test case for the storage backend +type TestCase struct { + name string + fn func(*testing.T, resource.StorageBackend) +} + +// StorageBackendTestSuite defines the test suite for storage backend +type StorageBackendTestSuite struct { + newBackend NewBackendFunc + cases []TestCase +} + +// NewStorageBackendTestSuite creates a new test suite +func NewStorageBackendTestSuite(newBackend NewBackendFunc) *StorageBackendTestSuite { + return &StorageBackendTestSuite{ + newBackend: newBackend, + cases: []TestCase{ + {"happy path", runTestIntegrationBackendHappyPath}, + {"watch write events from latest", runTestIntegrationBackendWatchWriteEventsFromLastest}, + {"list", runTestIntegrationBackendList}, + {"blob support", runTestIntegrationBlobSupport}, + }, + } +} + +// Run executes all test cases in the suite +func (s *StorageBackendTestSuite) Run(t *testing.T) { + for _, tc := range s.cases { + t.Run(tc.name, func(t *testing.T) { + tc.fn(t, s.newBackend(context.Background())) + }) + } +} + +// RunStorageBackendTest runs the storage backend test suite +func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc) { + suite := NewStorageBackendTestSuite(newBackend) + suite.Run(t) +} + +func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBackend) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := types.WithAuthInfo(context.Background(), authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{ + Claims: jwt.Claims{ + Subject: "testuser", + }, + Rest: authn.AccessTokenClaims{}, + })) + + server := newServer(t, backend) + + stream, err := backend.WatchWriteEvents(context.Background()) // Using a different context to avoid canceling the stream after the DefaultContextTimeout + require.NoError(t, err) + var rv1, rv2, rv3, rv4, rv5 int64 + + t.Run("Add 3 resources", func(t *testing.T) { + rv1, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) + require.NoError(t, err) + require.Greater(t, rv1, int64(0)) + + rv2, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) + require.NoError(t, err) + require.Greater(t, rv2, rv1) + + rv3, err = writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) + require.NoError(t, err) + require.Greater(t, rv3, rv2) + + stats, err := backend.GetResourceStats(ctx, "", 0) + require.NoError(t, err) + require.Len(t, stats, 1) + require.Equal(t, int64(3), stats[0].Count) + require.Equal(t, rv3, stats[0].ResourceVersion) + }) + + t.Run("Update item2", func(t *testing.T) { + rv4, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rv4, rv3) + }) + + t.Run("Delete item1", func(t *testing.T) { + rv5, err = writeEvent(ctx, backend, "item1", resource.WatchEvent_DELETED) + require.NoError(t, err) + require.Greater(t, rv5, rv4) + }) + + t.Run("Read latest item 2", func(t *testing.T) { + resp := backend.ReadResource(ctx, &resource.ReadRequest{Key: resourceKey("item2")}) + require.Nil(t, resp.Error) + require.Equal(t, rv4, resp.ResourceVersion) + require.Equal(t, "item2 MODIFIED", string(resp.Value)) + require.Equal(t, "folderuid", resp.Folder) + }) + + t.Run("Read early version of item2", func(t *testing.T) { + resp := backend.ReadResource(ctx, &resource.ReadRequest{ + Key: resourceKey("item2"), + ResourceVersion: rv3, // item2 was created at rv2 and updated at rv4 + }) + require.Nil(t, resp.Error) + require.Equal(t, rv2, resp.ResourceVersion) + require.Equal(t, "item2 ADDED", string(resp.Value)) + }) + + t.Run("PrepareList latest", func(t *testing.T) { + resp, err := server.List(ctx, &resource.ListRequest{ + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "namespace", + Group: "group", + Resource: "resource", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, resp.Error) + require.Len(t, resp.Items, 2) + require.Equal(t, "item2 MODIFIED", string(resp.Items[0].Value)) + require.Equal(t, "item3 ADDED", string(resp.Items[1].Value)) + require.Equal(t, rv5, resp.ResourceVersion) + }) + + t.Run("Watch events", func(t *testing.T) { + event := <-stream + require.Equal(t, "item1", event.Key.Name) + require.Equal(t, rv1, event.ResourceVersion) + require.Equal(t, resource.WatchEvent_ADDED, event.Type) + require.Equal(t, "folderuid", event.Folder) + + event = <-stream + require.Equal(t, "item2", event.Key.Name) + require.Equal(t, rv2, event.ResourceVersion) + require.Equal(t, resource.WatchEvent_ADDED, event.Type) + require.Equal(t, "folderuid", event.Folder) + + event = <-stream + require.Equal(t, "item3", event.Key.Name) + require.Equal(t, rv3, event.ResourceVersion) + require.Equal(t, resource.WatchEvent_ADDED, event.Type) + + event = <-stream + require.Equal(t, "item2", event.Key.Name) + require.Equal(t, rv4, event.ResourceVersion) + require.Equal(t, resource.WatchEvent_MODIFIED, event.Type) + + event = <-stream + require.Equal(t, "item1", event.Key.Name) + require.Equal(t, rv5, event.ResourceVersion) + require.Equal(t, resource.WatchEvent_DELETED, event.Type) + }) +} + +func runTestIntegrationBackendWatchWriteEventsFromLastest(t *testing.T, backend resource.StorageBackend) { + if infraDB.IsTestDbSQLite() { + t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + } + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) + + // Create a few resources before initing the watch + _, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) + require.NoError(t, err) + + // Start the watch + stream, err := backend.WatchWriteEvents(ctx) + require.NoError(t, err) + + // Create one more event + _, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) + require.NoError(t, err) + require.Equal(t, "item2", (<-stream).Key.Name) +} + +func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend) { + if infraDB.IsTestDbSQLite() { + t.Skip("TODO: test blocking, skipping to unblock Enterprise until we fix this") + } + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) + server := newServer(t, backend) + + // Create a few resources before starting the watch + rv1, _ := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED) + require.Greater(t, rv1, int64(0)) + rv2, _ := writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) // rv=2 - will be modified at rv=6 + require.Greater(t, rv2, rv1) + rv3, _ := writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED) // rv=3 - will be deleted at rv=7 + require.Greater(t, rv3, rv2) + rv4, _ := writeEvent(ctx, backend, "item4", resource.WatchEvent_ADDED) + require.Greater(t, rv4, rv3) + rv5, _ := writeEvent(ctx, backend, "item5", resource.WatchEvent_ADDED) + require.Greater(t, rv5, rv4) + rv6, _ := writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED) + require.Greater(t, rv6, rv5) + rv7, _ := writeEvent(ctx, backend, "item3", resource.WatchEvent_DELETED) + require.Greater(t, rv7, rv6) + rv8, _ := writeEvent(ctx, backend, "item6", resource.WatchEvent_ADDED) + require.Greater(t, rv8, rv7) + + t.Run("fetch all latest", func(t *testing.T) { + res, err := server.List(ctx, &resource.ListRequest{ + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Group: "group", + Resource: "resource", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 5) + // should be sorted by key ASC + require.Equal(t, "item1 ADDED", string(res.Items[0].Value)) + require.Equal(t, "item2 MODIFIED", string(res.Items[1].Value)) + require.Equal(t, "item4 ADDED", string(res.Items[2].Value)) + require.Equal(t, "item5 ADDED", string(res.Items[3].Value)) + require.Equal(t, "item6 ADDED", string(res.Items[4].Value)) + + require.Empty(t, res.NextPageToken) + }) + + t.Run("list latest first page ", func(t *testing.T) { + res, err := server.List(ctx, &resource.ListRequest{ + Limit: 3, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Group: "group", + Resource: "resource", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 3) + continueToken, err := sql.GetContinueToken(res.NextPageToken) + require.NoError(t, err) + require.Equal(t, "item1 ADDED", string(res.Items[0].Value)) + require.Equal(t, "item2 MODIFIED", string(res.Items[1].Value)) + require.Equal(t, "item4 ADDED", string(res.Items[2].Value)) + require.Equal(t, rv8, continueToken.ResourceVersion) + }) + + t.Run("list at revision", func(t *testing.T) { + res, err := server.List(ctx, &resource.ListRequest{ + ResourceVersion: rv4, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Group: "group", + Resource: "resource", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 4) + require.Equal(t, "item1 ADDED", string(res.Items[0].Value)) + require.Equal(t, "item2 ADDED", string(res.Items[1].Value)) + require.Equal(t, "item3 ADDED", string(res.Items[2].Value)) + require.Equal(t, "item4 ADDED", string(res.Items[3].Value)) + require.Empty(t, res.NextPageToken) + }) + + t.Run("fetch first page at revision with limit", func(t *testing.T) { + res, err := server.List(ctx, &resource.ListRequest{ + Limit: 3, + ResourceVersion: rv7, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Group: "group", + Resource: "resource", + }, + }, + }) + require.NoError(t, err) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 3) + t.Log(res.Items) + require.Equal(t, "item1 ADDED", string(res.Items[0].Value)) + require.Equal(t, "item2 MODIFIED", string(res.Items[1].Value)) + require.Equal(t, "item4 ADDED", string(res.Items[2].Value)) + + continueToken, err := sql.GetContinueToken(res.NextPageToken) + require.NoError(t, err) + require.Equal(t, rv7, continueToken.ResourceVersion) + }) + + t.Run("fetch second page at revision", func(t *testing.T) { + continueToken := &sql.ContinueToken{ + ResourceVersion: rv8, + StartOffset: 2, + } + res, err := server.List(ctx, &resource.ListRequest{ + NextPageToken: continueToken.String(), + Limit: 2, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Group: "group", + Resource: "resource", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 2) + t.Log(res.Items) + require.Equal(t, "item4 ADDED", string(res.Items[0].Value)) + require.Equal(t, "item5 ADDED", string(res.Items[1].Value)) + + continueToken, err = sql.GetContinueToken(res.NextPageToken) + require.NoError(t, err) + require.Equal(t, rv8, continueToken.ResourceVersion) + require.Equal(t, int64(4), continueToken.StartOffset) + }) + + // add 5 events for item1 - should be saved to history + rvHistory1, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory1, rv1) + rvHistory2, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory2, rvHistory1) + rvHistory3, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory3, rvHistory2) + rvHistory4, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory4, rvHistory3) + rvHistory5, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_MODIFIED) + require.NoError(t, err) + require.Greater(t, rvHistory5, rvHistory4) + + t.Run("fetch first history page at revision with limit", func(t *testing.T) { + res, err := server.List(ctx, &resource.ListRequest{ + Limit: 3, + Source: resource.ListRequest_HISTORY, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "namespace", + Group: "group", + Resource: "resource", + Name: "item1", + }, + }, + }) + require.NoError(t, err) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 3) + t.Log(res.Items) + // should be in desc order, so the newest RVs are returned first + require.Equal(t, "item1 MODIFIED", string(res.Items[0].Value)) + require.Equal(t, rvHistory5, res.Items[0].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) + require.Equal(t, rvHistory4, res.Items[1].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(res.Items[2].Value)) + require.Equal(t, rvHistory3, res.Items[2].ResourceVersion) + + continueToken, err := sql.GetContinueToken(res.NextPageToken) + require.NoError(t, err) + // should return the furthest back RV as the next page token + require.Equal(t, rvHistory3, continueToken.ResourceVersion) + }) + + t.Run("fetch second page of history at revision", func(t *testing.T) { + continueToken := &sql.ContinueToken{ + ResourceVersion: rvHistory3, + StartOffset: 2, + } + res, err := server.List(ctx, &resource.ListRequest{ + NextPageToken: continueToken.String(), + Limit: 2, + Source: resource.ListRequest_HISTORY, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: "namespace", + Group: "group", + Resource: "resource", + Name: "item1", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 2) + t.Log(res.Items) + require.Equal(t, "item1 MODIFIED", string(res.Items[0].Value)) + require.Equal(t, rvHistory2, res.Items[0].ResourceVersion) + require.Equal(t, "item1 MODIFIED", string(res.Items[1].Value)) + require.Equal(t, rvHistory1, res.Items[1].ResourceVersion) + }) +} + +func runTestIntegrationBlobSupport(t *testing.T, backend resource.StorageBackend) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) + server := newServer(t, backend) + store, ok := backend.(resource.BlobSupport) + require.True(t, ok) + + t.Run("put and fetch blob", func(t *testing.T) { + key := &resource.ResourceKey{ + Namespace: "ns", + Group: "g", + Resource: "r", + Name: "n", + } + + b1, err := server.PutBlob(ctx, &resource.PutBlobRequest{ + Resource: key, + Method: resource.PutBlobRequest_GRPC, + ContentType: "plain/text", + Value: []byte("hello 11111"), + }) + require.NoError(t, err) + require.Nil(t, b1.Error) + require.Equal(t, "c894ae57bd227b8f8c63f38a2ddf458b", b1.Hash) + + b2, err := server.PutBlob(ctx, &resource.PutBlobRequest{ + Resource: key, + Method: resource.PutBlobRequest_GRPC, + ContentType: "plain/text", + Value: []byte("hello 22222"), // the most recent + }) + require.NoError(t, err) + require.Nil(t, b2.Error) + require.Equal(t, "b0da48de4ff92e0ad0d836de4d746937", b2.Hash) + + // Check that we can still access both values + found, err := store.GetResourceBlob(ctx, key, &utils.BlobInfo{UID: b1.Uid}, true) + require.NoError(t, err) + require.Equal(t, []byte("hello 11111"), found.Value) + + found, err = store.GetResourceBlob(ctx, key, &utils.BlobInfo{UID: b2.Uid}, true) + require.NoError(t, err) + require.Equal(t, []byte("hello 22222"), found.Value) + }) +} + +func writeEvent(ctx context.Context, store resource.StorageBackend, name string, action resource.WatchEvent_Type) (int64, error) { + res := &unstructured.Unstructured{ + Object: map[string]any{}, + } + meta, err := utils.MetaAccessor(res) + if err != nil { + return 0, err + } + meta.SetFolder("folderuid") + return store.WriteEvent(ctx, resource.WriteEvent{ + Type: action, + Value: []byte(name + " " + resource.WatchEvent_Type_name[int32(action)]), + Key: &resource.ResourceKey{ + Namespace: "namespace", + Group: "group", + Resource: "resource", + Name: name, + }, + Object: meta, + }) +} + +func resourceKey(name string) *resource.ResourceKey { + return &resource.ResourceKey{ + Namespace: "namespace", + Group: "group", + Resource: "resource", + Name: name, + } +} + +func newServer(t *testing.T, b resource.StorageBackend) resource.ResourceServer { + t.Helper() + + server, err := resource.NewResourceServer(resource.ResourceServerOptions{ + Backend: b, + }) + require.NoError(t, err) + require.NotNil(t, server) + + return server +} From b4c4b9abbdc82d0a6b1feb42a87806c4fedca728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Thu, 20 Feb 2025 17:34:25 +0100 Subject: [PATCH 17/27] Remove embedded WriteEvent from WrittenEvent struct, to avoid having unnecessary fields. (#101081) --- pkg/registry/apis/dashboard/legacy/storage.go | 6 +++++- pkg/storage/unified/resource/cdk_backend.go | 5 ++++- pkg/storage/unified/resource/event.go | 14 ++++++++++---- pkg/storage/unified/sql/backend.go | 18 ++++++++---------- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index 7064d1d9994..239bc77ef90 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -8,6 +8,7 @@ import ( "time" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/utils" dashboard "github.com/grafana/grafana/pkg/apis/dashboard" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -86,7 +87,10 @@ func (a *dashboardSqlAccess) WriteEvent(ctx context.Context, event resource.Writ if a.subscribers != nil { go func() { write := &resource.WrittenEvent{ - WriteEvent: event, + Type: event.Type, + Key: event.Key, + PreviousRV: event.PreviousRV, + Value: event.Value, Timestamp: time.Now().UnixMilli(), ResourceVersion: rv, diff --git a/pkg/storage/unified/resource/cdk_backend.go b/pkg/storage/unified/resource/cdk_backend.go index eefba09d78f..19a7385a3f5 100644 --- a/pkg/storage/unified/resource/cdk_backend.go +++ b/pkg/storage/unified/resource/cdk_backend.go @@ -131,7 +131,10 @@ func (s *cdkBackend) WriteEvent(ctx context.Context, event WriteEvent) (rv int64 if s.stream != nil { go func() { write := &WrittenEvent{ - WriteEvent: event, + Type: event.Type, + Key: event.Key, + PreviousRV: event.PreviousRV, + Value: event.Value, Timestamp: time.Now().UnixMilli(), ResourceVersion: rv, } diff --git a/pkg/storage/unified/resource/event.go b/pkg/storage/unified/resource/event.go index 91c9225ee0c..fc408e23027 100644 --- a/pkg/storage/unified/resource/event.go +++ b/pkg/storage/unified/resource/event.go @@ -21,18 +21,24 @@ type WriteEvent struct { ObjectOld utils.GrafanaMetaAccessor } -// WriteEvents after they include a resource version +// WrittenEvent is a WriteEvent reported with a resource version. type WrittenEvent struct { - WriteEvent + Type WatchEvent_Type + Key *ResourceKey + PreviousRV int64 + + // The json payload (without resourceVersion) + Value []byte + // Metadata Folder string - // The resource version + // The resource version. ResourceVersion int64 // Timestamp when the event is created Timestamp int64 } -// A function to write events +// EventAppender is a function to write events. type EventAppender = func(context.Context, *WriteEvent) (int64, error) diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 777164b9e2b..18b95019429 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -851,17 +851,15 @@ func (b *backend) poll(ctx context.Context, grp string, res string, since int64, prevRV = new(int64) } stream <- &resource.WrittenEvent{ - WriteEvent: resource.WriteEvent{ - Value: rec.Value, - Key: &resource.ResourceKey{ - Namespace: rec.Key.Namespace, - Group: rec.Key.Group, - Resource: rec.Key.Resource, - Name: rec.Key.Name, - }, - Type: resource.WatchEvent_Type(rec.Action), - PreviousRV: *prevRV, + Value: rec.Value, + Key: &resource.ResourceKey{ + Namespace: rec.Key.Namespace, + Group: rec.Key.Group, + Resource: rec.Key.Resource, + Name: rec.Key.Name, }, + Type: resource.WatchEvent_Type(rec.Action), + PreviousRV: *prevRV, Folder: rec.Folder, ResourceVersion: rec.ResourceVersion, // Timestamp: , // TODO: add timestamp From c8d4ff28a45057eca282d901bdf535d7626c53df Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:47:08 -0300 Subject: [PATCH 18/27] fix(unified-storage): dashboards not persisting folder_id with unified storage (#100844) * fix dashboards not persisting folder_id with unified storage --- pkg/services/dashboards/database/database.go | 14 ++++++++++++++ .../dashboards/database/database_test.go | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 06c0c1a0550..33d9109257d 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -400,6 +400,20 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE userId = -1 } + // we don't save FolderID in kubernetes object when saving through k8s + // this block guarantees we save dashboards with folder_id and folder_uid in those cases + if !dash.IsFolder && dash.FolderUID != "" && dash.FolderID == 0 { // nolint:staticcheck + var existing dashboards.Dashboard + folderIdFound, err := sess.Where("uid=? AND org_id=?", dash.FolderUID, dash.OrgID).Get(&existing) + if err != nil { + return nil, err + } + + if folderIdFound { + dash.FolderID = existing.ID // nolint:staticcheck + } + } + if dash.ID > 0 { var existing dashboards.Dashboard dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existing) diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index ccb0aac838b..9849cc80691 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -378,6 +378,23 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.False(t, dashboard.Updated.IsZero()) }) + t.Run("Should populate FolderID if FolderUID is provided when creating a dashboard", func(t *testing.T) { + setup() + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": "folderId", + "tags": []interface{}{}, + }), + FolderUID: savedFolder.UID, + UserID: 100, + } + dashboard, err := dashboardStore.SaveDashboard(context.Background(), cmd) + require.NoError(t, err) + require.EqualValues(t, dashboard.FolderID, savedFolder.ID) //nolint:staticcheck + require.EqualValues(t, dashboard.FolderUID, savedFolder.UID) + }) + t.Run("Should be able to update dashboard by id and remove folderId", func(t *testing.T) { setup() cmd := dashboards.SaveDashboardCommand{ From 9de769318c3139eabc5bf9b8b68e1e9215557678 Mon Sep 17 00:00:00 2001 From: beejeebus Date: Thu, 20 Feb 2025 11:53:28 -0500 Subject: [PATCH 19/27] Add more errorsource attribution to InfluxDb datasource (#100969) This PR adds errorsource attribution to the influxql and flux query paths when the query model cannot be parsed, which is a user error. It also catches cases where the datasource configuration does not contain a scheme or host, and adds downstream attribution to those errors. Error handling on the influxql query path is updated to match 'all errors are per query, and stashed on the response object' pattern. Fixes https://github.com/grafana/oss-plugin-partnerships/issues/1250 --- pkg/tsdb/influxdb/flux/flux.go | 5 ++- pkg/tsdb/influxdb/influxql/influxql.go | 48 ++++++++++++++++++------ pkg/tsdb/influxdb/models/model_parser.go | 11 ++++-- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/pkg/tsdb/influxdb/flux/flux.go b/pkg/tsdb/influxdb/flux/flux.go index 18d2a6f725a..cab5c0d1067 100644 --- a/pkg/tsdb/influxdb/flux/flux.go +++ b/pkg/tsdb/influxdb/flux/flux.go @@ -31,7 +31,10 @@ func Query(ctx context.Context, dsInfo *models.DatasourceInfo, tsdbQuery backend for _, query := range tsdbQuery.Queries { qm, err := getQueryModel(query, timeRange, dsInfo) if err != nil { - tRes.Responses[query.RefID] = backend.DataResponse{Error: err} + tRes.Responses[query.RefID] = backend.DataResponse{ + Error: err, + ErrorSource: backend.ErrorSourceDownstream, + } continue } diff --git a/pkg/tsdb/influxdb/influxql/influxql.go b/pkg/tsdb/influxdb/influxql/influxql.go index e7de3267e6f..651df59e3c7 100644 --- a/pkg/tsdb/influxdb/influxql/influxql.go +++ b/pkg/tsdb/influxdb/influxql/influxql.go @@ -30,6 +30,7 @@ const ( var ( ErrInvalidHttpMode = errors.New("'httpMode' should be either 'GET' or 'POST'") + ErrInvalidUrl = errors.New("URL must contain scheme and host") glog = log.New("tsdb.influx_influxql") ) @@ -51,13 +52,17 @@ func Query(ctx context.Context, tracer trace.Tracer, dsInfo *models.DatasourceIn reqQuery := req.Queries[idx] query, err := models.QueryParse(reqQuery, logger) if err != nil { - return err + responseLock.Lock() + response.Responses[query.RefID] = backend.DataResponse{ + Error: err, + ErrorSource: backend.ErrorSourceDownstream, + } + responseLock.Unlock() + return nil } - rawQuery, err := query.Build(req) - if err != nil { - return err - } + // query.Build() unconditionally returns nil for error. + rawQuery, _ := query.Build(req) query.RefID = reqQuery.RefID query.RawQuery = rawQuery @@ -68,7 +73,13 @@ func Query(ctx context.Context, tracer trace.Tracer, dsInfo *models.DatasourceIn request, err := createRequest(ctx, logger, dsInfo, rawQuery, query.Policy) if err != nil { - return err + responseLock.Lock() + response.Responses[query.RefID] = backend.DataResponse{ + Error: err, + ErrorSource: backend.ErrorSourceDownstream, + } + responseLock.Unlock() + return nil } resp, err := execute(ctx, tracer, dsInfo, logger, query, request, features.IsEnabled(ctx, featuremgmt.FlagInfluxqlStreamingParser)) @@ -90,13 +101,15 @@ func Query(ctx context.Context, tracer trace.Tracer, dsInfo *models.DatasourceIn for _, reqQuery := range req.Queries { query, err := models.QueryParse(reqQuery, logger) if err != nil { - return &backend.QueryDataResponse{}, err + response.Responses[query.RefID] = backend.DataResponse{ + Error: err, + ErrorSource: backend.ErrorSourceDownstream, + } + continue } - rawQuery, err := query.Build(req) - if err != nil { - return &backend.QueryDataResponse{}, err - } + // query.Build() unconditionally returns nil for error. + rawQuery, _ := query.Build(req) query.RefID = reqQuery.RefID query.RawQuery = rawQuery @@ -107,7 +120,11 @@ func Query(ctx context.Context, tracer trace.Tracer, dsInfo *models.DatasourceIn request, err := createRequest(ctx, logger, dsInfo, rawQuery, query.Policy) if err != nil { - return &backend.QueryDataResponse{}, err + response.Responses[query.RefID] = backend.DataResponse{ + Error: err, + ErrorSource: backend.ErrorSourceDownstream, + } + continue } resp, err := execute(ctx, tracer, dsInfo, logger, query, request, features.IsEnabled(ctx, featuremgmt.FlagInfluxqlStreamingParser)) @@ -129,6 +146,13 @@ func createRequest(ctx context.Context, logger log.Logger, dsInfo *models.Dataso return nil, err } + // It's possible that the configuration is bad, and we'll have a URL + // without a scheme or host. This is valid from the PoV of the Go std + // library url.Parse(), but not for this data source. + if u.Host == "" || u.Scheme == "" { + return nil, ErrInvalidUrl + } + u.Path = path.Join(u.Path, "query") httpMode := dsInfo.HTTPMode diff --git a/pkg/tsdb/influxdb/models/model_parser.go b/pkg/tsdb/influxdb/models/model_parser.go index 011066b4ef9..90f8985fc75 100644 --- a/pkg/tsdb/influxdb/models/model_parser.go +++ b/pkg/tsdb/influxdb/models/model_parser.go @@ -1,6 +1,7 @@ package models import ( + "errors" "fmt" "strconv" "time" @@ -12,6 +13,10 @@ import ( "github.com/grafana/grafana/pkg/infra/log" ) +var ( + ErrInvalidQuery = errors.New("invalid InfluxDB query") +) + type InfluxdbQueryParser struct{} func QueryParse(query backend.DataQuery, logger log.Logger) (*Query, error) { @@ -33,17 +38,17 @@ func QueryParse(query backend.DataQuery, logger log.Logger) (*Query, error) { tags, err := parseTags(model) if err != nil { - return nil, err + return nil, errors.Join(ErrInvalidQuery, err) } groupBys, err := parseGroupBy(model) if err != nil { - return nil, err + return nil, errors.Join(ErrInvalidQuery, err) } selects, err := parseSelects(model) if err != nil { - return nil, err + return nil, errors.Join(ErrInvalidQuery, err) } interval := query.Interval From 95c05a1df971583e22df2965e4ed93279dab61fd Mon Sep 17 00:00:00 2001 From: Kat Yang <69819079+yangkb09@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:02:14 -0500 Subject: [PATCH 20/27] MetricsDrilldown: Update name of queryless metrics experience (#100675) * Chore: Update copy and comments to refer to Metrics Drilldown * chore: replace explore-metrics with metrics-drilldown in api.ts * chore: replace explore-metrics with metrics-drilldown in a pi.test.ts * chore: rename remaining instances in /public/app/features/trails * Update public/app/features/trails/Integrations/dashboardIntegration.ts Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> * Update public/app/features/trails/Integrations/dashboardIntegration.ts Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> --------- Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> --- packages/grafana-prometheus/src/index.ts | 2 +- .../trails/Integrations/dashboardIntegration.ts | 4 ++-- .../MetricSelect/AddToExplorationsButton.tsx | 2 +- .../trails/MetricSelect/MetricSelectScene.tsx | 2 +- public/app/features/trails/MetricSelect/api.ts | 4 ++-- .../trails/helpers/MetricDatasourceHelper.ts | 4 ++-- public/app/features/trails/otel/api.test.ts | 12 ++++++------ public/app/features/trails/otel/api.ts | 14 +++++++------- public/app/features/trails/otel/util.ts | 2 +- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/grafana-prometheus/src/index.ts b/packages/grafana-prometheus/src/index.ts index 4eb15995aae..2e7bc495150 100644 --- a/packages/grafana-prometheus/src/index.ts +++ b/packages/grafana-prometheus/src/index.ts @@ -86,7 +86,7 @@ export { } from './types'; export { PrometheusVariableSupport } from './variables'; -// For explore metrics +// For Metrics Drilldown export { default as PromQlLanguageProvider } from './language_provider'; export { getPrometheusTime } from './language_utils'; export { isValidLegacyName, utf8Support, wrapUtf8Filters } from './utf8_support'; diff --git a/public/app/features/trails/Integrations/dashboardIntegration.ts b/public/app/features/trails/Integrations/dashboardIntegration.ts index 4ff039f377b..dd52e73fe44 100644 --- a/public/app/features/trails/Integrations/dashboardIntegration.ts +++ b/public/app/features/trails/Integrations/dashboardIntegration.ts @@ -61,7 +61,7 @@ export async function addDataTrailPanelAction(dashboard: DashboardScene, panel: if (subMenu.length > 0) { items.push({ - text: 'Explore metrics', + text: 'Metrics drilldown', iconClassName: 'code-branch', subMenu: getUnique(subMenu), }); @@ -109,7 +109,7 @@ function createCommonEmbeddedTrailStateProps(item: QueryMetric, dashboard: Dashb const commonProps = { scene: embeddedTrail, - title: 'Explore metrics', + title: 'Metrics drilldown', }; return commonProps; diff --git a/public/app/features/trails/MetricSelect/AddToExplorationsButton.tsx b/public/app/features/trails/MetricSelect/AddToExplorationsButton.tsx index da46b7635d0..bbdd7110530 100644 --- a/public/app/features/trails/MetricSelect/AddToExplorationsButton.tsx +++ b/public/app/features/trails/MetricSelect/AddToExplorationsButton.tsx @@ -80,7 +80,7 @@ export class AddToExplorationButton extends SceneObjectBase i }, ...Array.from(rootGroup?.groups.keys() ?? []).map((g) => ({ label: `${g}_`, value: g })), ]} - className="explore-metrics-metric-prefix-select" + className="metrics-drilldown-metric-prefix-select" /> {!metric && hasOtelResources && ( diff --git a/public/app/features/trails/MetricSelect/api.ts b/public/app/features/trails/MetricSelect/api.ts index bb807ee31e9..c36cbfe3e2c 100644 --- a/public/app/features/trails/MetricSelect/api.ts +++ b/public/app/features/trails/MetricSelect/api.ts @@ -57,7 +57,7 @@ export async function getMetricNamesWithoutScopes( ...(limit ? { limit } : {}), }; - const response = await getBackendSrv().get(url, params, 'explore-metrics-names'); + const response = await getBackendSrv().get(url, params, 'metrics-drilldown-names'); if (limit && response.warnings?.includes(LIMIT_REACHED)) { return { ...response, limitReached: true, missingOtelTargets }; @@ -82,7 +82,7 @@ export async function getMetricNamesWithScopes( filters, '__name__', limit, - 'explore-metrics-names' + 'metrics-drilldown-names' ); if (jobs.length > 0 && instances.length > 0) { diff --git a/public/app/features/trails/helpers/MetricDatasourceHelper.ts b/public/app/features/trails/helpers/MetricDatasourceHelper.ts index 5a64feb9584..ff7d1fa62f2 100644 --- a/public/app/features/trails/helpers/MetricDatasourceHelper.ts +++ b/public/app/features/trails/helpers/MetricDatasourceHelper.ts @@ -154,7 +154,7 @@ export class MetricDatasourceHelper { } /** - * Used for additional filtering for adhoc vars labels in Explore metrics. + * Used for additional filtering for adhoc vars labels in Metrics Drilldown. * @param options * @returns */ @@ -170,7 +170,7 @@ export class MetricDatasourceHelper { } /** - * Used for additional filtering for adhoc vars label values in Explore metrics. + * Used for additional filtering for adhoc vars label values in Metrics Drilldown. * @param options * @returns */ diff --git a/public/app/features/trails/otel/api.test.ts b/public/app/features/trails/otel/api.test.ts index 97e5f4a01ae..9ed900fa8be 100644 --- a/public/app/features/trails/otel/api.test.ts +++ b/public/app/features/trails/otel/api.test.ts @@ -30,10 +30,10 @@ jest.mock('@grafana/runtime', () => ({ requestId?: string, options?: Partial ) => { - // explore-metrics-otel-resources + // metrics-drilldown-otel-resources if ( - requestId === 'explore-metrics-otel-check-total-count(target_info{}) by (job, instance)' || - requestId === 'explore-metrics-otel-check-total-count(metric) by (job, instance)' + requestId === 'metrics-drilldown-otel-check-total-count(target_info{}) by (job, instance)' || + requestId === 'metrics-drilldown-otel-check-total-count(metric) by (job, instance)' ) { return Promise.resolve({ data: { @@ -43,17 +43,17 @@ jest.mock('@grafana/runtime', () => ({ ], }, }); - } else if (requestId === 'explore-metrics-otel-resources-deployment-env') { + } else if (requestId === 'metrics-drilldown-otel-resources-deployment-env') { return Promise.resolve({ data: ['env1', 'env2'] }); } else if ( requestId === - 'explore-metrics-otel-resources-metric-job-instance-metric{job=~"job1|job2",instance=~"instance1|instance2"}' + 'metrics-drilldown-otel-resources-metric-job-instance-metric{job=~"job1|job2",instance=~"instance1|instance2"}' ) { // part of getFilteredResourceAttributes to get metric labels. We prioritize metric labels over resource attributes so we use these to filter return Promise.resolve({ data: ['promotedResourceAttribute'] }); } else if ( requestId === - 'explore-metrics-otel-resources-metric-job-instance-target_info{job=~"job1|job2",instance=~"instance1|instance2"}' + 'metrics-drilldown-otel-resources-metric-job-instance-target_info{job=~"job1|job2",instance=~"instance1|instance2"}' ) { // part of getFilteredResourceAttributes to get instance labels return Promise.resolve({ data: ['promotedResourceAttribute', 'resourceAttribute'] }); diff --git a/public/app/features/trails/otel/api.ts b/public/app/features/trails/otel/api.ts index 542e9b2cd70..3e1bf5bb293 100644 --- a/public/app/features/trails/otel/api.ts +++ b/public/app/features/trails/otel/api.ts @@ -56,7 +56,7 @@ export async function totalOtelResources( const responseTotal = await getBackendSrv().get( url, paramsTotalTargets, - `explore-metrics-otel-check-total-${query}` + `metrics-drilldown-otel-check-total-${query}` ); let jobs: string[] = []; @@ -127,7 +127,7 @@ export async function getDeploymentEnvironmentsWithoutScopes( const response = await getBackendSrv().get( url, params, - 'explore-metrics-otel-resources-deployment-env' + 'metrics-drilldown-otel-resources-deployment-env' ); // exclude __name__ or previously chosen filters @@ -162,7 +162,7 @@ export async function getDeploymentEnvironmentsWithScopes( ], 'deployment_environment', undefined, - 'explore-metrics-otel-resources-deployment-env' + 'metrics-drilldown-otel-resources-deployment-env' ); // exclude __name__ or previously chosen filters return response.data.data; @@ -230,7 +230,7 @@ export async function getFilteredResourceAttributes( const metricResponse = await getBackendSrv().get( url, metricParams, - `explore-metrics-otel-resources-metric-job-instance-${metricMatchParam}` + `metrics-drilldown-otel-resources-metric-job-instance-${metricMatchParam}` ); // the metric labels here const metricLabels = metricResponse.data ?? []; @@ -249,7 +249,7 @@ export async function getFilteredResourceAttributes( const targetInfoResponse = await getBackendSrv().get( url, targetInfoParams, - `explore-metrics-otel-resources-metric-job-instance-${targetInfoMatchParam}` + `metrics-drilldown-otel-resources-metric-job-instance-${targetInfoMatchParam}` ); const targetInfoAttributes = targetInfoResponse.data ?? []; @@ -294,7 +294,7 @@ export async function getNonPromotedOtelResources(datasourceUid: string, timeRan const targetInfoResponse = getBackendSrv().get( url, targetInfoParams, - `explore-metrics-all-otel-resources-on-target_info` + `metrics-drilldown-all-otel-resources-on-target_info` ); // all labels in all metrics @@ -311,7 +311,7 @@ export async function getNonPromotedOtelResources(datasourceUid: string, timeRan const metricResponse = await getBackendSrv().get( url, metricParams, - `explore-metrics-all-metric-labels-not-otel-resource-attributes` + `metrics-drilldown-all-metric-labels-not-otel-resource-attributes` ); const promResponses = await Promise.all([targetInfoResponse, metricResponse]); // otel resource attributes diff --git a/public/app/features/trails/otel/util.ts b/public/app/features/trails/otel/util.ts index 166d089ff4e..8b1d4e29ca5 100644 --- a/public/app/features/trails/otel/util.ts +++ b/public/app/features/trails/otel/util.ts @@ -548,7 +548,7 @@ function checkLabelPromotion(filters: AdHocVariableFilter[], nonPromotedOtelReso * Once we know this, we can add the selected filter to either the * VAR_OTEL_RESOURCES or VAR_FILTERS variable. * - * When the correct variable is updated, the rest of the explore metrics behavior will remain the same. + * When the correct variable is updated, the rest of the Metrics Drilldown behavior will remain the same. * * @param newStateFilters * @param prevStateFilters From 801ffea206fe44eb5f9d70e47f3e667cb1b60621 Mon Sep 17 00:00:00 2001 From: Vardan Torosyan Date: Thu, 20 Feb 2025 18:07:31 +0100 Subject: [PATCH 21/27] Docs: Passwordless auth is not available in cloud (#100825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Docs: Passwordless auth is not available in cloud * Update index.md --------- Co-authored-by: Irene Rodríguez --- .../configure-authentication/passwordless/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md index 06120e6c7da..4a4874ecb2a 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/passwordless/index.md @@ -15,7 +15,9 @@ Passwordless authentication lets Grafana users authenticate with a magic link or ## Enable passwordless authentication -{{< docs/experimental product="passwordless authentication" featureFlag="passwordlessMagicLinkAuthentication" >}} +{{% admonition type="note" %}} +Passwordless authentication is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `passwordlessMagicLinkAuthentication` feature toggle in Grafana to use this feature. +{{% /admonition %}} To enable passwordless authentication, use the following configuration: From b1b5b4766cd7a9bf54d527a98aaa32a5ab296d91 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Thu, 20 Feb 2025 12:22:04 -0500 Subject: [PATCH 22/27] Alerting: Sanitize Slack image upload comment labels (#101084) * Alerting: Sanitize Slack image upload comment labels upgrades grafana/alerting to d43046 --- go.mod | 2 +- go.sum | 4 ++-- pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/apistore/go.sum | 4 ++-- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/go.sum | 4 ++-- pkg/tests/api/alerting/api_notification_channel_test.go | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 8e30f7555b4..bbbee0fabf9 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index a48dd64155d..5bd68002377 100644 --- a/go.sum +++ b/go.sum @@ -1511,8 +1511,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 h1:GCgtfJ0G/uHdmZl9oTz2ok31zFT1HJEhVcsmVJFpSBg= -github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= +github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 h1:Z53rmFhUeif1/cFjOQqxHyv+YfXac4vzyl9yzdjxOoY= +github.com/grafana/alerting v0.0.0-20250219142948-d43046431703/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 0d3f27f6f58..e758daa2d06 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -192,7 +192,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 // indirect + github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 // indirect github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 0aaa26d90ec..fa94a7e92ec 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -566,8 +566,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 h1:GCgtfJ0G/uHdmZl9oTz2ok31zFT1HJEhVcsmVJFpSBg= -github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= +github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 h1:Z53rmFhUeif1/cFjOQqxHyv+YfXac4vzyl9yzdjxOoY= +github.com/grafana/alerting v0.0.0-20250219142948-d43046431703/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 1e999c1bba3..28745ab869d 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -117,7 +117,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 // indirect + github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 56e82e0a61a..09e3a20b8a6 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -397,8 +397,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1 h1:GCgtfJ0G/uHdmZl9oTz2ok31zFT1HJEhVcsmVJFpSBg= -github.com/grafana/alerting v0.0.0-20250219042117-00d67037a5f1/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= +github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 h1:Z53rmFhUeif1/cFjOQqxHyv+YfXac4vzyl9yzdjxOoY= +github.com/grafana/alerting v0.0.0-20250219142948-d43046431703/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 0774f90c30b..4753d915446 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -2525,7 +2525,7 @@ var expNonEmailNotifications = map[string][]string{ "attachments": [ { "title": "Integration Test [FIRING:1] SlackAlert1 (default)", - "title_link": "http://localhost:3000/alerting/grafana/UID_SlackAlert1/view", + "title_link": "http://localhost:3000/alerting/grafana/UID_SlackAlert1/view?orgId=1", "text": "Integration Test ", "fallback": "Integration Test [FIRING:1] SlackAlert1 (default)", "footer": "Grafana v", @@ -2545,7 +2545,7 @@ var expNonEmailNotifications = map[string][]string{ "attachments": [ { "title": "[FIRING:1] SlackAlert2 (default)", - "title_link": "http://localhost:3000/alerting/grafana/UID_SlackAlert2/view", + "title_link": "http://localhost:3000/alerting/grafana/UID_SlackAlert2/view?orgId=1", "text": "**Firing**\n\nValue: A=1\nLabels:\n - alertname = SlackAlert2\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_SlackAlert2/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%%3DUID_SlackAlert2&orgId=1\n", "fallback": "[FIRING:1] SlackAlert2 (default)", "footer": "Grafana v", From b78a63b0ad2ed51f4c679ed144cf40b21037a230 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Thu, 20 Feb 2025 12:47:40 -0500 Subject: [PATCH 23/27] Alerting: Use new image TokenProvider and send image url in annotation (#99989) * Send new annotation containing image url * Use new image TokenProvider with TokenStore New abstraction GetImage no longer needs to support parsing both token and url from annotations, as remote AM will use the new URLProvider. Instead, we use the new generic TokenProvider and give it a TokenStore backed by the grafana database. That means we revert back to always using token simplifying code and security considerations. * Upgrade grafana/alerting to merged commit SHA --- go.mod | 2 +- go.sum | 4 +- pkg/services/ngalert/models/image.go | 5 +- pkg/services/ngalert/notifier/images.go | 140 +++-------- pkg/services/ngalert/notifier/images_test.go | 230 +++++++------------ pkg/services/ngalert/notifier/log.go | 4 + pkg/services/ngalert/state/compat.go | 15 +- pkg/services/ngalert/state/compat_test.go | 13 +- pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/apistore/go.sum | 4 +- pkg/storage/unified/resource/go.mod | 2 +- pkg/storage/unified/resource/go.sum | 4 +- 12 files changed, 148 insertions(+), 277 deletions(-) diff --git a/go.mod b/go.mod index bbbee0fabf9..99f3d09f9fb 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index 5bd68002377..839d2e19349 100644 --- a/go.sum +++ b/go.sum @@ -1511,8 +1511,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 h1:Z53rmFhUeif1/cFjOQqxHyv+YfXac4vzyl9yzdjxOoY= -github.com/grafana/alerting v0.0.0-20250219142948-d43046431703/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= +github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 h1:LGH+tVzHCDrR9hsltmkP4jmNRg5IreQw5CNFbJKlnts= +github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= diff --git a/pkg/services/ngalert/models/image.go b/pkg/services/ngalert/models/image.go index 07c8578cdd9..1a90090aa53 100644 --- a/pkg/services/ngalert/models/image.go +++ b/pkg/services/ngalert/models/image.go @@ -6,8 +6,11 @@ import ( ) var ( - // ErrImageNotFound is returned when the image does not exist. + // ErrImageNotFound is returned when the image does not exist or is expired. ErrImageNotFound = errors.New("image not found") + + // ErrImageDataUnavailable is returned when image data is unavailable. Usually because the image is missing a path. + ErrImageDataUnavailable = errors.New("image data is unavailable") ) type Image struct { diff --git a/pkg/services/ngalert/notifier/images.go b/pkg/services/ngalert/notifier/images.go index d8effb176ce..d25b20bce80 100644 --- a/pkg/services/ngalert/notifier/images.go +++ b/pkg/services/ngalert/notifier/images.go @@ -6,145 +6,61 @@ import ( "io" "os" "path/filepath" - "strings" alertingImages "github.com/grafana/alerting/images" - alertingModels "github.com/grafana/alerting/models" - alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" ) -type imageProvider struct { +type tokenStore struct { store store.ImageStore logger log.Logger } +var _ alertingImages.TokenStore = (*tokenStore)(nil) + func newImageProvider(store store.ImageStore, logger log.Logger) alertingImages.Provider { - return &imageProvider{ + return alertingImages.NewTokenProvider(&tokenStore{ store: store, logger: logger, - } + }, newLogWrapper(logger)) } -func (i imageProvider) GetImage(ctx context.Context, uri string) (*alertingImages.Image, error) { - image, err := i.getImageFromURI(ctx, uri) +func (t tokenStore) GetImage(ctx context.Context, token string) (*alertingImages.Image, error) { + image, err := t.store.GetImage(ctx, token) if err != nil { if errors.Is(err, models.ErrImageNotFound) { - i.logger.Info("Image not found in database", "uri", uri) return nil, alertingImages.ErrImageNotFound } return nil, err } return &alertingImages.Image{ - Token: image.Token, - Path: image.Path, - URL: image.URL, - CreatedAt: image.CreatedAt, + URL: image.URL, + RawData: func(_ context.Context) (alertingImages.ImageContent, error) { + if image.Path == "" { + return alertingImages.ImageContent{}, models.ErrImageDataUnavailable + } + b, err := readImage(image.Path, t.logger) + if err != nil { + return alertingImages.ImageContent{}, err + } + return alertingImages.ImageContent{ + Name: filepath.Base(image.Path), + Content: b, + }, nil + }, }, nil } -func (i imageProvider) GetImageURL(ctx context.Context, alert *alertingNotify.Alert) (string, error) { - uri, err := getImageURI(alert) - if err != nil { - return "", err - } - - // If the identifier is a URL, validate that it corresponds to a stored, non-expired image. - if strings.HasPrefix(uri, "http") { - i.logger.Debug("Received an image URL in annotations", "alert", alert) - exists, err := i.store.URLExists(ctx, uri) - if err != nil { - return "", err - } - if !exists { - i.logger.Info("Image URL not found in database", "alert", alert) - return "", alertingImages.ErrImageNotFound - } - return uri, nil - } - - // If the identifier is a token, remove the prefix, get the image and return the URL. - token := strings.TrimPrefix(uri, "token://") - i.logger.Debug("Received an image token in annotations", "alert", alert, "token", token) - return i.getImageURLFromToken(ctx, token) -} - -// getImageURLFromToken takes a token and returns the URL of the image that token belongs to. -func (i imageProvider) getImageURLFromToken(ctx context.Context, token string) (string, error) { - image, err := i.store.GetImage(ctx, token) - if err != nil { - if errors.Is(err, models.ErrImageNotFound) { - i.logger.Info("Image not found in database", "token", token) - return "", alertingImages.ErrImageNotFound - } - return "", err - } - - if !image.HasURL() { - return "", alertingImages.ErrImagesNoURL - } - return image.URL, nil -} - -func (i imageProvider) GetRawImage(ctx context.Context, alert *alertingNotify.Alert) (io.ReadCloser, string, error) { - uri, err := getImageURI(alert) - if err != nil { - return nil, "", err - } - - image, err := i.getImageFromURI(ctx, uri) - if err != nil { - if errors.Is(err, models.ErrImageNotFound) { - i.logger.Info("Image not found in database", "alert", alert) - return nil, "", alertingImages.ErrImageNotFound - } - return nil, "", err - } - if !image.HasPath() { - return nil, "", alertingImages.ErrImagesNoPath - } - - // Return image bytes and filename. - readCloser, err := openImage(image.Path) - if err != nil { - i.logger.Error("Error looking for image on disk", "alert", alert, "path", image.Path, "error", err) - return nil, "", err - } - filename := filepath.Base(image.Path) - return readCloser, filename, nil -} - -func (i imageProvider) getImageFromURI(ctx context.Context, uri string) (*models.Image, error) { - // Check whether the uri is a URL or a token to know how to query the DB. - if strings.HasPrefix(uri, "http") { - i.logger.Debug("Received an image URL in annotations") - return i.store.GetImageByURL(ctx, uri) - } - - token := strings.TrimPrefix(uri, "token://") - i.logger.Debug("Received an image token in annotations", "token", token) - return i.store.GetImage(ctx, token) -} - -// getImageURI is a helper function to retrieve the image URI from the alert annotations as a string. -func getImageURI(alert *alertingNotify.Alert) (string, error) { - uri, ok := alert.Annotations[alertingModels.ImageTokenAnnotation] - if !ok { - return "", alertingImages.ErrNoImageForAlert - } - return string(uri), nil -} - -// openImage returns an the io representation of an image from the given path. -func openImage(path string) (io.ReadCloser, error) { +// readImage returns an image from the given path. +func readImage(path string, logger log.Logger) ([]byte, error) { fp := filepath.Clean(path) _, err := os.Stat(fp) if os.IsNotExist(err) || os.IsPermission(err) { - return nil, alertingImages.ErrImageNotFound + return nil, models.ErrImageNotFound } f, err := os.Open(fp) @@ -152,5 +68,11 @@ func openImage(path string) (io.ReadCloser, error) { return nil, err } - return f, nil + defer func() { + if err := f.Close(); err != nil { + logger.Error("Failed to close image file", "error", err) + } + }() + + return io.ReadAll(f) } diff --git a/pkg/services/ngalert/notifier/images_test.go b/pkg/services/ngalert/notifier/images_test.go index b16bd0e6360..85ed780cb3b 100644 --- a/pkg/services/ngalert/notifier/images_test.go +++ b/pkg/services/ngalert/notifier/images_test.go @@ -2,7 +2,6 @@ package notifier import ( "context" - "io" "os" "path/filepath" "testing" @@ -11,177 +10,98 @@ import ( alertingImages "github.com/grafana/alerting/images" alertingModels "github.com/grafana/alerting/models" alertingNotify "github.com/grafana/alerting/notify" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" - "github.com/prometheus/common/model" - "github.com/stretchr/testify/require" ) func TestGetImage(t *testing.T) { - fakeImageStore := store.NewFakeImageStore(t) - store := newImageProvider(fakeImageStore, log.NewNopLogger()) + testBytes := []byte("some test bytes") + testPath := generateTestFile(t, testBytes) - t.Run("queries by token when it gets a token", func(tt *testing.T) { - img := models.Image{ - Token: "test", - URL: "http://localhost:1234", - Path: "test.png", - } - err := fakeImageStore.SaveImage(context.Background(), &img) - require.NoError(tt, err) - - // nolint:staticcheck - savedImg, err := store.GetImage(context.Background(), "token://"+img.Token) - require.NoError(tt, err) - require.Equal(tt, savedImg.Token, img.Token) - require.Equal(tt, savedImg.URL, img.URL) - require.Equal(tt, savedImg.Path, img.Path) - }) - - t.Run("queries by URL when it gets a URL", func(tt *testing.T) { - img := models.Image{ - Token: "test", - Path: "test.png", - URL: "https://test.com/test.png", - } - err := fakeImageStore.SaveImage(context.Background(), &img) - require.NoError(tt, err) - - // nolint:staticcheck - savedImg, err := store.GetImage(context.Background(), img.URL) - require.NoError(tt, err) - require.Equal(tt, savedImg.Token, img.Token) - require.Equal(tt, savedImg.URL, img.URL) - require.Equal(tt, savedImg.Path, img.Path) - }) -} - -func TestGetImageURL(t *testing.T) { var ( - imageWithoutURL = models.Image{ - Token: "test-no-url", - CreatedAt: time.Now().UTC(), - ExpiresAt: time.Now().UTC().Add(24 * time.Hour), - } - testImage = models.Image{ - Token: "test", + imageWithoutPath = models.Image{ + Token: "test-token-no-path", URL: "https://test.com", CreatedAt: time.Now().UTC(), ExpiresAt: time.Now().UTC().Add(24 * time.Hour), } - ) - - fakeImageStore := store.NewFakeImageStore(t, &imageWithoutURL, &testImage) - store := newImageProvider(fakeImageStore, log.NewNopLogger()) - - tests := []struct { - name string - uri string - expURL string - expErr error - }{ - { - "URL does not exist", - "https://invalid.com/test", - "", - alertingImages.ErrImageNotFound, - }, { - "existing URL", - testImage.URL, - testImage.URL, - nil, - }, { - "token does not exist", - "token://invalid", - "", - alertingImages.ErrImageNotFound, - }, { - "existing token", - "token://" + testImage.Token, - testImage.URL, - nil, - }, { - "image has no URL", - "token://" + imageWithoutURL.Token, - "", - alertingImages.ErrImagesNoURL, - }, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - alert := alertingNotify.Alert{ - Alert: model.Alert{ - Annotations: model.LabelSet{alertingModels.ImageTokenAnnotation: model.LabelValue(test.uri)}, - }, - } - url, err := store.GetImageURL(context.Background(), &alert) - require.ErrorIs(tt, err, test.expErr) - require.Equal(tt, test.expURL, url) - }) - } -} - -func TestGetRawImage(t *testing.T) { - var ( - testBytes = []byte("some test bytes") - testPath = generateTestFile(t, testBytes) - imageWithoutPath = models.Image{ - Token: "test-no-path", - URL: "https://test-no-path.com", - CreatedAt: time.Now().UTC(), - ExpiresAt: time.Now().UTC().Add(24 * time.Hour), - } testImage = models.Image{ - Token: "test", + Token: "test-token", URL: "https://test.com", Path: testPath, CreatedAt: time.Now().UTC(), ExpiresAt: time.Now().UTC().Add(24 * time.Hour), } + testImageMissingFile = models.Image{ + Token: "test-token-missing-file", + URL: "https://test.com", + Path: "/tmp/missing/1234asdf.png", + CreatedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(24 * time.Hour), + } ) - fakeImageStore := store.NewFakeImageStore(t, &imageWithoutPath, &testImage) + fakeImageStore := store.NewFakeImageStore(t, &imageWithoutPath, &testImage, &testImageMissingFile) store := newImageProvider(fakeImageStore, log.NewNopLogger()) tests := []struct { - name string - uri string - expFilename string - expBytes []byte - expErr error + name string + token string + url string + expImage *alertingImages.Image + expImageContent *alertingImages.ImageContent + expRawDataErr error }{ { - "URL does not exist", - "https://invalid.com/test", - "", - nil, - alertingImages.ErrImageNotFound, + name: "Given existing raw token, expect image", + token: testImage.Token, + expImage: &alertingImages.Image{ + URL: testImage.URL, + }, }, { - "existing URL", - testImage.URL, - filepath.Base(testPath), - testBytes, - nil, + name: "Given existing token and url, expect image", + token: testImage.Token, + url: testImage.URL, + expImage: &alertingImages.Image{ + URL: testImage.URL, + }, }, { - "token does not exist", - "token://invalid", - "", - nil, - alertingImages.ErrImageNotFound, + name: "Given existing with just url, expect nil", + token: "", + url: testImage.URL, + expImage: nil, }, { - "existing token", - "token://" + testImage.Token, - filepath.Base(testPath), - testBytes, - nil, + name: "Given missing raw token, expect nil", + token: "invalid", + expImage: nil, }, { - "image has no path", - "token://" + imageWithoutPath.Token, - "", - nil, - alertingImages.ErrImagesNoPath, + name: "Given image with Path, expect RawData", + token: testImage.Token, + expImage: &alertingImages.Image{ + URL: testImage.URL, + }, + expImageContent: &alertingImages.ImageContent{ + Name: filepath.Base(testImage.Path), + Content: testBytes, + }, + }, { + name: "Given image with Path but file doesn't exist, expect RawData error", + token: testImageMissingFile.Token, + expImage: &alertingImages.Image{ + URL: testImageMissingFile.URL, + }, + expRawDataErr: models.ErrImageNotFound, + }, { + name: "Given image without Path, expect RawData error", + token: imageWithoutPath.Token, + expImage: &alertingImages.Image{ + URL: imageWithoutPath.URL, + }, + expRawDataErr: models.ErrImageDataUnavailable, }, } @@ -189,18 +109,24 @@ func TestGetRawImage(t *testing.T) { t.Run(test.name, func(tt *testing.T) { alert := alertingNotify.Alert{ Alert: model.Alert{ - Annotations: model.LabelSet{alertingModels.ImageTokenAnnotation: model.LabelValue(test.uri)}, + Annotations: model.LabelSet{alertingModels.ImageTokenAnnotation: model.LabelValue(test.token)}, }, } - readCloser, filename, err := store.GetRawImage(context.Background(), &alert) - require.ErrorIs(tt, err, test.expErr) - require.Equal(tt, test.expFilename, filename) - - if test.expBytes != nil { - b, err := io.ReadAll(readCloser) + image, err := store.GetImage(context.Background(), alert) + require.NoError(tt, err) + if test.expImage == nil { + require.Nil(tt, image) + return + } + require.Equal(tt, test.expImage.URL, image.URL) + if test.expImageContent != nil { + ic, err := image.RawData(context.Background()) require.NoError(tt, err) - require.Equal(tt, test.expBytes, b) - require.NoError(t, readCloser.Close()) + require.Equal(tt, *test.expImageContent, ic) + } + if test.expRawDataErr != nil { + _, err := image.RawData(context.Background()) + require.ErrorIs(tt, err, test.expRawDataErr) } }) } diff --git a/pkg/services/ngalert/notifier/log.go b/pkg/services/ngalert/notifier/log.go index c9dc97ea413..4aef94c49ed 100644 --- a/pkg/services/ngalert/notifier/log.go +++ b/pkg/services/ngalert/notifier/log.go @@ -10,6 +10,10 @@ var LoggerFactory alertingLogging.LoggerFactory = func(logger string, ctx ...any return &logWrapper{log.New(append([]any{logger}, ctx...)...)} } +func newLogWrapper(logger log.Logger, ctx ...any) alertingLogging.Logger { + return &logWrapper{logger.New(ctx...)} +} + type logWrapper struct { *log.ConcreteLogger } diff --git a/pkg/services/ngalert/state/compat.go b/pkg/services/ngalert/state/compat.go index 55e1fdf76fe..7f4b5c41428 100644 --- a/pkg/services/ngalert/state/compat.go +++ b/pkg/services/ngalert/state/compat.go @@ -17,6 +17,7 @@ import ( apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" + ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" ) const ( @@ -48,8 +49,8 @@ func StateToPostableAlert(transition StateTransition, appURL *url.URL) *models.P nA[alertingModels.ValueStringAnnotation] = alertState.LastEvaluationString } - if alertState.Image != nil && alertState.Image.Token != "" { - nA[alertingModels.ImageTokenAnnotation] = alertState.Image.Token + if alertState.Image != nil { + attachImageAnnotations(alertState.Image, nA) } if alertState.StateReason != "" { @@ -153,3 +154,13 @@ func FromAlertsStateToStoppedAlert(firingStates []StateTransition, appURL *url.U } return alerts } + +// attachImageAnnotations attaches image annotations to the alert. +func attachImageAnnotations(image *ngModels.Image, a data.Labels) { + if image.Token != "" { + a[alertingModels.ImageTokenAnnotation] = image.Token + } + if image.URL != "" { + a[alertingModels.ImageURLAnnotation] = image.URL + } +} diff --git a/pkg/services/ngalert/state/compat_test.go b/pkg/services/ngalert/state/compat_test.go index bd0189badb8..84d5ad8e6fa 100644 --- a/pkg/services/ngalert/state/compat_test.go +++ b/pkg/services/ngalert/state/compat_test.go @@ -120,10 +120,10 @@ func Test_StateToPostableAlert(t *testing.T) { require.Equal(t, expected, result.Annotations) }) - t.Run("add __alertImageToken__ if there is an image token", func(t *testing.T) { + t.Run("add both annotations if there is an image token and url", func(t *testing.T) { alertState := randomTransition(eval.Normal, tc.state) alertState.Annotations = randomMapOfStrings() - alertState.Image = &ngModels.Image{Token: "test_token"} + alertState.Image = &ngModels.Image{Token: "test_token", URL: "test_url"} result := StateToPostableAlert(alertState, appURL) @@ -131,12 +131,17 @@ func Test_StateToPostableAlert(t *testing.T) { for k, v := range alertState.Annotations { expected[k] = v } - expected["__alertImageToken__"] = alertState.Image.Token + expected[alertingModels.ImageTokenAnnotation] = alertState.Image.Token + expected[alertingModels.ImageURLAnnotation] = alertState.Image.URL + + // Sanity check that the annotation is correct. + require.Contains(t, result.Annotations[alertingModels.ImageTokenAnnotation], alertState.Image.Token) + require.Contains(t, result.Annotations[alertingModels.ImageURLAnnotation], alertState.Image.URL) require.Equal(t, expected, result.Annotations) }) - t.Run("don't add __alertImageToken__ if there's no image token", func(t *testing.T) { + t.Run("don't add annotations if there's no image token or url", func(t *testing.T) { alertState := randomTransition(eval.Normal, tc.state) alertState.Annotations = randomMapOfStrings() alertState.Image = &ngModels.Image{} diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index e758daa2d06..16b8b6442de 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -192,7 +192,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 // indirect + github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 // indirect github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index fa94a7e92ec..faf5e7a9e0f 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -566,8 +566,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 h1:Z53rmFhUeif1/cFjOQqxHyv+YfXac4vzyl9yzdjxOoY= -github.com/grafana/alerting v0.0.0-20250219142948-d43046431703/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= +github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 h1:LGH+tVzHCDrR9hsltmkP4jmNRg5IreQw5CNFbJKlnts= +github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 28745ab869d..a7d81ec25ca 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -117,7 +117,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 // indirect + github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 09e3a20b8a6..9ee7f749839 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -397,8 +397,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/alerting v0.0.0-20250219142948-d43046431703 h1:Z53rmFhUeif1/cFjOQqxHyv+YfXac4vzyl9yzdjxOoY= -github.com/grafana/alerting v0.0.0-20250219142948-d43046431703/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= +github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 h1:LGH+tVzHCDrR9hsltmkP4jmNRg5IreQw5CNFbJKlnts= +github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= From 2313eca636483a2bcb211491a3d03c6a866c68a1 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:55:28 -0500 Subject: [PATCH 24/27] Chore: Update local go dependencies (#101088) --- apps/alerting/notifications/go.mod | 2 +- apps/alerting/notifications/go.sum | 4 +-- go.mod | 20 +++++++-------- go.sum | 40 ++++++++++++++--------------- go.work.sum | 2 +- pkg/storage/unified/apistore/go.mod | 10 ++++---- pkg/storage/unified/apistore/go.sum | 8 +++--- pkg/storage/unified/resource/go.mod | 4 +-- 8 files changed, 45 insertions(+), 45 deletions(-) diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index de67c2ef820..ebbf0284681 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -36,7 +36,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d // indirect + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 60a25e25bcd..34db4a1a692 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -73,8 +73,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk= github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d h1:0WJ4uZ3gtbX7S5RGMarA+0TccmB7qEPhhNm34NcfBzs= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d/go.mod h1:PNmbi49lVrv2b2I8pdu46dTs2728lKEbnVuQD8I5MnM= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 h1:bOKkAzmkCnK2EN0LGsHoq5HyfXMLKXrr8+q35Fh+XQI= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295/go.mod h1:1iWqntBW91TsiyqneHrqTFV12vd1Tei+f5VY2ovkNAo= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= diff --git a/go.mod b/go.mod index 99f3d09f9fb..8e2e1792445 100644 --- a/go.mod +++ b/go.mod @@ -200,21 +200,21 @@ require ( ) require ( - github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173 // @grafana/plugins-platform-backend - github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d // @grafana/alerting-backend - github.com/grafana/grafana/apps/investigations v0.0.0-20250217190546-9df6412e9255 // @fcjack @matryer - github.com/grafana/grafana/apps/playlist v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad - github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad - github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8 // @grafana/plugins-platform-backend + github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250220163425-b4c4b9abbdc8 // @grafana/alerting-backend + github.com/grafana/grafana/apps/investigations v0.0.0-20250220163425-b4c4b9abbdc8 // @fcjack @matryer + github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // @grafana/grafana-app-platform-squad // This needs to be here for other projects that import grafana/grafana // For local development grafana/grafana will always use the local files // Check go.work file for details github.com/grafana/grafana/pkg/promlib v0.0.8 // @grafana/oss-big-tent - github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad - github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage - github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage + github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250220154326-6e5de80ef295 // @grafana/grafana-search-and-storage + github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250220154326-6e5de80ef295 // @grafana/grafana-search-and-storage ) require github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend diff --git a/go.sum b/go.sum index 839d2e19349..88780f81fc9 100644 --- a/go.sum +++ b/go.sum @@ -1547,28 +1547,28 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= -github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173 h1:uOM89HiWVVOTls0LrD4coHTckb2lA4U0sIJwCYdbhbw= -github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= -github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d h1:NRVOtiG1aUwOazBj9KM7X2o2shsM6TchqisezzoH1gw= -github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d/go.mod h1:AvleS6icyPmcBjihtx5jYEvdzLmHGBp66NuE0AMR57A= -github.com/grafana/grafana/apps/investigations v0.0.0-20250217190546-9df6412e9255 h1:4bEH/v1cbODIie8rMAqrsUlXeWLu7ht9SXNIrdvMWmA= -github.com/grafana/grafana/apps/investigations v0.0.0-20250217190546-9df6412e9255/go.mod h1:ygFcJP2McdSeMJVj/3YrKafZMc/lZBsp54HO51MtJYw= -github.com/grafana/grafana/apps/playlist v0.0.0-20250121113133-e747350fee2d h1:cJ+9jgHw8Z17rM+DCdxvyWxPqH9OB9fkPLbnWEO+QGg= -github.com/grafana/grafana/apps/playlist v0.0.0-20250121113133-e747350fee2d/go.mod h1:DjJe5osrW/BKrzN9hAAOSElNWutj1bcriExa7iDP7kA= -github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d h1:aBD5kzsIAh50vjNqUkWK9mNpLGIBYAnKkWtUepGNAiQ= -github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d h1:0WJ4uZ3gtbX7S5RGMarA+0TccmB7qEPhhNm34NcfBzs= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d/go.mod h1:PNmbi49lVrv2b2I8pdu46dTs2728lKEbnVuQD8I5MnM= -github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d h1:vVaLOibSj5lDwCb+pa0fly9uods8nTq1NJPYrTuPAGE= -github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d/go.mod h1:srJ6H0DL7c9Cz4CmK0h4AOlC1sm8UDwn9jBZ+RHxBt4= +github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8 h1:mG/6nDlEBVxWlo2GQJVASzucw3ByPIBsec06XcPrjgQ= +github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= +github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250220163425-b4c4b9abbdc8 h1:w42GlvkmHG4nM/p1kb2nKmROVP+AHtL3qWEYMhnhCVM= +github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:dHhFF484qs1cmdIShKCB3kl+tMJyc4yuwgTQ3Afz37o= +github.com/grafana/grafana/apps/investigations v0.0.0-20250220163425-b4c4b9abbdc8 h1:jRcI2fE/u0tHfCmPLIt21M3DYOt3L6BmZcu/LeV2sRw= +github.com/grafana/grafana/apps/investigations v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:ygFcJP2McdSeMJVj/3YrKafZMc/lZBsp54HO51MtJYw= +github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450 h1:h3HsylGnuZiBgT5Q/N6bH+LazY3+nA98R76Xwcd0zXg= +github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450/go.mod h1:KKIsWpbv88Lwwcvdjon73zFL7vNJvuXLtsSoUjJErTw= +github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0= +github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 h1:bOKkAzmkCnK2EN0LGsHoq5HyfXMLKXrr8+q35Fh+XQI= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295/go.mod h1:1iWqntBW91TsiyqneHrqTFV12vd1Tei+f5VY2ovkNAo= +github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 h1:ivbywO8ZnmzDDkn169qUb9REsCGYAA7H8W6VfqSmCEw= +github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295/go.mod h1:OugmKouuvgWeOI8Kghram2Pv8b/1SuXRR8/iW068uLk= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= -github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d h1:cYBjuhb3m5oC6Z00Kw8DdySFaNhwb38SMxx0oXnz5vQ= -github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= -github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d h1:4w4x8g6xCSRj06er81atNSQVL0fTSwLekDYsRxpz+M0= -github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d/go.mod h1:CXpwZ3Mkw6xVlGKc0SqUxqXCP3Uv182q6qAQnLaLxRg= -github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d h1:xzJi+ioYZpiGYjiox9eZYqu1zczNV2PBnG6Y5XK7Ar0= -github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d/go.mod h1:2jv9CnelxFIH0q4FBaKt8BqaJyjruLmGk/31TzgsrPo= +github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 h1:wSqgLKFwI7fyeqf3djRXGClBLb/UPjZ4XPm/UsKFDB0= +github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450/go.mod h1:HGz9/wKeN6U48g4F8RxHzpsFxJAR9anVltZp07A86UA= +github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250220154326-6e5de80ef295 h1:mDRND2VJaVzhIHrcAfOCV1X39QHWGcVByQkUeqBjpNs= +github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250220154326-6e5de80ef295/go.mod h1:b4i60JgZZjH8haVc4Uw8ye0FzxX4r28Y0EvduqxBdWo= +github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250220154326-6e5de80ef295 h1:AtOyCicDN4xqF2ZHCXaVBQj/BQpFtb/RfYD8KlwGkaY= +github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250220154326-6e5de80ef295/go.mod h1:QAzEkmHuxP5RPdc9H+l/Nxl+VOUtV075/Daqf77Nhf0= github.com/grafana/grafana/pkg/util/xorm v0.0.1 h1:72QZjxWIWpSeOF8ob4aMV058kfgZyeetkAB8dmeti2o= github.com/grafana/grafana/pkg/util/xorm v0.0.1/go.mod h1:eNfbB9f2jM8o9RfwqwjY8SYm5tvowJ8Ly+iE4P9rXII= github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 h1:NznuPwItog+rwdVg8hAuGKP29ndRSzJAwhxKldkP8oQ= diff --git a/go.work.sum b/go.work.sum index 297372e3a99..76d7190d1b2 100644 --- a/go.work.sum +++ b/go.work.sum @@ -586,7 +586,6 @@ github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= @@ -617,6 +616,7 @@ github.com/grafana/cog v0.0.23 h1:/0CCJ24Z8XXM2DnboSd2FzoIswUroqIZzVr8oJWmMQs= github.com/grafana/cog v0.0.23/go.mod h1:jrS9indvWuDs60RHEZpLaAkmZdgyoLKMOEUT0jiB1t0= github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90= +github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 16b8b6442de..d99962d0d35 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -16,9 +16,9 @@ require ( github.com/google/uuid v1.6.0 github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d - github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d - github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 + github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 + github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250220154326-6e5de80ef295 github.com/stretchr/testify v1.10.0 gocloud.dev v0.40.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 @@ -200,9 +200,9 @@ require ( github.com/grafana/grafana-aws-sdk v0.31.5 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect github.com/grafana/grafana-plugin-sdk-go v0.266.0 // indirect - github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d // indirect + github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // indirect github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect - github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d // indirect + github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index faf5e7a9e0f..4d78ed949da 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -586,12 +586,12 @@ github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtM github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= -github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d h1:aBD5kzsIAh50vjNqUkWK9mNpLGIBYAnKkWtUepGNAiQ= -github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= +github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0= +github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= -github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d h1:cYBjuhb3m5oC6Z00Kw8DdySFaNhwb38SMxx0oXnz5vQ= -github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= +github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 h1:wSqgLKFwI7fyeqf3djRXGClBLb/UPjZ4XPm/UsKFDB0= +github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450/go.mod h1:HGz9/wKeN6U48g4F8RxHzpsFxJAR9anVltZp07A86UA= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index a7d81ec25ca..57c54262114 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -16,8 +16,8 @@ require ( github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible github.com/grafana/grafana-plugin-sdk-go v0.266.0 - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d - github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // indirect + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 + github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 github.com/hashicorp/golang-lru/v2 v2.0.7 From e54149e551ce95ea466bb72c931cd14808045537 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Thu, 20 Feb 2025 17:56:55 +0000 Subject: [PATCH 25/27] Explore: Move drilldown apps from Explore to a new navbar item "Drilldown" (#100409) * Move drilldown apps from Explore to a new navbar item "Drilldown" * Commit make i18n-extract * Update drilldown icon * Added alert to explore with call out to drilldown apps * Add isNew field for nav item which shows a "New!" badge on the navbar and expands it by default * Fix e2e test --- .../loki-table-explore-to-dash.spec.ts | 1 + packages/grafana-data/src/types/icon.ts | 1 + packages/grafana-data/src/types/navModel.ts | 1 + packages/grafana-icons/svg/drilldown.svg | 6 ++++ pkg/api/accesscontrol.go | 5 +++- pkg/api/api.go | 1 + pkg/services/accesscontrol/models.go | 3 +- pkg/services/navtree/models.go | 3 ++ pkg/services/navtree/navtreeimpl/applinks.go | 8 +++--- pkg/services/navtree/navtreeimpl/navtree.go | 24 ++++++++++++---- .../AppChrome/MegaMenu/MegaMenuItem.tsx | 6 ++-- .../AppChrome/MegaMenu/MegaMenuItemText.tsx | 2 +- .../app/core/utils/navBarItem-translations.ts | 2 ++ public/app/features/explore/Explore.tsx | 28 +++++++++++++++++++ public/app/routes/routes.tsx | 4 +++ public/img/icons/unicons/drilldown.svg | 6 ++++ public/locales/en-US/grafana.json | 9 ++++++ public/locales/pseudo-LOCALE/grafana.json | 9 ++++++ 18 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 packages/grafana-icons/svg/drilldown.svg create mode 100644 public/img/icons/unicons/drilldown.svg diff --git a/e2e/various-suite/loki-table-explore-to-dash.spec.ts b/e2e/various-suite/loki-table-explore-to-dash.spec.ts index 1477f6f0165..f4867855ca3 100644 --- a/e2e/various-suite/loki-table-explore-to-dash.spec.ts +++ b/e2e/various-suite/loki-table-explore-to-dash.spec.ts @@ -175,6 +175,7 @@ describe('Loki Query Editor', () => { // One row with two cells cy.get('[role="cell"]').should('have.length', 2); + cy.contains('label', 'targetLabelName').scrollIntoView(); cy.contains('label', 'targetLabelName').should('be.visible'); cy.contains('label', 'targetLabelName').click(); cy.contains('label', 'targetLabelName').within(() => { diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 357cf07e46e..b1092680181 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -93,6 +93,7 @@ export const availableIconsIndex = { 'document-layout-left': true, 'download-alt': true, draggabledots: true, + drilldown: true, edit: true, 'ellipsis-v': true, enter: true, diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts index 066d879df3d..f9ebb23fc07 100644 --- a/packages/grafana-data/src/types/navModel.ts +++ b/packages/grafana-data/src/types/navModel.ts @@ -14,6 +14,7 @@ export interface NavLinkDTO { sortWeight?: number; hideFromTabs?: boolean; roundIcon?: boolean; + isNew?: boolean; /** * This is true for some sections that have no children (but is still a section) **/ diff --git a/packages/grafana-icons/svg/drilldown.svg b/packages/grafana-icons/svg/drilldown.svg new file mode 100644 index 00000000000..27f627ce19f --- /dev/null +++ b/packages/grafana-icons/svg/drilldown.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index f74e32f1b50..1855531c0ce 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -60,12 +60,15 @@ func (hs *HTTPServer) declareFixedRoles() error { Role: ac.RoleDTO{ Name: "fixed:datasources:explorer", DisplayName: "Explorer", - Description: "Enable the Explore feature. Data source permissions still apply; you can only query data sources for which you have query permissions.", + Description: "Enable the Explore and Drilldown features. Data source permissions still apply; you can only query data sources for which you have query permissions.", Group: "Data sources", Permissions: []ac.Permission{ { Action: ac.ActionDatasourcesExplore, }, + { + Action: ac.ActionDatasourcesDrilldown, + }, }, }, Grants: []string{string(org.RoleEditor)}, diff --git a/pkg/api/api.go b/pkg/api/api.go index acc8a29c21f..5b085c89d00 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -183,6 +183,7 @@ func (hs *HTTPServer) registerRoutes() { } r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index) + r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesDrilldown)), hs.Index) r.Get("/playlists/", reqSignedIn, hs.Index) r.Get("/playlists/*", reqSignedIn, hs.Index) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index dc12171eaa5..4c15e3c5e56 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -386,7 +386,8 @@ const ( ActionSettingsWrite = "settings:write" // Datasources actions - ActionDatasourcesExplore = "datasources:explore" + ActionDatasourcesExplore = "datasources:explore" + ActionDatasourcesDrilldown = "datasources:drilldown" // Global Scopes ScopeGlobalUsersAll = "global.users:*" diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go index bf324abc076..97d7ab1112b 100644 --- a/pkg/services/navtree/models.go +++ b/pkg/services/navtree/models.go @@ -16,6 +16,7 @@ const ( WeightSavedItems WeightDashboard WeightExplore + WeightDrilldown WeightAlerting WeightAlertsAndIncidents WeightTestingAndSynthetics @@ -37,6 +38,7 @@ const ( NavIDRoot = "root" NavIDDashboards = "dashboards/browse" NavIDExplore = "explore" + NavIDDrilldown = "drilldown" NavIDCfg = "cfg" // NavIDCfg is the id for org configuration navigation node NavIDAlertsAndIncidents = "alerts-and-incidents" NavIDTestingAndSynthetics = "testing-and-synthetics" @@ -70,6 +72,7 @@ type NavLink struct { EmptyMessageId string `json:"emptyMessageId,omitempty"` PluginID string `json:"pluginId,omitempty"` // (Optional) The ID of the plugin that registered nav link (e.g. as a standalone plugin page) IsCreateAction bool `json:"isCreateAction,omitempty"` + IsNew bool `json:"isNew,omitempty"` // (Optional) Adds "New!" badge to the nav link and expands it by default Keywords []string `json:"keywords,omitempty"` ParentItem *NavLink `json:"parentItem,omitempty"` // (Optional) The parent item of the nav link } diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 1adc19e0d57..5c6b88756d3 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -286,9 +286,9 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-k8s-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 1, Text: "Kubernetes"}, "grafana-dbo11y-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 2, Text: "Databases"}, "grafana-app-observability-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApplication, Text: "Application", Icon: "graph-bar"}, - "grafana-lokiexplore-app": {SectionID: navtree.NavIDExplore, SortWeight: 2, Text: "Logs"}, - "grafana-exploretraces-app": {SectionID: navtree.NavIDExplore, SortWeight: 3, Text: "Traces"}, - "grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 4, Text: "Profiles"}, + "grafana-lokiexplore-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 2, Text: "Logs"}, + "grafana-exploretraces-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 3, Text: "Traces"}, + "grafana-pyroscope-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 4, Text: "Profiles"}, "grafana-kowalski-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightFrontend, Text: "Frontend", Icon: "frontend-observability"}, "grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 2, Text: "Synthetics"}, "grafana-irm-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "IRM"}, @@ -310,7 +310,7 @@ func (s *ServiceImpl) readNavigationSettings() { } if s.features.IsEnabledGlobally(featuremgmt.FlagExploreMetricsUseExternalAppPlugin) { - s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Metrics"} + s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDDrilldown, SortWeight: 1, Text: "Metrics"} } if s.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAdvisor) { diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 1f461db67f3..7158c8884dc 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -120,7 +120,6 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere } if s.cfg.ExploreEnabled && hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) { - exploreChildNavLinks := s.buildExploreNavLinks(c) treeRoot.AddSection(&navtree.NavLink{ Text: "Explore", Id: navtree.NavIDExplore, @@ -128,7 +127,20 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere Icon: "compass", SortWeight: navtree.WeightExplore, Url: s.cfg.AppSubURL + "/explore", - Children: exploreChildNavLinks, + }) + } + + if hasAccess(ac.EvalPermission(ac.ActionDatasourcesDrilldown)) { + drilldownChildNavLinks := s.buildDrilldownNavLinks(c) + treeRoot.AddSection(&navtree.NavLink{ + Text: "Drilldown", + Id: navtree.NavIDDrilldown, + SubTitle: "Drill down into your data using Grafana's powerful queryless apps", + Icon: "drilldown", + IsNew: true, + SortWeight: navtree.WeightDrilldown, + Url: s.cfg.AppSubURL + "/drilldown", + Children: drilldownChildNavLinks, }) } @@ -560,10 +572,10 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *n return nil } -func (s *ServiceImpl) buildExploreNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink { - exploreChildNavs := []*navtree.NavLink{} +func (s *ServiceImpl) buildDrilldownNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink { + drilldownChildNavs := []*navtree.NavLink{} if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) && !s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetricsUseExternalAppPlugin) { - exploreChildNavs = append(exploreChildNavs, &navtree.NavLink{ + drilldownChildNavs = append(drilldownChildNavs, &navtree.NavLink{ Text: "Metrics", SubTitle: "Queryless exploration of your metrics", Id: "explore/metrics", @@ -571,5 +583,5 @@ func (s *ServiceImpl) buildExploreNavLinks(c *contextmodel.ReqContext) []*navtre Icon: "code-branch", }) } - return exploreChildNavs + return drilldownChildNavs } diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx index ce8b9a53d40..4379283f3c5 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx @@ -5,8 +5,9 @@ import { useLocation } from 'react-router-dom-v5-compat'; import { useLocalStorage } from 'react-use'; import { GrafanaTheme2, NavModelItem, toIconName } from '@grafana/data'; -import { useStyles2, Text, IconButton, Icon, Stack } from '@grafana/ui'; +import { useStyles2, Text, IconButton, Icon, Stack, Badge } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; +import { t } from 'app/core/internationalization'; import { Indent } from '../../Indent/Indent'; @@ -35,7 +36,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPi const isActive = link === activeItem || (level === MAX_DEPTH && hasActiveChild); const [sectionExpanded, setSectionExpanded] = useLocalStorage( `grafana.navigation.expanded[${link.text}]`, - Boolean(hasActiveChild) + Boolean(hasActiveChild || link.isNew) ); const showExpandButton = level < MAX_DEPTH && Boolean(linkHasChildren(link) || link.emptyMessage); const item = useRef(null); @@ -107,6 +108,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPi > {level === 0 && iconElement && {iconElement}} {link.text} + {link.isNew && }
diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx index 471450c7d23..b6d18b50d9d 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx @@ -114,7 +114,7 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive']) => ({ position: 'relative', width: '100%', - '&:hover, &:focus-visible': { + '&:hover span, &:focus-visible span': { color: theme.colors.text.primary, textDecoration: 'underline', }, diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index 993a914d2c7..eb533d88b07 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -61,6 +61,8 @@ export function getNavTitle(navId: string | undefined) { return t('nav.scenes.title', 'Scenes'); case 'explore': return t('nav.explore.title', 'Explore'); + case 'drilldown': + return t('nav.drilldown.title', 'Drilldown'); case 'alerting': return t('nav.alerting.title', 'Alerting'); case 'plugin-page-grafana-oncall-app': diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 28ad370648e..4927fc5be4a 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -23,14 +23,18 @@ import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { AdHocFilterItem, + Alert, ErrorBoundaryAlert, + LinkButton, PanelContainer, ScrollContainer, + Stack, Themeable2, withTheme2, } from '@grafana/ui'; import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/src/components/Table/types'; import { supportedFeatures } from 'app/core/history/richHistoryStorageProvider'; +import { t, Trans } from 'app/core/internationalization'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { StoreState } from 'app/types'; @@ -564,6 +568,9 @@ export class Explore extends PureComponent { if (showCorrelationHelper && correlationEditorHelperData !== undefined) { correlationsBox = ; } + const isDsCompatibleWithDrilldown = ['prometheus', 'loki', 'tempo', 'grafana-pyroscope-datasource'].includes( + datasourceInstance?.type || '' + ); return ( @@ -593,6 +600,27 @@ export class Explore extends PureComponent { <> + {isDsCompatibleWithDrilldown && ( + + + + + Looking for the Grafana Explore apps? They are now called the Grafana Drilldown apps + and can be found under Menu > Drilldown + + + + Go to Grafana Drilldown + + + + )} {correlationsBox} , + }, { path: '/apps', component: () => , diff --git a/public/img/icons/unicons/drilldown.svg b/public/img/icons/unicons/drilldown.svg new file mode 100644 index 00000000000..27f627ce19f --- /dev/null +++ b/public/img/icons/unicons/drilldown.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e80a59d9129..d7cba3f8990 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1419,6 +1419,11 @@ }, "explore": { "add-to-dashboard": "Add to dashboard", + "drilldownInfo": { + "action": "Go to Grafana Drilldown", + "description": "Looking for the Grafana Explore apps? They are now called the Grafana Drilldown apps and can be found under <1>Menu > Drilldown", + "title": "Explore Metrics, Logs, Traces and Profiles have moved!" + }, "logs": { "logs-volume": { "add-filters": "Add more labels to your query to narrow down your search.", @@ -2499,6 +2504,9 @@ "detect": { "title": "Detect" }, + "drilldown": { + "title": "Drilldown" + }, "explore": { "title": "Explore" }, @@ -2714,6 +2722,7 @@ "close": "Close menu", "dock": "Dock menu", "list-label": "Navigation", + "new": "New!", "open": "Open menu", "undock": "Undock menu" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 43096c5ce25..2dc0f928c44 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1419,6 +1419,11 @@ }, "explore": { "add-to-dashboard": "Åđđ ŧő đäşĥþőäřđ", + "drilldownInfo": { + "action": "Ğő ŧő Ğřäƒäʼnä Đřįľľđőŵʼn", + "description": "Ŀőőĸįʼnģ ƒőř ŧĥę Ğřäƒäʼnä Ēχpľőřę äppş? Ŧĥęy äřę ʼnőŵ čäľľęđ ŧĥę Ğřäƒäʼnä Đřįľľđőŵʼn äppş äʼnđ čäʼn þę ƒőūʼnđ ūʼnđęř <1>Męʼnū > Đřįľľđőŵʼn", + "title": "Ēχpľőřę Męŧřįčş, Ŀőģş, Ŧřäčęş äʼnđ Přőƒįľęş ĥävę mővęđ!" + }, "logs": { "logs-volume": { "add-filters": "Åđđ mőřę ľäþęľş ŧő yőūř qūęřy ŧő ʼnäřřőŵ đőŵʼn yőūř şęäřčĥ.", @@ -2499,6 +2504,9 @@ "detect": { "title": "Đęŧęčŧ" }, + "drilldown": { + "title": "Đřįľľđőŵʼn" + }, "explore": { "title": "Ēχpľőřę" }, @@ -2714,6 +2722,7 @@ "close": "Cľőşę męʼnū", "dock": "Đőčĸ męʼnū", "list-label": "Ńävįģäŧįőʼn", + "new": "Ńęŵ!", "open": "Øpęʼn męʼnū", "undock": "Ůʼnđőčĸ męʼnū" }, From 49e5f77dd182d923f8e2fc89980a225daf98f162 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:00:15 -0600 Subject: [PATCH 26/27] CI: Remove unused release_publisher scripts (#101019) * Remove the unused `release_publisher` script. * Remove the "whats new check" in Drone. * Automatically set the What's New URL in releases based on the tagged version. --- .drone.yml | 47 +--- package.json | 2 +- pkg/build/cmd/grafanacom.go | 41 ++-- pkg/build/cmd/grafanacom_test.go | 24 +++ pkg/build/versions/parse.go | 6 +- scripts/build/publish.sh | 18 -- .../release_publisher/externalrelease.go | 71 ------- scripts/build/release_publisher/main.go | 80 ------- .../build/release_publisher/publisher_test.go | 201 ------------------ ...nterprise-5.4.0-123pre1.linux-amd64.tar.gz | 0 ...se-5.4.0-123pre1.linux-amd64.tar.gz.sha256 | 1 - ...enterprise-5.4.0-123pre1.windows-amd64.msi | 0 ...ise-5.4.0-123pre1.windows-amd64.msi.sha256 | 1 - ...enterprise-5.4.0-123pre1.windows-amd64.zip | 0 ...ise-5.4.0-123pre1.windows-amd64.zip.sha256 | 1 - ...rafana-enterprise-5.4.0-123pre1.x86_64.rpm | 0 ...enterprise-5.4.0-123pre1.x86_64.rpm.sha256 | 1 - ...grafana-enterprise_5.4.0-123pre1_amd64.deb | 0 ...-enterprise_5.4.0-123pre1_amd64.deb.sha256 | 1 - .../drone/pipelines/whats_new_checker.star | 42 ---- scripts/drone/rgm.star | 5 - 21 files changed, 59 insertions(+), 483 deletions(-) delete mode 100755 scripts/build/publish.sh delete mode 100644 scripts/build/release_publisher/externalrelease.go delete mode 100644 scripts/build/release_publisher/main.go delete mode 100644 scripts/build/release_publisher/publisher_test.go delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.msi delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.msi.sha256 delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb delete mode 100644 scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 delete mode 100644 scripts/drone/pipelines/whats_new_checker.star diff --git a/.drone.yml b/.drone.yml index 4e3b9943f49..df536b9f81c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4119,51 +4119,6 @@ volumes: path: /var/run/docker.sock name: docker --- -clone: - retries: 3 -depends_on: [] -environment: - EDITION: oss -image_pull_secrets: -- gcr -- gar -kind: pipeline -name: release-whatsnew-checker -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: [] - environment: - CGO_ENABLED: 0 - image: golang:1.23.5-alpine - name: compile-build-cmd -- commands: - - ./bin/build whatsnew-checker - depends_on: - - compile-build-cmd - image: golang:1.23.5-alpine - name: whats-new-checker -trigger: - event: - exclude: - - promote - ref: - exclude: - - refs/tags/*-cloud* - include: - - refs/tags/v* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- clone: retries: 3 depends_on: @@ -5605,6 +5560,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 7a16cbedacf8a7381304abfde88319ce712a133e24c1ab304543c4c7ac318816 +hmac: 6e01278547a6f0803b7723e5f1e73bd94c572fa4805517232d3897717cee30f6 ... diff --git a/package.json b/package.json index 949b224ab90..5a830bf9c88 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "generate-apis": "yarn process-specs && rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts" }, "grafana": { - "whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v11-5/", + "whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v%[1]s-%[2]s/", "releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/" }, "devDependencies": { diff --git a/pkg/build/cmd/grafanacom.go b/pkg/build/cmd/grafanacom.go index ab53217f7a4..cb413f697bc 100644 --- a/pkg/build/cmd/grafanacom.go +++ b/pkg/build/cmd/grafanacom.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/build/gcloud/storage" "github.com/grafana/grafana/pkg/build/gcom" "github.com/grafana/grafana/pkg/build/packaging" + "github.com/grafana/grafana/pkg/build/versions" ) const grafanaAPI = "https://grafana.com/api" @@ -45,6 +46,7 @@ func GrafanaCom(c *cli.Context) error { } version := metadata.GrafanaVersion + semver := versions.ParseSemver(version) if releaseMode.Mode == config.Cronjob { gcs, err := storage.New() if err != nil { @@ -69,7 +71,13 @@ func GrafanaCom(c *cli.Context) error { if grafanaAPIKey == "" { return cli.Exit("the environment variable GRAFANA_COM_API_KEY must be set", 1) } - whatsNewURL, releaseNotesURL, err := getReleaseURLs() + + pkgjson, err := getPackageJSON() + if err != nil { + return cli.Exit(err.Error(), 1) + } + + whatsNewURL, releaseNotesURL, err := getReleaseURLs(semver, pkgjson) if err != nil { return cli.Exit(err.Error(), 1) } @@ -97,25 +105,32 @@ func GrafanaCom(c *cli.Context) error { return nil } -func getReleaseURLs() (string, string, error) { - type grafanaConf struct { - WhatsNewURL string `json:"whatsNewUrl"` - ReleaseNotesURL string `json:"releaseNotesUrl"` - } - type packageConf struct { - Grafana grafanaConf `json:"grafana"` - } +type grafanaConf struct { + WhatsNewURL string `json:"whatsNewUrl"` + ReleaseNotesURL string `json:"releaseNotesUrl"` +} +type packageConf struct { + Grafana grafanaConf `json:"grafana"` +} + +func getPackageJSON() (*packageConf, error) { pkgB, err := os.ReadFile("package.json") if err != nil { - return "", "", fmt.Errorf("failed to read package.json: %w", err) + return nil, fmt.Errorf("failed to read package.json: %w", err) } var pconf packageConf if err := json.Unmarshal(pkgB, &pconf); err != nil { - return "", "", fmt.Errorf("failed to decode package.json: %w", err) + return nil, fmt.Errorf("failed to decode package.json: %w", err) } - if _, err := url.ParseRequestURI(pconf.Grafana.WhatsNewURL); err != nil { + + return &pconf, nil +} + +func getReleaseURLs(semver versions.Semver, pconf *packageConf) (string, string, error) { + u := fmt.Sprintf(pconf.Grafana.WhatsNewURL, semver.Major, semver.Minor, semver.Patch) + if _, err := url.ParseRequestURI(u); err != nil { return "", "", fmt.Errorf("grafana.whatsNewUrl is invalid in package.json: %q", pconf.Grafana.WhatsNewURL) } if _, err := url.ParseRequestURI(pconf.Grafana.ReleaseNotesURL); err != nil { @@ -123,7 +138,7 @@ func getReleaseURLs() (string, string, error) { pconf.Grafana.ReleaseNotesURL) } - return pconf.Grafana.WhatsNewURL, pconf.Grafana.ReleaseNotesURL, nil + return u, pconf.Grafana.ReleaseNotesURL, nil } func Builds(baseURL *url.URL, grafana, version string, packages []packaging.BuildArtifact) ([]GCOMPackage, error) { diff --git a/pkg/build/cmd/grafanacom_test.go b/pkg/build/cmd/grafanacom_test.go index 9f4fbd7c981..1b0eb0bcf20 100644 --- a/pkg/build/cmd/grafanacom_test.go +++ b/pkg/build/cmd/grafanacom_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/build/packaging" + "github.com/grafana/grafana/pkg/build/versions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -258,3 +259,26 @@ func TestBuildsWithPlus(t *testing.T) { }) } } + +func TestReleaseURLs(t *testing.T) { + f := "https://grafana.com/whats-new-in-v%[1]s-%[2]s" + + smv := versions.Semver{ + Major: "1", + Minor: "2", + Patch: "3", + } + + conf := packageConf{ + Grafana: grafanaConf{ + WhatsNewURL: f, + ReleaseNotesURL: "https://example.com", + }, + } + + expect := "https://grafana.com/whats-new-in-v1-2" + + a, _, err := getReleaseURLs(smv, &conf) + require.NoError(t, err) + require.Equal(t, expect, a) +} diff --git a/pkg/build/versions/parse.go b/pkg/build/versions/parse.go index 0a38af4329c..cf53f6230d4 100644 --- a/pkg/build/versions/parse.go +++ b/pkg/build/versions/parse.go @@ -1,6 +1,9 @@ package versions -import "regexp" +import ( + "regexp" + "strings" +) var semverRegex = regexp.MustCompile(`^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) @@ -13,6 +16,7 @@ type Semver struct { } func ParseSemver(version string) Semver { + version = strings.TrimPrefix(version, "v") matches := semverRegex.FindStringSubmatch(version) results := make(map[string]string) for i, name := range semverRegex.SubexpNames() { diff --git a/scripts/build/publish.sh b/scripts/build/publish.sh deleted file mode 100755 index 5da55e36b49..00000000000 --- a/scripts/build/publish.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# no relation to publish.go - -# shellcheck disable=SC2124 - -EXTRA_OPTS="$@" - -# Right now we hack this in into the publish script. -# Eventually we might want to keep a list of all previous releases somewhere. -_releaseNoteUrl="https://community.grafana.com/t/release-notes-v7-0-x/29381" -_whatsNewUrl="https://grafana.com/docs/grafana/latest/guides/whats-new-in-v7-0/" - -./scripts/build/release_publisher/release_publisher \ - --wn "${_whatsNewUrl}" \ - --rn "${_releaseNoteUrl}" \ - --version "${CIRCLE_TAG}" \ - --apikey "${GRAFANA_COM_API_KEY}" "${EXTRA_OPTS}" diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go deleted file mode 100644 index 31c2daf15ca..00000000000 --- a/scripts/build/release_publisher/externalrelease.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "strings" - "time" -) - -type releaseFromExternalContent struct { - getter urlGetter - rawVersion string - artifactConfigurations []buildArtifact -} - -func (re releaseFromExternalContent) prepareRelease(baseArchiveURL, whatsNewURL string, releaseNotesURL string, nightly bool) (*release, error) { - version := re.rawVersion[1:] - beta := strings.Contains(version, "beta") - var rt releaseType - if beta { - rt = BETA - } else if nightly { - rt = NIGHTLY - } else { - rt = STABLE - } - - builds := []build{} - for _, ba := range re.artifactConfigurations { - url := ba.getURL(baseArchiveURL, version, rt) - sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", url)) - if err != nil { - return nil, err - } - builds = append(builds, newBuild(url, ba, sha256)) - } - - r := release{ - Version: version, - ReleaseDate: time.Now().UTC(), - Stable: rt.stable(), - Beta: rt.beta(), - Nightly: rt.nightly(), - WhatsNewURL: whatsNewURL, - ReleaseNotesURL: releaseNotesURL, - Builds: builds, - } - return &r, nil -} - -type urlGetter interface { - getContents(url string) (string, error) -} - -type getHTTPContents struct{} - -func (getHTTPContents) getContents(url string) (string, error) { - response, err := http.Get(url) - if err != nil { - return "", err - } - - defer response.Body.Close() - all, err := io.ReadAll(response.Body) - if err != nil { - return "", err - } - - return string(all), nil -} diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go deleted file mode 100644 index 97e70f5465f..00000000000 --- a/scripts/build/release_publisher/main.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "os" -) - -func main() { - var version string - var whatsNewURL string - var releaseNotesURL string - var dryRun bool - var enterprise bool - var nightly bool - var apiKey string - - flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)") - flag.StringVar(&whatsNewURL, "wn", "", "What's new url (ex: --wn http://docs.grafana.org/guides/whats-new-in-v5-2/)") - flag.StringVar(&releaseNotesURL, "rn", "", "Grafana version (ex: --rn https://community.grafana.com/t/release-notes-v5-2-x/7894)") - flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)") - flag.BoolVar(&dryRun, "dry-run", false, "--dry-run") - flag.BoolVar(&enterprise, "enterprise", false, "--enterprise") - flag.BoolVar(&nightly, "nightly", false, "--nightly (default: false)") - flag.Parse() - - if len(os.Args) == 1 { - fmt.Println("Usage: go run publisher.go main.go --version --wn --rn --apikey --dry-run false --enterprise false --nightly false") - fmt.Println("example: go run publisher.go main.go --version v5.2.0-beta2 --wn http://docs.grafana.org/guides/whats-new-in-v5-2/ --rn https://community.grafana.com/t/release-notes-v5-2-x/7894 --apikey ASDF123 --dry-run --enterprise") - os.Exit(1) - } - - if dryRun { - log.Println("Dry-run has been enabled.") - } - var baseURL string - var builder releaseBuilder - var product string - - archiveProviderRoot := "https://dl.grafana.com" - buildArtifacts := completeBuildArtifactConfigurations - - if enterprise { - product = "grafana-enterprise" - baseURL = createBaseURL(archiveProviderRoot, "enterprise", product, nightly) - } else { - product = "grafana" - baseURL = createBaseURL(archiveProviderRoot, "oss", product, nightly) - } - - builder = releaseFromExternalContent{ - getter: getHTTPContents{}, - rawVersion: version, - artifactConfigurations: buildArtifacts, - } - - p := publisher{ - apiKey: apiKey, - apiURI: "https://grafana.com/api", - product: product, - dryRun: dryRun, - enterprise: enterprise, - baseArchiveURL: baseURL, - builder: builder, - } - if err := p.doRelease(whatsNewURL, releaseNotesURL, nightly); err != nil { - log.Fatalf("error: %v", err) - } -} -func createBaseURL(root string, bucketName string, product string, nightly bool) string { - var subPath string - if nightly { - subPath = "main" - } else { - subPath = "release" - } - - return fmt.Sprintf("%s/%s/%s/%s", root, bucketName, subPath, product) -} diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go deleted file mode 100644 index d31d161d21a..00000000000 --- a/scripts/build/release_publisher/publisher_test.go +++ /dev/null @@ -1,201 +0,0 @@ -package main - -import ( - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPreparingReleaseFromRemote(t *testing.T) { - cases := []struct { - version string - expectedVersion string - whatsNewURL string - relNotesURL string - nightly bool - expectedBeta bool - expectedStable bool - expectedArch string - expectedOs string - expectedURL string - baseArchiveURL string - buildArtifacts []buildArtifact - }{ - { - version: "v5.2.0-beta1", - expectedVersion: "5.2.0-beta1", - whatsNewURL: "https://whatsnews.foo/", - relNotesURL: "https://relnotes.foo/", - nightly: false, - expectedBeta: true, - expectedStable: false, - expectedArch: "amd64", - expectedOs: "linux", - expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.0-beta1.linux-amd64.tar.gz", - baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", - buildArtifacts: []buildArtifact{{"linux", "amd64", ".linux-amd64.tar.gz", ""}}, - }, - { - version: "v5.2.3", - expectedVersion: "5.2.3", - whatsNewURL: "https://whatsnews.foo/", - relNotesURL: "https://relnotes.foo/", - nightly: false, - expectedBeta: false, - expectedStable: true, - expectedArch: "amd64", - expectedOs: "rhel", - expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.3-1.x86_64.rpm", - baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", - buildArtifacts: []buildArtifact{{"rhel", "amd64", ".x86_64.rpm", ""}}, - }, - { - version: "v5.4.0-pre1asdf", - expectedVersion: "5.4.0-pre1asdf", - whatsNewURL: "https://whatsnews.foo/", - relNotesURL: "https://relnotes.foo/", - nightly: true, - expectedBeta: false, - expectedStable: false, - expectedArch: "amd64", - expectedOs: "rhel", - expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.4.0~pre1asdf-1.x86_64.rpm", - baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", - buildArtifacts: []buildArtifact{{"rhel", "amd64", ".x86_64.rpm", ""}}, - }, - { - version: "v5.4.0-pre1asdf", - expectedVersion: "5.4.0-pre1asdf", - whatsNewURL: "https://whatsnews.foo/", - relNotesURL: "https://relnotes.foo/", - nightly: true, - expectedBeta: false, - expectedStable: false, - expectedArch: "armv6", - expectedOs: "deb", - expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-rpi_5.4.0~pre1asdf_armhf.deb", - baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", - buildArtifacts: []buildArtifact{ - {os: "deb", arch: "armv6", urlPostfix: "_armhf.deb", packagePostfix: "-rpi"}, - }, - }, - { - version: "v5.4.0-pre1asdf", - expectedVersion: "5.4.0-pre1asdf", - whatsNewURL: "https://whatsnews.foo/", - relNotesURL: "https://relnotes.foo/", - nightly: true, - expectedBeta: false, - expectedStable: false, - expectedArch: "amd64", - expectedOs: "win-installer", - expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.4.0-pre1asdf.windows-amd64.msi", - baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", - buildArtifacts: []buildArtifact{{"win-installer", "amd64", ".windows-amd64.msi", ""}}, - }, - { - version: "v5.4.0-pre1asdf", - expectedVersion: "5.4.0-pre1asdf", - whatsNewURL: "https://whatsnews.foo/", - relNotesURL: "https://relnotes.foo/", - nightly: true, - expectedBeta: false, - expectedStable: false, - expectedArch: "amd64", - expectedOs: "win", - expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.4.0-pre1asdf.windows-amd64.zip", - baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", - buildArtifacts: []buildArtifact{{"win", "amd64", ".windows-amd64.zip", ""}}, - }, - } - - for _, test := range cases { - builder := releaseFromExternalContent{ - getter: mockHTTPGetter{}, - rawVersion: test.version, - artifactConfigurations: test.buildArtifacts, - } - - t.Log("Preparing release", "baseArchiveURL", test.baseArchiveURL, "nightly", test.nightly) - rel, err := builder.prepareRelease(test.baseArchiveURL, test.whatsNewURL, test.relNotesURL, test.nightly) - require.NoError(t, err) - - assert.Equal(t, test.expectedBeta, rel.Beta) - assert.Equal(t, test.expectedStable, rel.Stable) - assert.Equal(t, test.expectedVersion, rel.Version) - - assert.Len(t, rel.Builds, len(test.buildArtifacts)) - - build := rel.Builds[0] - assert.Equal(t, test.expectedArch, build.Arch) - assert.Equal(t, test.expectedOs, build.Os) - assert.Equal(t, test.expectedURL, build.URL) - } -} - -type mockHTTPGetter struct{} - -func (mockHTTPGetter) getContents(url string) (string, error) { - return url, nil -} - -func TestFilterBuildArtifacts(t *testing.T) { - buildArtifacts, _ := filterBuildArtifacts(completeBuildArtifactConfigurations, Add, []artifactFilter{ - {os: "deb", arch: "amd64"}, - {os: "rhel", arch: "amd64"}, - {os: "linux", arch: "amd64"}, - {os: "win", arch: "amd64"}, - }) - - if len(buildArtifacts) != 4 { - t.Errorf("Expected 4 build artifacts after filtering, but was %v", len(buildArtifacts)) - } - - buildArtifacts, err := filterBuildArtifacts([]buildArtifact{ - { - os: "linux", - arch: "amd64", - }, - { - os: "arm", - arch: "amd64", - }, - { - os: "darwin", - arch: "amd64", - }, - }, Remove, []artifactFilter{ - {os: "darwin", arch: "amd64"}, - }) - - if err != nil { - t.Error() - } - - if len(buildArtifacts) != 2 { - t.Errorf("Expected 2 artifacts, was %v", len(buildArtifacts)) - } - - for _, ba := range buildArtifacts { - if ba.arch == "amd64" && ba.os == "darwin" { - t.Errorf("darwin/amd64 should be gone due to filtering") - } - } - - left := []buildArtifact{ - { - os: "linux", - arch: "amd64", - }, - { - os: "arm", - arch: "amd64", - }, - } - - if !reflect.DeepEqual(left, buildArtifacts) { - t.Errorf("Lists should have been equal but was, expected=%v, actual=%v", left, buildArtifacts) - } -} diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 deleted file mode 100644 index c3068040269..00000000000 --- a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz.sha256 +++ /dev/null @@ -1 +0,0 @@ -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.msi b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.msi deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.msi.sha256 b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.msi.sha256 deleted file mode 100644 index c3068040269..00000000000 --- a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.msi.sha256 +++ /dev/null @@ -1 +0,0 @@ -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 deleted file mode 100644 index c3068040269..00000000000 --- a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip.sha256 +++ /dev/null @@ -1 +0,0 @@ -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 b/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 deleted file mode 100644 index c3068040269..00000000000 --- a/scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256 +++ /dev/null @@ -1 +0,0 @@ -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb b/scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 b/scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 deleted file mode 100644 index c3068040269..00000000000 --- a/scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256 +++ /dev/null @@ -1 +0,0 @@ -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/scripts/drone/pipelines/whats_new_checker.star b/scripts/drone/pipelines/whats_new_checker.star deleted file mode 100644 index 2c56be573ce..00000000000 --- a/scripts/drone/pipelines/whats_new_checker.star +++ /dev/null @@ -1,42 +0,0 @@ -""" -This module contains logic for checking if the package.json whats new url matches with the in-flight tag. -""" - -load( - "scripts/drone/steps/lib.star", - "compile_build_cmd", -) -load( - "scripts/drone/utils/images.star", - "images", -) -load( - "scripts/drone/utils/utils.star", - "pipeline", -) - -def whats_new_checker_step(): - return { - "name": "whats-new-checker", - "image": images["go"], - "depends_on": [ - "compile-build-cmd", - ], - "commands": [ - "./bin/build whatsnew-checker", - ], - } - -def whats_new_checker_pipeline(trigger): - environment = {"EDITION": "oss"} - steps = [ - compile_build_cmd(), - whats_new_checker_step(), - ] - return pipeline( - name = "release-whatsnew-checker", - trigger = trigger, - services = [], - steps = steps, - environment = environment, - ) diff --git a/scripts/drone/rgm.star b/scripts/drone/rgm.star index 61694556c37..f81effc892a 100644 --- a/scripts/drone/rgm.star +++ b/scripts/drone/rgm.star @@ -16,10 +16,6 @@ load( "scripts/drone/pipelines/test_frontend.star", "test_frontend", ) -load( - "scripts/drone/pipelines/whats_new_checker.star", - "whats_new_checker_pipeline", -) load( "scripts/drone/steps/github.star", "github_app_generate_token_step", @@ -291,7 +287,6 @@ def rgm_tag_pipeline(): return [ build, - whats_new_checker_pipeline(tag_trigger), verify_release_pipeline( trigger = tag_trigger, name = "rgm-tag-verify-prerelease-assets", From ab754bc6bd1ed201bc1ae7ac412a6a2a968c681d Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Thu, 20 Feb 2025 19:14:02 +0000 Subject: [PATCH 27/27] Rename Explore Apps to App Drilldown (#101096) --- docs/sources/datasources/pyroscope/_index.md | 2 +- .../configure-pyroscope-data-source.md | 2 +- .../pyroscope/query-profile-data.md | 22 +++++------ .../datasources/tempo/query-editor/_index.md | 2 +- .../explore/simplified-exploration/_index.md | 16 ++++---- .../simplified-exploration/metrics/index.md | 39 +++++++++---------- .../feature-toggles/index.md | 8 ++-- 7 files changed, 45 insertions(+), 46 deletions(-) diff --git a/docs/sources/datasources/pyroscope/_index.md b/docs/sources/datasources/pyroscope/_index.md index 069902bfc0a..60efe8f3f33 100644 --- a/docs/sources/datasources/pyroscope/_index.md +++ b/docs/sources/datasources/pyroscope/_index.md @@ -56,7 +56,7 @@ To use profiling data, you should: - [Configure your application to send profiles](/docs/pyroscope//configure-client/) - [Configure the Grafana Pyroscope data source](./configure-pyroscope-data-source/). -- [View and query profiling data using Explore Profiles or the query editor ](./query-profile-data/) +- [View and query profiling data using Profiles Drilldown or the query editor ](./query-profile-data/) ## Continuous profiling diff --git a/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md b/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md index 6f6b30ad045..0acfd01df3c 100644 --- a/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md +++ b/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md @@ -104,7 +104,7 @@ If you are running a self-managed Grafana instance or need to configure an addit The connection URL for a self-managed Pyroscope backend depends on how Pyroscope is deployed. Refer to the steps under [Query profiles in Grafana](https://grafana.com/docs/pyroscope//deploy-kubernetes/helm/#query-profiles-in-grafana) for more information on how to configure the data source. -If you plan to use the [Explore Profiles](ref:explore-profiles) application and you are running a self-managed Pyroscope backend in microservices mode, the data source connection URL should point to a gateway or proxy that routes requests to the corresponding Pyroscope service. +If you plan to use the [Profiles Drilldown](ref:explore-profiles) application and you are running a self-managed Pyroscope backend in microservices mode, the data source connection URL should point to a gateway or proxy that routes requests to the corresponding Pyroscope service. Refer to the [Helm ingress configuration](https://github.com/grafana/pyroscope/blob/main/operations/pyroscope/helm/pyroscope/templates/ingress.yaml) for specific routing requirements. ## Authentication diff --git a/docs/sources/datasources/pyroscope/query-profile-data.md b/docs/sources/datasources/pyroscope/query-profile-data.md index 5f391efe0b7..d0917ccfe12 100644 --- a/docs/sources/datasources/pyroscope/query-profile-data.md +++ b/docs/sources/datasources/pyroscope/query-profile-data.md @@ -1,5 +1,5 @@ --- -description: Explore your profiling data using Explore Profiles or the Pyroscope query editor. +description: Explore your profiling data using Profiles Drilldown or the Pyroscope query editor. keywords: - query - profiling @@ -54,22 +54,22 @@ https://grafana.com/docs/grafana-cloud/visualizations/panels-visualizations/visu # Query profile data -You can query your profile data using the open source Grafana Explore Profiles app or the built-in Grafana Pyroscope data source query editor. +You can query your profile data using the open source Grafana Profiles Drilldown app or the built-in Grafana Pyroscope data source query editor. -- Explore Profiles provides a queryless experience for inspecting your profiling data with opinionated workflows to assist your investigation. +- Profiles Drilldown provides a queryless experience for inspecting your profiling data with opinionated workflows to assist your investigation. - Data source query editor provides complete control over your data exploration and is recommended for experienced users. In addition, you can embed flame graph panels in Grafana dashboards. Refer to the [Flame graph panel](ref:flame-graph-panel) documentation for details. -## Explore Profiles +## Profiles Drilldown -[Explore Profiles](ref:explore-profiles) is a native Grafana application designed to integrate seamlessly with Pyroscope, the open source continuous profiling platform, providing a smooth, queryless experience for browsing and analyzing profiling data. +[Grafana Profiles Drilldown](ref:explore-profiles) is a native Grafana application designed to integrate seamlessly with Pyroscope, the open source continuous profiling platform, providing a smooth, queryless experience for browsing and analyzing profiling data. -You can use Explore Profiles in Grafana Cloud or in your own Grafana instance. -For more information, refer to [Access or install Explore Profiles](ref:explore-profiles-install). +You can use Profiles Drilldown in Grafana Cloud or in your own Grafana instance. +For more information, refer to [Access or install Profiles Drilldown](ref:explore-profiles-install). -![Explore Profiles home screen](/media/docs/explore-profiles/explore-profiles-homescreen-v0.1.17.png) +![Profiles Drilldown home screen](/media/docs/explore-profiles/explore-profiles-homescreen-v0.1.17.png) ### Use cases @@ -80,10 +80,10 @@ The main use cases are the following: - Proactive: Cutting costs, addressing latency issues, or optimizing memory usage for applications - Reactive: Resolving incidents with line-level accuracy or debugging active latency/memory issues -Explore Profiles provides an intuitive interface to specifically support proactivee and reactive use cases. +Profiles Drilldown provides an intuitive interface to specifically support proactivee and reactive use cases. You get a holistic view of all of your services and how they're functioning, but also the ability to drill down for more targeted root cause analysis. -Explore Profiles offers a convenient platform to analyze profiles and get insights that are impossible to get from using other traditional signals like logs, metrics, or tracing. +Profiles Drilldown offers a convenient platform to analyze profiles and get insights that are impossible to get from using other traditional signals like logs, metrics, or tracing. {{< youtube id="x9aPw_CbIQc" >}} @@ -93,7 +93,7 @@ Explore Profiles offers a convenient platform to analyze profiles and get insigh The Pyroscope data source query editor gives you access to a profile type selector, a label selector, and collapsible options. -Like Explore Profiles, the query editor also provides a flame graph to visualize data. +Like Profiles Drilldown, the query editor also provides a flame graph to visualize data. ![Query editor](/media/docs/pyroscope/query-editor/query-editor.png 'Query editor') diff --git a/docs/sources/datasources/tempo/query-editor/_index.md b/docs/sources/datasources/tempo/query-editor/_index.md index b6aa5c0f49b..a6b85cfd436 100644 --- a/docs/sources/datasources/tempo/query-editor/_index.md +++ b/docs/sources/datasources/tempo/query-editor/_index.md @@ -61,7 +61,7 @@ The queries use [TraceQL](/docs/tempo/latest/traceql), the query language design For general documentation on querying data sources in Grafana, refer to [Query and transform data](ref:query-transform-data). {{< admonition type="tip" >}} -Don't know TraceQL? Try [Explore Traces](ref:explore-traces-app), an intuitive, queryless app that lets you explore your tracing data using RED metrics. +Don't know TraceQL? Try [Grafana Traces Drilldown](ref:explore-traces-app), an intuitive, queryless app that lets you explore your tracing data using RED metrics. {{< /admonition >}} ## Before you begin diff --git a/docs/sources/explore/simplified-exploration/_index.md b/docs/sources/explore/simplified-exploration/_index.md index 60612ae02ba..bf43b04d41e 100644 --- a/docs/sources/explore/simplified-exploration/_index.md +++ b/docs/sources/explore/simplified-exploration/_index.md @@ -8,27 +8,27 @@ title: Simplified exploration menuTitle: Simplified exploration weight: 100 hero: - title: Simplified exploration with the Explore apps + title: Simplified exploration with the Drilldown apps level: 1 width: 100 height: 100 - description: Use Explore Profiles to investigate and identify issues using profiling data. + description: Use the Drilldown apps to investigate and identify issues using telemetry data. cards: title_class: pt-0 lh-1 items: - - title: Explore Metrics + - title: Grafana Metrics Drilldown href: ./metrics/ description: Quickly find related metrics with a few clicks, without needing to write PromQL queries to retrieve metrics. height: 24 - - title: Explore Logs + - title: Grafana Logs Drilldown href: ./logs/ description: Visualize log volumes to easily detect anomalies or significant changes over time, without needing to compose LogQL queries. height: 24 - - title: Explore Traces + - title: Grafana Traces Drilldown href: ./traces/ description: Use Rate, Errors, and Duration (RED) metrics derived from traces to investigate and understand errors and latency issues within complex distributed systems. height: 24 - - title: Explore Profiles + - title: Grafana Profiles Drilldown href: ./profiles/ description: View and analyze high-level service performance, identify problem processes for optimization, and diagnose issues to determine root causes. height: 24 @@ -36,14 +36,14 @@ cards: # Simplified exploration -The Grafana Explore apps are designed for effortless data exploration through intuitive, queryless interactions. +The Grafana Drilldown apps are designed for effortless data exploration through intuitive, queryless interactions. Easily explore telemetry signals with these specialized tools, tailored specifically for the Grafana databases to provide quick and accurate insights. To learn more, read: - [From multi-line queries to no-code investigations: meeting Grafana users where they are](https://grafana.com/blog/2024/10/22/from-multi-line-queries-to-no-code-investigations-meeting-grafana-users-where-they-are/) -- [A queryless experience for exploring metrics, logs, traces, and profiles: Introducing the Explore apps suite for Grafana](https://grafana.com/blog/2024/09/24/queryless-metrics-logs-traces-profiles/). +- [A queryless experience for exploring metrics, logs, traces, and profiles: Introducing the Drilldown apps suite for Grafana](https://grafana.com/blog/2024/09/24/queryless-metrics-logs-traces-profiles/). {{< youtube id="MSHeWWsHaIA" >}} diff --git a/docs/sources/explore/simplified-exploration/metrics/index.md b/docs/sources/explore/simplified-exploration/metrics/index.md index 127dd6cb265..a9d5d291772 100644 --- a/docs/sources/explore/simplified-exploration/metrics/index.md +++ b/docs/sources/explore/simplified-exploration/metrics/index.md @@ -4,19 +4,19 @@ labels: - cloud - enterprise - oss -title: Explore Metrics +title: Metrics Drilldown aliases: - ../explore-metrics/ # /docs/grafana/latest/explore/explore-metrics/ canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/metrics/ -description: Explore Metrics lets you browse Prometheus-compatible metrics using an intuitive, queryless experience. +description: Grafana Metrics Drilldown lets you browse Prometheus-compatible metrics using an intuitive, queryless experience. weight: 200 --- -# Grafana Explore Metrics +# Grafana Metrics Drilldown -Grafana Explore Metrics is a query-less experience for browsing **Prometheus-compatible** metrics. Quickly find related metrics with just a few simple clicks, without needing to write PromQL queries to retrieve metrics. +Grafana Metrics Drilldown is a query-less experience for browsing **Prometheus-compatible** metrics. Quickly find related metrics with just a few simple clicks, without needing to write PromQL queries to retrieve metrics. -With Explore Metrics, you can: +With Grafana Metrics Drilldown, you can: - Easily segment metrics based on their labels, so you can immediately spot anomalies and identify issues. - Automatically display the optimal visualization for each metric type (gauge vs. counter, for example) without manual setup. @@ -25,26 +25,25 @@ With Explore Metrics, you can: - View a history of user steps when navigating through metrics and their filters. - Seamlessly pivot to related telemetry, including log data. -{{< docs/play title="Explore Metrics" url="https://play.grafana.org/explore/metrics/trail?from=now-1h&to=now&var-ds=grafanacloud-demoinfra-prom&var-filters=&refresh=&metricPrefix=all" >}} +{{< docs/play title="Grafana Metrics Drilldown" url="https://play.grafana.org/explore/metrics/trail?from=now-1h&to=now&var-ds=grafanacloud-demoinfra-prom&var-filters=&refresh=&metricPrefix=all" >}} -You can access Explore Metrics either as a standalone experience or as part of Grafana dashboards. +You can access Grafana Metrics Drilldown either as a standalone experience or as part of Grafana dashboards. ## Standalone experience -To access Explore Metrics as a standalone experience: +To access Grafana Metrics Drilldown as a standalone experience: -1. Click the arrow next to **Explore** in the Grafana left-side menu and click **Metrics**. You are taken to an overview page that shows recent metrics, bookmarks, and the option to select a new metric exploration. -1. To get started with a new exploration, click **+ New metric exploration**. +1. Click the arrow next to **Drilldown** in the Grafana left-side menu and click **Metrics**. You are taken to an overview page that shows recent metrics, bookmarks, and the option to select a new metric exploration. +1. To get started with a new exploration, click **Let's start!**. 1. Select **Prometheus** or any Prometheus-compatible data source available in the drop-down menu under **Data source**. -1. Click **+ Add label** to select a label-value pair from the drop-down menu. You can add multiple label-value pairs. A label type will appear above the selected label with a drop-down list of options from which to choose. For example, if you select the label `container` a drop-down list of available containers appears. +1. Click **+ Add label** to select a label-value pair from the drop-down menu. You can add multiple label-value pairs. A label type appears above the selected label with a drop-down list of options from which to choose. For example, if you select the label `container` a drop-down list of available containers appears. 1. You can also search for metrics using keywords under **Search metrics** in the search bar. 1. Use the time picker to select a date and time range from the drop-down menu or use an absolute time range. 1. Click the down arrow next to the **Refresh** icon to set a refresh rate from the drop-down menu. The default is `Off`. -1. Click the **Settings** icon and toggle **Always keep selected metric graph in-view** to keep your main graph always in view on the Breakdown drill-down tab. The **History** button in the upper left corner tracks every step navigating through metric exploration. -![show metrics explore overview](/media/metrics-explore/metrics-explore-overview.png) +![show metrics explore overview](/media/metrics-explore/metrics-drilldown-overview.png) ### Metrics exploration @@ -54,19 +53,19 @@ To further explore a metric, click **Select** in the upper right corner of the m - The **Overview** tab provides a description for each metric, as well as the metric `type` and `unit` associated with the metric. It also provides a list of labels associated with the metric. Click on any label to view drill-down visualizations. - The **Breakdown** tab depicts time series visualizations for each of the label-value pairs for the selected metric. You can further drill down on each label and click **Add to filter** to add the label/value pair into your filters. You can also change the **View** from grid to rows. -- The **Related metrics** tab depicts related metrics with relevant key words. You can repeat the drill down process for any related metric. Toggle **SHow previews** to preview visualizations. +- The **Related metrics** tab depicts related metrics with relevant key words. You can repeat the drill down process for any related metric. Toggle **Show previews** to preview visualizations. -Once you have gathered your metrics exploration data you can: +After you have gathered your metrics exploration data you can: -- Click the **Explore** icon on the right side to open the graph in Explore, where you can modify the query or add the graph to a dashboard or incident. -- Click the **Share** icon on the right side to copy the metric drill down URL to the clipboard so it can be shared. +- Click the **Open in Explore** icon on the right side to open the graph in Explore, where you can modify the query or add the graph to a dashboard or incident. +- Click the **Copy URL** icon on the right side to copy the metric drill down URL to the clipboard so it can be shared. - Click the **Star** icon on the right side to bookmark and save the metrics exploration. ## Dashboard experience -To access Explore Metrics via a dashboard: +To access Grafana Metrics Drilldown via a dashboard: 1. Navigate to your dashboard. 1. Select a time series panel. -1. Click the panel menu in the upper right and select **Explore Metrics**. If there are multiple metrics, click on the one you want to explore. -1. You will see a slide out drawer with the Metrics Experience, starting with the drill down. You can access the standalone experience by clicking **Open** in the upper right. +1. Click the panel menu in the upper right and select **Metrics Drilldown**. If there are multiple metrics, click on the one you want to explore. +1. You see a slide out drawer with the Metrics Experience, starting with the drill down. You can access the standalone experience by clicking **Open** in the upper right. 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 a3e28d65042..716f2ace6f9 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -56,7 +56,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | | `ssoSettingsApi` | Enables the SSO settings API and the OAuth configuration UIs in Grafana | Yes | | `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes | -| `exploreMetrics` | Enables the new Explore Metrics core app | Yes | +| `exploreMetrics` | Enables the new Grafana Metrics Drilldown core app | Yes | | `alertingSimplifiedRouting` | Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule | Yes | | `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes | | `lokiQueryHints` | Enables query hints for Loki | Yes | @@ -200,9 +200,9 @@ Experimental features might be changed or removed without prior notice. | `dataplaneAggregator` | Enable grafana dataplane aggregator | | `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | | `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | -| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | -| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | -| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | +| `exploreLogsShardSplitting` | Used in Logs Drilldown to split queries into multiple queries based on the number of shards | +| `exploreLogsAggregatedMetrics` | Used in Logs Drilldown to query by aggregated metrics | +| `exploreLogsLimitedTimeRange` | Used in Logs Drilldown to limit the time range | | `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | | `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | | `rolePickerDrawer` | Enables the new role picker drawer design |