From 3056e8310bc0964cddf6ac8c8c92d3a04a11dcba Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:06:35 +0300 Subject: [PATCH 01/11] DashboardScenes: Skip flaky scenes E2E tests (#89769) skip flaky tests --- e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts | 3 ++- e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts | 3 ++- e2e/scenes/various-suite/prometheus-editor.spec.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts index b91ccf5c270..089b3ec4beb 100644 --- a/e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts +++ b/e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts @@ -1,7 +1,8 @@ import testDashboard from '../dashboards/DashboardLiveTest.json'; import { e2e } from '../utils'; -describe('Dashboard Live streaming support', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/dashboards-suite/dashboard-live-streaming.spec.ts +describe.skip('Dashboard Live streaming support', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.importDashboard(testDashboard, 1000); diff --git a/e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts b/e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts index 2881e7dfe5d..40ec3eace50 100644 --- a/e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts +++ b/e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts @@ -6,7 +6,8 @@ const DATASOURCE_ID = 'sandbox-test-datasource'; let DATASOURCE_CONNECTION_ID = ''; const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance'; -describe('Datasource sandbox', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/frontend-sandbox-datasource.spec.ts +describe.skip('Datasource sandbox', () => { before(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); diff --git a/e2e/scenes/various-suite/prometheus-editor.spec.ts b/e2e/scenes/various-suite/prometheus-editor.spec.ts index a35ca282e50..ffe8eee9894 100644 --- a/e2e/scenes/various-suite/prometheus-editor.spec.ts +++ b/e2e/scenes/various-suite/prometheus-editor.spec.ts @@ -43,8 +43,8 @@ function navigateToEditor(editorType: editorType, name: string): void { e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(name).scrollIntoView().should('be.visible').click(); } - -describe('Prometheus query editor', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/prometheus-editor.spec.ts +describe.skip('Prometheus query editor', () => { it('should have a kickstart component', () => { navigateToEditor('Code', 'prometheus'); e2e.components.QueryBuilder.queryPatterns().scrollIntoView().should('exist'); From 16b050f676f4583f2218fde95ac2fe6b5605e37c Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:21:10 -0500 Subject: [PATCH 02/11] CI: fix release comms workflow (#89744) * add runs-on key * remove unneeded env key * use more specific quotes in commands * fix block syntax * fix workflow dispatch event check --- .github/workflows/release-comms.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-comms.yml b/.github/workflows/release-comms.yml index 8e5e862ed61..a62d04a78c0 100644 --- a/.github/workflows/release-comms.yml +++ b/.github/workflows/release-comms.yml @@ -19,9 +19,9 @@ on: jobs: post_release: name: Post-release comms - env: + runs-on: ubuntu-latest steps: - - if: github.event.workflow_dispatch + - if: github.event_name == 'workflow_dispatch' run: | echo "VERSION=${{ inputs.version }}" >> $GITHUB_ENV echo "DRY_RUN=${{ inputs.dry_run }}" >> $GITHUB_ENV @@ -29,8 +29,13 @@ jobs: run: | echo "VERSION=$(echo ${{ github.head_ref }} | sed -e 's/release\///g')" >> $GITHUB_ENV echo "DRY_RUN=false" >> $GITHUB_ENV - - run: "echo push-grafana-tag $VERSION (dry run: $DRY_RUN)" - - run: "echo post changelog to forums for $VERSION (dry run: $DRY_RUN)" - - run: "echo create github release" - - run: "echo publish docs for $VERSION (dry run: $DRY_RUN)" - - run: "announce on slack that $VERSION has been released (dry run: $DRY_RUN)" + - run: | + echo "push-grafana-tag ${VERSION} (dry run: ${DRY_RUN})" + - run: | + echo "post changelog to forums for ${VERSION} (dry run: ${DRY_RUN})" + - run: | + echo "create github release for tag ${VERSION} (dry run: ${DRY_RUN})" + - run: | + echo "publish docs for ${VERSION} (dry run: ${DRY_RUN})" + - run: | + echo "announce on slack that ${VERSION} has been released (dry run: ${DRY_RUN})" From 06d585039683b40c1f449c8edbe206b5e7d6572b Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 26 Jun 2024 10:25:37 -0400 Subject: [PATCH 03/11] Alerting: Update alerting state history API to authorize access using RBAC (#89579) * add method CanReadAllRules to rule authorization service * add alias type Namespace for Folder in ngalert's models package. It implements the Namespacer interface that is used by authz logic * update state history's backends to authorize access to rules. * update Loki to add folders UIDs to query. * Update BuildLogQuery to drop filter by folders if it's too long and fall back to in-memory filtering. --- .../annotationsimpl/loki/historian_store.go | 2 +- .../ngalert/accesscontrol/fakes/rules.go | 9 + pkg/services/ngalert/accesscontrol/rules.go | 8 + .../ngalert/accesscontrol/rules_test.go | 51 ++ pkg/services/ngalert/models/alert_rule.go | 7 + pkg/services/ngalert/ngalert.go | 12 +- pkg/services/ngalert/ngalert_test.go | 19 +- .../ngalert/state/historian/annotation.go | 23 +- .../state/historian/annotation_test.go | 39 +- pkg/services/ngalert/state/historian/loki.go | 134 ++++- .../ngalert/state/historian/loki_test.go | 473 +++++++++++++++--- .../ngalert/state/manager_bench_test.go | 4 +- pkg/services/ngalert/state/manager_test.go | 7 +- 13 files changed, 699 insertions(+), 89 deletions(-) diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store.go b/pkg/services/annotations/annotationsimpl/loki/historian_store.go index 3fd0d7ca907..abac7aadcc2 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store.go @@ -99,7 +99,7 @@ func (r *LokiHistorianStore) Get(ctx context.Context, query *annotations.ItemQue } } - logQL, err := historian.BuildLogQuery(buildHistoryQuery(query, accessResources.Dashboards, rule.UID), r.client.MaxQuerySize()) + logQL, _, err := historian.BuildLogQuery(buildHistoryQuery(query, accessResources.Dashboards, rule.UID), nil, r.client.MaxQuerySize()) if err != nil { grafanaErr := errutil.Error{} if errors.As(err, &grafanaErr) { diff --git a/pkg/services/ngalert/accesscontrol/fakes/rules.go b/pkg/services/ngalert/accesscontrol/fakes/rules.go index 52989e44eab..c1dc569848d 100644 --- a/pkg/services/ngalert/accesscontrol/fakes/rules.go +++ b/pkg/services/ngalert/accesscontrol/fakes/rules.go @@ -24,6 +24,7 @@ type FakeRuleService struct { HasAccessInFolderFunc func(context.Context, identity.Requester, models.Namespaced) (bool, error) AuthorizeAccessInFolderFunc func(context.Context, identity.Requester, models.Namespaced) error AuthorizeRuleChangesFunc func(context.Context, identity.Requester, *store.GroupDelta) error + CanReadAllRulesFunc func(context.Context, identity.Requester) (bool, error) Calls []Call } @@ -99,3 +100,11 @@ func (s *FakeRuleService) AuthorizeRuleChanges(ctx context.Context, user identit } return nil } + +func (s *FakeRuleService) CanReadAllRules(ctx context.Context, user identity.Requester) (bool, error) { + s.Calls = append(s.Calls, Call{"CanReadAllRules", []interface{}{ctx, user}}) + if s.CanReadAllRulesFunc != nil { + return s.CanReadAllRulesFunc(ctx, user) + } + return false, nil +} diff --git a/pkg/services/ngalert/accesscontrol/rules.go b/pkg/services/ngalert/accesscontrol/rules.go index 2b8e2fd2914..812cc5816eb 100644 --- a/pkg/services/ngalert/accesscontrol/rules.go +++ b/pkg/services/ngalert/accesscontrol/rules.go @@ -76,6 +76,14 @@ func (r *RuleService) getRulesQueryEvaluator(rules ...*models.AlertRule) accessc return accesscontrol.EvalAll(evals...) } +// CanReadAllRules returns true when user has access to all folders and can read rules in them. +func (r *RuleService) CanReadAllRules(ctx context.Context, user identity.Requester) (bool, error) { + return r.HasAccess(ctx, user, accesscontrol.EvalAll( + accesscontrol.EvalPermission(ruleRead, dashboards.ScopeFoldersProvider.GetResourceAllScope()), + accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceAllScope()), + )) +} + // AuthorizeDatasourceAccessForRule checks that user has access to all data sources declared by the rule func (r *RuleService) AuthorizeDatasourceAccessForRule(ctx context.Context, user identity.Requester, rule *models.AlertRule) error { ds := r.getRulesQueryEvaluator(rule) diff --git a/pkg/services/ngalert/accesscontrol/rules_test.go b/pkg/services/ngalert/accesscontrol/rules_test.go index 5a5a4e17bde..cc85799ba3c 100644 --- a/pkg/services/ngalert/accesscontrol/rules_test.go +++ b/pkg/services/ngalert/accesscontrol/rules_test.go @@ -453,3 +453,54 @@ func Test_authorizeAccessToRuleGroup(t *testing.T) { require.ErrorIs(t, result, ErrAuthorizationBase) }) } + +func TestCanReadAllRules(t *testing.T) { + ac := &recordingAccessControlFake{} + svc := RuleService{ + genericService{ac: ac}, + } + + testCases := []struct { + permissions map[string][]string + expected bool + }{ + { + permissions: map[string][]string{ + ruleRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, + }, + expected: true, + }, + { + permissions: make(map[string][]string), + }, + { + permissions: map[string][]string{ + ruleRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("test")}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, + }, + }, + { + permissions: map[string][]string{ + ruleRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("test")}, + }, + }, + { + permissions: map[string][]string{ + ruleRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, + }, + }, + { + permissions: map[string][]string{ + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, + }, + }, + } + + for _, tc := range testCases { + result, err := svc.CanReadAllRules(context.Background(), createUserWithPermissions(tc.permissions)) + assert.NoError(t, err) + assert.Equalf(t, tc.expected, result, "permissions: %v", tc.permissions) + } +} diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index b379810b579..198153b711d 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -20,6 +20,7 @@ import ( alertingModels "github.com/grafana/alerting/models" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/cmputil" @@ -274,6 +275,12 @@ type Namespaced interface { GetNamespaceUID() string } +type Namespace folder.Folder + +func (n Namespace) GetNamespaceUID() string { + return n.UID +} + // AlertRuleWithOptionals This is to avoid having to pass in additional arguments deep in the call stack. Alert rule // object is created in an early validation step without knowledge about current alert rule fields or if they need to be // overridden. This is done in a later step and, in that step, we did not have knowledge about if a field was optional diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index e5fbb77b581..539cb38fd25 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -366,7 +366,7 @@ func (ng *AlertNG) init() error { // There are a set of feature toggles available that act as short-circuits for common configurations. // If any are set, override the config accordingly. ApplyStateHistoryFeatureToggles(&ng.Cfg.UnifiedAlerting.StateHistory, ng.FeatureToggles, ng.Log) - history, err := configureHistorianBackend(initCtx, ng.Cfg.UnifiedAlerting.StateHistory, ng.annotationsRepo, ng.dashboardService, ng.store, ng.Metrics.GetHistorianMetrics(), ng.Log, ng.tracer) + history, err := configureHistorianBackend(initCtx, ng.Cfg.UnifiedAlerting.StateHistory, ng.annotationsRepo, ng.dashboardService, ng.store, ng.Metrics.GetHistorianMetrics(), ng.Log, ng.tracer, ac.NewRuleService(ng.accesscontrol)) if err != nil { return err } @@ -531,7 +531,7 @@ type Historian interface { state.Historian } -func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingStateHistorySettings, ar annotations.Repository, ds dashboards.DashboardService, rs historian.RuleStore, met *metrics.Historian, l log.Logger, tracer tracing.Tracer) (Historian, error) { +func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingStateHistorySettings, ar annotations.Repository, ds dashboards.DashboardService, rs historian.RuleStore, met *metrics.Historian, l log.Logger, tracer tracing.Tracer, ac historian.AccessControl) (Historian, error) { if !cfg.Enabled { met.Info.WithLabelValues("noop").Set(0) return historian.NewNopHistorian(), nil @@ -546,7 +546,7 @@ func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingS if backend == historian.BackendTypeMultiple { primaryCfg := cfg primaryCfg.Backend = cfg.MultiPrimary - primary, err := configureHistorianBackend(ctx, primaryCfg, ar, ds, rs, met, l, tracer) + primary, err := configureHistorianBackend(ctx, primaryCfg, ar, ds, rs, met, l, tracer, ac) if err != nil { return nil, fmt.Errorf("multi-backend target \"%s\" was misconfigured: %w", cfg.MultiPrimary, err) } @@ -555,7 +555,7 @@ func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingS for _, b := range cfg.MultiSecondaries { secCfg := cfg secCfg.Backend = b - sec, err := configureHistorianBackend(ctx, secCfg, ar, ds, rs, met, l, tracer) + sec, err := configureHistorianBackend(ctx, secCfg, ar, ds, rs, met, l, tracer, ac) if err != nil { return nil, fmt.Errorf("multi-backend target \"%s\" was miconfigured: %w", b, err) } @@ -568,7 +568,7 @@ func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingS if backend == historian.BackendTypeAnnotations { store := historian.NewAnnotationStore(ar, ds, met) annotationBackendLogger := log.New("ngalert.state.historian", "backend", "annotations") - return historian.NewAnnotationBackend(annotationBackendLogger, store, rs, met), nil + return historian.NewAnnotationBackend(annotationBackendLogger, store, rs, met, ac), nil } if backend == historian.BackendTypeLoki { lcfg, err := historian.NewLokiConfig(cfg) @@ -577,7 +577,7 @@ func configureHistorianBackend(ctx context.Context, cfg setting.UnifiedAlertingS } req := historian.NewRequester() lokiBackendLogger := log.New("ngalert.state.historian", "backend", "loki") - backend := historian.NewRemoteLokiBackend(lokiBackendLogger, lcfg, req, met, tracer) + backend := historian.NewRemoteLokiBackend(lokiBackendLogger, lcfg, req, met, tracer, rs, ac) testConnCtx, cancelFunc := context.WithTimeout(ctx, 10*time.Second) defer cancelFunc() diff --git a/pkg/services/ngalert/ngalert_test.go b/pkg/services/ngalert/ngalert_test.go index cb9b9863349..b64dbb5092d 100644 --- a/pkg/services/ngalert/ngalert_test.go +++ b/pkg/services/ngalert/ngalert_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/folder" + acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" @@ -67,8 +68,9 @@ func TestConfigureHistorianBackend(t *testing.T) { Enabled: true, Backend: "invalid-backend", } + ac := &acfakes.FakeRuleService{} - _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) + _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer, ac) require.ErrorContains(t, err, "unrecognized") }) @@ -82,8 +84,9 @@ func TestConfigureHistorianBackend(t *testing.T) { Backend: "multiple", MultiPrimary: "invalid-backend", } + ac := &acfakes.FakeRuleService{} - _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) + _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer, ac) require.ErrorContains(t, err, "multi-backend target") require.ErrorContains(t, err, "unrecognized") @@ -99,8 +102,9 @@ func TestConfigureHistorianBackend(t *testing.T) { MultiPrimary: "annotations", MultiSecondaries: []string{"annotations", "invalid-backend"}, } + ac := &acfakes.FakeRuleService{} - _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) + _, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer, ac) require.ErrorContains(t, err, "multi-backend target") require.ErrorContains(t, err, "unrecognized") @@ -117,8 +121,9 @@ func TestConfigureHistorianBackend(t *testing.T) { LokiReadURL: "http://gone.invalid", LokiWriteURL: "http://gone.invalid", } + ac := &acfakes.FakeRuleService{} - h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) + h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer, ac) require.NotNil(t, h) require.NoError(t, err) @@ -133,8 +138,9 @@ func TestConfigureHistorianBackend(t *testing.T) { Enabled: true, Backend: "annotations", } + ac := &acfakes.FakeRuleService{} - h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) + h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer, ac) require.NotNil(t, h) require.NoError(t, err) @@ -155,8 +161,9 @@ grafana_alerting_state_history_info{backend="annotations"} 1 cfg := setting.UnifiedAlertingStateHistorySettings{ Enabled: false, } + ac := &acfakes.FakeRuleService{} - h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer) + h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer, ac) require.NotNil(t, h) require.NoError(t, err) diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index 409fc1823c7..e44c33ec86b 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -13,9 +13,11 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -23,6 +25,12 @@ import ( history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) +type AccessControl interface { + CanReadAllRules(ctx context.Context, user identity.Requester) (bool, error) + AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, rule ngmodels.Namespaced) error + HasAccessInFolder(ctx context.Context, user identity.Requester, rule ngmodels.Namespaced) (bool, error) +} + // AnnotationBackend is an implementation of state.Historian that uses Grafana Annotations as the backing datastore. type AnnotationBackend struct { store AnnotationStore @@ -30,10 +38,12 @@ type AnnotationBackend struct { clock clock.Clock metrics *metrics.Historian log log.Logger + ac AccessControl } type RuleStore interface { GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) (*ngmodels.AlertRule, error) + GetUserVisibleNamespaces(ctx context.Context, orgID int64, user identity.Requester) (map[string]*folder.Folder, error) } type AnnotationStore interface { @@ -41,13 +51,20 @@ type AnnotationStore interface { Save(ctx context.Context, panel *PanelKey, annotations []annotations.Item, orgID int64, logger log.Logger) error } -func NewAnnotationBackend(logger log.Logger, annotations AnnotationStore, rules RuleStore, metrics *metrics.Historian) *AnnotationBackend { +func NewAnnotationBackend( + logger log.Logger, + annotations AnnotationStore, + rules RuleStore, + metrics *metrics.Historian, + ac AccessControl, +) *AnnotationBackend { return &AnnotationBackend{ store: annotations, rules: rules, clock: clock.New(), metrics: metrics, log: logger, + ac: ac, } } @@ -107,6 +124,10 @@ func (h *AnnotationBackend) Query(ctx context.Context, query ngmodels.HistoryQue return nil, fmt.Errorf("no such rule exists") } + if err := h.ac.AuthorizeAccessInFolder(ctx, query.SignedInUser, rule); err != nil { + return nil, err + } + q := annotations.ItemQuery{ AlertID: rule.ID, OrgID: query.OrgID, diff --git a/pkg/services/ngalert/state/historian/annotation_test.go b/pkg/services/ngalert/state/historian/annotation_test.go index 9b5541d98c9..2b3e3c574ab 100644 --- a/pkg/services/ngalert/state/historian/annotation_test.go +++ b/pkg/services/ngalert/state/historian/annotation_test.go @@ -4,28 +4,33 @@ import ( "bytes" "context" "encoding/json" + "errors" "math" "testing" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/dashboards" + acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" + "github.com/grafana/grafana/pkg/services/user" ) func TestAnnotationHistorian(t *testing.T) { @@ -48,6 +53,31 @@ func TestAnnotationHistorian(t *testing.T) { } }) + t.Run("alert annotations are authorized", func(t *testing.T) { + anns := createTestAnnotationBackendSut(t) + ac := &acfakes.FakeRuleService{} + expectedErr := errors.New("test-error") + ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced models.Namespaced) error { + return expectedErr + } + anns.ac = ac + + items := []annotations.Item{createAnnotation()} + require.NoError(t, anns.store.Save(context.Background(), nil, items, 1, log.NewNopLogger())) + + q := models.HistoryQuery{ + RuleUID: "my-rule", + OrgID: 1, + SignedInUser: &user.SignedInUser{Name: "test-user", OrgID: 1}, + } + _, err := anns.Query(context.Background(), q) + + require.ErrorIs(t, err, expectedErr) + assert.Len(t, ac.Calls, 1) + assert.Equal(t, "AuthorizeAccessInFolder", ac.Calls[0].MethodName) + assert.Equal(t, q.SignedInUser, ac.Calls[0].Arguments[1]) + }) + t.Run("annotation queries send expected item query", func(t *testing.T) { store := &interceptingAnnotationStore{} anns := createTestAnnotationSutWithStore(t, store) @@ -132,7 +162,8 @@ func createTestAnnotationSutWithStore(t *testing.T, annotations AnnotationStore) models.RuleGen.With(models.RuleMuts.WithOrgID(1), withUID("my-rule")).GenerateRef(), } annotationBackendLogger := log.New("ngalert.state.historian", "backend", "annotations") - return NewAnnotationBackend(annotationBackendLogger, annotations, rules, met) + ac := &acfakes.FakeRuleService{} + return NewAnnotationBackend(annotationBackendLogger, annotations, rules, met, ac) } func createTestAnnotationBackendSutWithMetrics(t *testing.T, met *metrics.Historian) *AnnotationBackend { @@ -146,7 +177,8 @@ func createTestAnnotationBackendSutWithMetrics(t *testing.T, met *metrics.Histor dbs.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) store := NewAnnotationStore(fakeAnnoRepo, dbs, met) annotationBackendLogger := log.New("ngalert.state.historian", "backend", "annotations") - return NewAnnotationBackend(annotationBackendLogger, store, rules, met) + ac := &acfakes.FakeRuleService{} + return NewAnnotationBackend(annotationBackendLogger, store, rules, met, ac) } func createFailingAnnotationSut(t *testing.T, met *metrics.Historian) *AnnotationBackend { @@ -159,7 +191,8 @@ func createFailingAnnotationSut(t *testing.T, met *metrics.Historian) *Annotatio dbs.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) annotationBackendLogger := log.New("ngalert.state.historian", "backend", "annotations") store := NewAnnotationStore(fakeAnnoRepo, dbs, met) - return NewAnnotationBackend(annotationBackendLogger, store, rules, met) + ac := &acfakes.FakeRuleService{} + return NewAnnotationBackend(annotationBackendLogger, store, rules, met, ac) } func createAnnotation() annotations.Item { diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index fefbe48e462..6a6de55a57f 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "math" + "regexp" "sort" + "strings" "time" "github.com/benbjohnson/clock" @@ -16,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -75,15 +78,19 @@ type RemoteLokiBackend struct { clock clock.Clock metrics *metrics.Historian log log.Logger + ac AccessControl + ruleStore RuleStore } -func NewRemoteLokiBackend(logger log.Logger, cfg LokiConfig, req client.Requester, metrics *metrics.Historian, tracer tracing.Tracer) *RemoteLokiBackend { +func NewRemoteLokiBackend(logger log.Logger, cfg LokiConfig, req client.Requester, metrics *metrics.Historian, tracer tracing.Tracer, ruleStore RuleStore, ac AccessControl) *RemoteLokiBackend { return &RemoteLokiBackend{ client: NewLokiClient(cfg, req, metrics, logger, tracer), externalLabels: cfg.ExternalLabels, clock: clock.New(), metrics: metrics, log: logger, + ac: ac, + ruleStore: ruleStore, } } @@ -133,11 +140,19 @@ func (h *RemoteLokiBackend) Record(ctx context.Context, rule history_model.RuleM // Query retrieves state history entries from an external Loki instance and formats the results into a dataframe. func (h *RemoteLokiBackend) Query(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { - logQL, err := BuildLogQuery(query, h.client.MaxQuerySize()) + uids, err := h.getFolderUIDsForFilter(ctx, query) if err != nil { return nil, err } + logQL, filterByFolderSkipped, err := BuildLogQuery(query, uids, h.client.MaxQuerySize()) + if err != nil { + return nil, err + } + if filterByFolderSkipped { + h.log.FromContext(ctx).Warn("Filter by folder skipped because it's too long. Use in-memory filtering", "folders", len(uids)) + } + now := time.Now().UTC() if query.To.IsZero() { query.To = now @@ -151,7 +166,7 @@ func (h *RemoteLokiBackend) Query(ctx context.Context, query models.HistoryQuery if err != nil { return nil, err } - return merge(res, query.RuleUID) + return merge(res, uids) } func buildSelectors(query models.HistoryQuery) ([]Selector, error) { @@ -176,7 +191,12 @@ func buildSelectors(query models.HistoryQuery) ([]Selector, error) { } // merge will put all the results in one array sorted by timestamp. -func merge(res QueryRes, ruleUID string) (*data.Frame, error) { +func merge(res QueryRes, folderUIDToFilter []string) (*data.Frame, error) { + filterByFolderUIDMap := make(map[string]struct{}, len(folderUIDToFilter)) + for _, uid := range folderUIDToFilter { + filterByFolderUIDMap[uid] = struct{}{} + } + // Find the total number of elements in all arrays. totalLen := 0 for _, arr := range res.Data.Result { @@ -210,6 +230,18 @@ func merge(res QueryRes, ruleUID string) (*data.Frame, error) { if len(stream.Values) == pointers[i] { continue } + // check if stream should be in the results + if len(filterByFolderUIDMap) > 0 { + folderLbl, ok := stream.Stream[FolderUIDLabel] + if !ok { + continue // skip entries without folder UID, only if needs filtering + } + _, ok = filterByFolderUIDMap[folderLbl] + if !ok { + continue + } + } + curTime := stream.Values[pointers[i]].T.UnixNano() if pointers[i] < len(stream.Values) && curTime < minTime { minTime = curTime @@ -365,7 +397,7 @@ func NewSelector(label, op, value string) (Selector, error) { return Selector{Label: label, Op: Operator(op), Value: value}, nil } -func selectorString(selectors []Selector) string { +func selectorString(selectors []Selector, folderUIDs []string) string { if len(selectors) == 0 { return "{}" } @@ -374,8 +406,23 @@ func selectorString(selectors []Selector) string { for _, s := range selectors { query += fmt.Sprintf("%s%s%q,", s.Label, s.Op, s.Value) } - // Remove the last comma, as we append one to every selector. - query = query[:len(query)-1] + + if len(folderUIDs) > 0 { + b := strings.Builder{} + b.Grow(len(folderUIDs)*40 + len(FolderUIDLabel)) // rough estimate of the length + b.WriteString(FolderUIDLabel) + b.WriteString("=~`") + b.WriteString(regexp.QuoteMeta(folderUIDs[0])) + for _, uid := range folderUIDs[1:] { + b.WriteString("|") + b.WriteString(regexp.QuoteMeta(uid)) + } + b.WriteString("`") + query += b.String() + } else { + // Remove the last comma, as we append one to every selector. + query = query[:len(query)-1] + } return "{" + query + "}" } @@ -388,14 +435,18 @@ func isValidOperator(op string) bool { } // BuildLogQuery converts models.HistoryQuery and a list of folder UIDs to a Loki query. -// Returns a Loki query or error if log query cannot be constructed. If user-defined query exceeds maximum allowed size returns ErrLokiQueryTooLong -func BuildLogQuery(query models.HistoryQuery, maxQuerySize int) (string, error) { +// If query size exceeds the `maxQuerySize` then it re-builds query ignoring the folderUIDs. If it's still bigger - returns ErrQueryTooLong. +// Returns a tuple: +// - loki query +// - true if filter by folder UID was not added to the query ignored +// - error if log query cannot be constructed, and ErrQueryTooLong if user-defined query exceeds maximum allowed size +func BuildLogQuery(query models.HistoryQuery, folderUIDs []string, maxQuerySize int) (string, bool, error) { selectors, err := buildSelectors(query) if err != nil { - return "", fmt.Errorf("failed to build the provided selectors: %w", err) + return "", false, fmt.Errorf("failed to build the provided selectors: %w", err) } - logQL := selectorString(selectors) + logQL := selectorString(selectors, folderUIDs) if queryHasLogFilters(query) { logQL = fmt.Sprintf("%s | json", logQL) @@ -424,10 +475,22 @@ func BuildLogQuery(query models.HistoryQuery, maxQuerySize int) (string, error) logQL += labelFilters if len(logQL) > maxQuerySize { + // if request is too long, try to drop filter by folder UIDs. + if len(folderUIDs) > 0 { + logQL, tooLong, err := BuildLogQuery(query, nil, maxQuerySize) + if err != nil { + return "", false, err + } + if tooLong { + return "", false, NewErrLokiQueryTooLong(logQL, maxQuerySize) + } + return logQL, true, nil + } // if the query is too long even without filter by folders, then fail - return "", NewErrLokiQueryTooLong(logQL, maxQuerySize) + return "", false, NewErrLokiQueryTooLong(logQL, maxQuerySize) } - return logQL, nil + + return logQL, false, nil } func queryHasLogFilters(query models.HistoryQuery) bool { @@ -436,3 +499,48 @@ func queryHasLogFilters(query models.HistoryQuery) bool { query.PanelID != 0 || len(query.Labels) > 0 } + +func (h *RemoteLokiBackend) getFolderUIDsForFilter(ctx context.Context, query models.HistoryQuery) ([]string, error) { + bypass, err := h.ac.CanReadAllRules(ctx, query.SignedInUser) + if err != nil { + return nil, err + } + if bypass { // if user has access to all rules and folder, remove filter + return nil, nil + } + // if there is a filter by rule UID, find that rule UID and make sure that user has access to it. + if query.RuleUID != "" { + rule, err := h.ruleStore.GetAlertRuleByUID(ctx, &models.GetAlertRuleByUIDQuery{ + UID: query.RuleUID, + OrgID: query.OrgID, + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch alert rule by UID: %w", err) + } + if rule == nil { + return nil, models.ErrAlertRuleNotFound + } + return nil, h.ac.AuthorizeAccessInFolder(ctx, query.SignedInUser, rule) + } + // if no filter, then we need to get all namespaces user has access to + folders, err := h.ruleStore.GetUserVisibleNamespaces(ctx, query.OrgID, query.SignedInUser) + if err != nil { + return nil, fmt.Errorf("failed to fetch folders that user can access: %w", err) + } + uids := make([]string, 0, len(folders)) + // now keep only UIDs of folder in which user can read rules. + for _, f := range folders { + hasAccess, err := h.ac.HasAccessInFolder(ctx, query.SignedInUser, models.Namespace(*f)) + if err != nil { + return nil, err + } + if !hasAccess { + continue + } + uids = append(uids, f.UID) + } + if len(uids) == 0 { + return nil, accesscontrol.NewAuthorizationErrorGeneric("read rules in any folder") + } + return uids, nil +} diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go index ac92673f06a..97c37b3dd02 100644 --- a/pkg/services/ngalert/state/historian/loki_test.go +++ b/pkg/services/ngalert/state/historian/loki_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -15,16 +16,24 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/folder" + rulesAuthz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" + acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" + "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" + "github.com/grafana/grafana/pkg/services/org" ) func TestRemoteLokiBackend(t *testing.T) { @@ -197,12 +206,17 @@ func TestRemoteLokiBackend(t *testing.T) { t.Run("selector string", func(t *testing.T) { selectors := []Selector{{"name", "=", "Bob"}, {"age", "=~", "30"}} expected := "{name=\"Bob\",age=~\"30\"}" - result := selectorString(selectors) + result := selectorString(selectors, nil) + require.Equal(t, expected, result) + + selectors = []Selector{{"name", "=", "quoted\"string"}, {"age", "=~", "30"}} + expected = "{name=\"quoted\\\"string\",age=~\"30\",folderUID=~`some\\\\d\\.r\\$|normal_string`}" + result = selectorString(selectors, []string{`some\d.r$`, "normal_string"}) require.Equal(t, expected, result) selectors = []Selector{} expected = "{}" - result = selectorString(selectors) + result = selectorString(selectors, nil) require.Equal(t, expected, result) }) @@ -221,10 +235,12 @@ func TestRemoteLokiBackend(t *testing.T) { func TestBuildLogQuery(t *testing.T) { maxQuerySize := 110 cases := []struct { - name string - query models.HistoryQuery - exp string - expErr error + name string + query models.HistoryQuery + folderUIDs []string + exp string + expErr error + expDropped bool }{ { name: "default includes state history label and orgID label", @@ -282,8 +298,7 @@ func TestBuildLogQuery(t *testing.T) { "customlabel": "customvalue", }, }, - exp: `{orgID="123",from="state-history"} | json | ruleUID="rule-uid" | labels_customlabel="customvalue"`, - }, + exp: `{orgID="123",from="state-history"} | json | ruleUID="rule-uid" | labels_customlabel="customvalue"`}, { name: "should return if query does not exceed max limit", query: models.HistoryQuery{ @@ -306,16 +321,38 @@ func TestBuildLogQuery(t *testing.T) { }, expErr: ErrLokiQueryTooLong, }, + { + name: "filters by all namespaces", + query: models.HistoryQuery{ + OrgID: 123, + }, + folderUIDs: []string{"folder-1", "folder\\d"}, + exp: `{orgID="123",from="state-history",folderUID=~` + "`folder-1|folder\\\\d`" + `}`, + }, + { + name: "should drop folders if it's too long", + query: models.HistoryQuery{ + OrgID: 123, + RuleUID: "rule-uid", + Labels: map[string]string{ + "customlabel": "customvalue", + }, + }, + folderUIDs: []string{"folder-1", "folder-2", "folder\\d"}, + exp: `{orgID="123",from="state-history"} | json | ruleUID="rule-uid" | labels_customlabel="customvalue"`, + expDropped: true, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - res, err := BuildLogQuery(tc.query, maxQuerySize) + res, dropped, err := BuildLogQuery(tc.query, tc.folderUIDs, maxQuerySize) if tc.expErr != nil { require.ErrorIs(t, err, tc.expErr) return } require.LessOrEqual(t, len(res), maxQuerySize) + require.Equal(t, tc.expDropped, dropped) require.NoError(t, err) require.Equal(t, tc.exp, res) }) @@ -324,10 +361,10 @@ func TestBuildLogQuery(t *testing.T) { func TestMerge(t *testing.T) { testCases := []struct { - name string - res QueryRes - ruleID string - expectedTime []time.Time + name string + res QueryRes + expected *data.Frame + folderUIDs []string }{ { name: "Should return values from multiple streams in right order", @@ -336,28 +373,55 @@ func TestMerge(t *testing.T) { Result: []Stream{ { Stream: map[string]string{ - "current": "pending", + "from": "state-history", + "orgID": "1", + "group": "test-group-1", + "folderUID": "test-folder-1", + "extra": "label", }, Values: []Sample{ - {time.Unix(0, 1), `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": "b"}}`}, + {time.Unix(1, 0), `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, }, }, { Stream: map[string]string{ - "current": "firing", + "from": "state-history", + "orgID": "1", + "group": "test-group-2", + "folderUID": "test-folder-1", }, Values: []Sample{ - {time.Unix(0, 2), `{"schemaVersion": 1, "previous": "pending", "current": "firing", "values":{"a": "b"}}`}, + {time.Unix(2, 0), `{"schemaVersion": 1, "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-2"}`}, }, }, }, }, }, - ruleID: "123456", - expectedTime: []time.Time{ - time.Unix(0, 1), - time.Unix(0, 2), - }, + expected: data.NewFrame("states", + data.NewField(dfTime, data.Labels{}, []time.Time{ + time.Unix(1, 0), + time.Unix(2, 0), + }), + data.NewField(dfLine, data.Labels{}, []json.RawMessage{ + toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Values: jsonifyValues(map[string]float64{"a": 1.5})}), + toJson(LokiEntry{RuleUID: "test-rule-2", SchemaVersion: 1, Previous: "pending", Current: "firing", Values: jsonifyValues(map[string]float64{"a": 2.5})}), + }), + data.NewField(dfLabels, data.Labels{}, []json.RawMessage{ + toJson(map[string]string{ + StateHistoryLabelKey: "state-history", + OrgIDLabel: "1", + GroupLabel: "test-group-1", + FolderUIDLabel: "test-folder-1", + "extra": "label", + }), + toJson(map[string]string{ + StateHistoryLabelKey: "state-history", + OrgIDLabel: "1", + GroupLabel: "test-group-2", + FolderUIDLabel: "test-folder-1", + }), + }), + ), }, { name: "Should handle empty values", @@ -366,15 +430,18 @@ func TestMerge(t *testing.T) { Result: []Stream{ { Stream: map[string]string{ - "current": "normal", + "extra": "labels", }, Values: []Sample{}, }, }, }, }, - ruleID: "123456", - expectedTime: []time.Time{}, + expected: data.NewFrame("states", + data.NewField(dfTime, data.Labels{}, []time.Time{}), + data.NewField(dfLine, data.Labels{}, []json.RawMessage{}), + data.NewField(dfLabels, data.Labels{}, []json.RawMessage{}), + ), }, { name: "Should handle multiple values in one stream", @@ -383,50 +450,188 @@ func TestMerge(t *testing.T) { Result: []Stream{ { Stream: map[string]string{ - "current": "normal", + "from": "state-history", + "orgID": "1", + "group": "test-group-1", + "folderUID": "test-folder-1", }, Values: []Sample{ - {time.Unix(0, 1), `{"schemaVersion": 1, "previous": "firing", "current": "normal", "values":{"a": "b"}}`}, - {time.Unix(0, 2), `{"schemaVersion": 1, "previous": "firing", "current": "normal", "values":{"a": "b"}}`}, + {time.Unix(1, 0), `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, + {time.Unix(5, 0), `{"schemaVersion": 1, "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2"}`}, }, }, { Stream: map[string]string{ - "current": "firing", + "from": "state-history", + "orgID": "1", + "group": "test-group-2", + "folderUID": "test-folder-1", }, Values: []Sample{ - {time.Unix(0, 3), `{"schemaVersion": 1, "previous": "pending", "current": "firing", "values":{"a": "b"}}`}, + {time.Unix(2, 0), `{"schemaVersion": 1, "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-3"}`}, }, }, }, }, }, - ruleID: "123456", - expectedTime: []time.Time{ - time.Unix(0, 1), - time.Unix(0, 2), - time.Unix(0, 3), + expected: data.NewFrame("states", + data.NewField(dfTime, data.Labels{}, []time.Time{ + time.Unix(1, 0), + time.Unix(2, 0), + time.Unix(5, 0), + }), + data.NewField(dfLine, data.Labels{}, []json.RawMessage{ + toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Values: jsonifyValues(map[string]float64{"a": 1.5})}), + toJson(LokiEntry{RuleUID: "test-rule-3", SchemaVersion: 1, Previous: "pending", Current: "firing", Values: jsonifyValues(map[string]float64{"a": 2.5})}), + toJson(LokiEntry{RuleUID: "test-rule-2", SchemaVersion: 1, Previous: "pending", Current: "normal", Values: jsonifyValues(map[string]float64{"a": 0.5})}), + }), + data.NewField(dfLabels, data.Labels{}, []json.RawMessage{ + toJson(map[string]string{ + StateHistoryLabelKey: "state-history", + OrgIDLabel: "1", + GroupLabel: "test-group-1", + FolderUIDLabel: "test-folder-1", + }), + toJson(map[string]string{ + StateHistoryLabelKey: "state-history", + OrgIDLabel: "1", + GroupLabel: "test-group-2", + FolderUIDLabel: "test-folder-1", + }), + toJson(map[string]string{ + StateHistoryLabelKey: "state-history", + OrgIDLabel: "1", + GroupLabel: "test-group-1", + FolderUIDLabel: "test-folder-1", + }), + }), + ), + }, + { + name: "should filter streams by folder UID", + folderUIDs: []string{"test-folder-1"}, + res: QueryRes{ + Data: QueryData{ + Result: []Stream{ + { + Stream: map[string]string{ + "from": "state-history", + "orgID": "1", + "group": "test-group-1", + "folderUID": "test-folder-1", + }, + Values: []Sample{ + {time.Unix(1, 0), `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, + {time.Unix(5, 0), `{"schemaVersion": 1, "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2"}`}, + }, + }, + { + Stream: map[string]string{ + "from": "state-history", + "orgID": "1", + "group": "test-group-2", + "folderUID": "test-folder-2", + }, + Values: []Sample{ + {time.Unix(2, 0), `{"schemaVersion": 1, "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-3"}`}, + }, + }, + }, + }, }, + expected: data.NewFrame("states", + data.NewField(dfTime, data.Labels{}, []time.Time{ + time.Unix(1, 0), + time.Unix(5, 0), + }), + data.NewField(dfLine, data.Labels{}, []json.RawMessage{ + toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Values: jsonifyValues(map[string]float64{"a": 1.5})}), + toJson(LokiEntry{RuleUID: "test-rule-2", SchemaVersion: 1, Previous: "pending", Current: "normal", Values: jsonifyValues(map[string]float64{"a": 0.5})}), + }), + data.NewField(dfLabels, data.Labels{}, []json.RawMessage{ + toJson(map[string]string{ + StateHistoryLabelKey: "state-history", + OrgIDLabel: "1", + GroupLabel: "test-group-1", + FolderUIDLabel: "test-folder-1", + }), + toJson(map[string]string{ + StateHistoryLabelKey: "state-history", + OrgIDLabel: "1", + GroupLabel: "test-group-1", + FolderUIDLabel: "test-folder-1", + }), + }), + ), + }, + { + name: "should skip streams without folder UID if filter is specified", + folderUIDs: []string{"test-folder-1"}, + res: QueryRes{ + Data: QueryData{ + Result: []Stream{ + { + Stream: map[string]string{ + "group": "test-group-1", + }, + Values: []Sample{ + {time.Unix(1, 0), `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, + {time.Unix(5, 0), `{"schemaVersion": 1, "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2"}`}, + }, + }, + }, + }, + }, + expected: data.NewFrame("states", + data.NewField(dfTime, data.Labels{}, []time.Time{}), + data.NewField(dfLine, data.Labels{}, []json.RawMessage{}), + data.NewField(dfLabels, data.Labels{}, []json.RawMessage{}), + ), + }, + { + name: "should return streams without folder UID if filter is not specified", + folderUIDs: []string{}, + res: QueryRes{ + Data: QueryData{ + Result: []Stream{ + { + Stream: map[string]string{ + "group": "test-group-1", + }, + Values: []Sample{ + {time.Unix(1, 0), `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, + }, + }, + }, + }, + }, + expected: data.NewFrame("states", + data.NewField(dfTime, data.Labels{}, []time.Time{ + time.Unix(1, 0), + }), + data.NewField(dfLine, data.Labels{}, []json.RawMessage{ + toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Values: jsonifyValues(map[string]float64{"a": 1.5})}), + }), + data.NewField(dfLabels, data.Labels{}, []json.RawMessage{ + toJson(map[string]string{ + GroupLabel: "test-group-1", + }), + }), + ), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - m, err := merge(tc.res, tc.ruleID) + expectedJson, err := tc.expected.MarshalJSON() require.NoError(t, err) + m, err := merge(tc.res, tc.folderUIDs) + require.NoError(t, err) + actualJson, err := m.MarshalJSON() + assert.NoError(t, err) - var dfTimeColumn *data.Field - for _, f := range m.Fields { - if f.Name == dfTime { - dfTimeColumn = f - } - } - - require.NotNil(t, dfTimeColumn) - - for i := 0; i < len(tc.expectedTime); i++ { - require.Equal(t, tc.expectedTime[i], dfTimeColumn.At(i)) - } + assert.Equal(t, tc.expected.Rows(), m.Rows()) + assert.JSONEq(t, string(expectedJson), string(actualJson)) }) } } @@ -434,7 +639,7 @@ func TestMerge(t *testing.T) { func TestRecordStates(t *testing.T) { t.Run("writes state transitions to loki", func(t *testing.T) { req := NewFakeRequester() - loki := createTestLokiBackend(req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) + loki := createTestLokiBackend(t, req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) rule := createTestRule() states := singleFromNormal(&state.State{ State: eval.Alerting, @@ -450,8 +655,8 @@ func TestRecordStates(t *testing.T) { t.Run("emits expected write metrics", func(t *testing.T) { reg := prometheus.NewRegistry() met := metrics.NewHistorianMetrics(reg, metrics.Subsystem) - loki := createTestLokiBackend(NewFakeRequester(), met) - errLoki := createTestLokiBackend(NewFakeRequester().WithResponse(badResponse()), met) //nolint:bodyclose + loki := createTestLokiBackend(t, NewFakeRequester(), met) + errLoki := createTestLokiBackend(t, NewFakeRequester().WithResponse(badResponse()), met) //nolint:bodyclose rule := createTestRule() states := singleFromNormal(&state.State{ State: eval.Alerting, @@ -486,7 +691,7 @@ grafana_alerting_state_history_writes_total{backend="loki",org="1"} 2 t.Run("elides request if nothing to send", func(t *testing.T) { req := NewFakeRequester() - loki := createTestLokiBackend(req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) + loki := createTestLokiBackend(t, req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) rule := createTestRule() states := []state.StateTransition{} @@ -498,7 +703,7 @@ grafana_alerting_state_history_writes_total{backend="loki",org="1"} 2 t.Run("succeeds with special chars in labels", func(t *testing.T) { req := NewFakeRequester() - loki := createTestLokiBackend(req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) + loki := createTestLokiBackend(t, req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) rule := createTestRule() states := singleFromNormal(&state.State{ State: eval.Alerting, @@ -521,7 +726,7 @@ grafana_alerting_state_history_writes_total{backend="loki",org="1"} 2 t.Run("adds external labels to log lines", func(t *testing.T) { req := NewFakeRequester() - loki := createTestLokiBackend(req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) + loki := createTestLokiBackend(t, req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) rule := createTestRule() states := singleFromNormal(&state.State{ State: eval.Alerting, @@ -537,7 +742,153 @@ grafana_alerting_state_history_writes_total{backend="loki",org="1"} 2 }) } -func createTestLokiBackend(req client.Requester, met *metrics.Historian) *RemoteLokiBackend { +func TestGetFolderUIDsForFilter(t *testing.T) { + orgID := int64(1) + rule := models.RuleGen.With(models.RuleMuts.WithNamespaceUID("folder-1")).GenerateRef() + folders := []string{ + "folder-1", + "folder-2", + "folder-3", + } + usr := accesscontrol.BackgroundUser("test", 1, org.RoleNone, nil) + + createLoki := func(ac AccessControl) *RemoteLokiBackend { + req := NewFakeRequester() + loki := createTestLokiBackend(t, req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) + rules := fakes.NewRuleStore(t) + f := make([]*folder.Folder, 0, len(folders)) + for _, uid := range folders { + f = append(f, &folder.Folder{UID: uid, OrgID: orgID}) + } + rules.Folders = map[int64][]*folder.Folder{ + orgID: f, + } + rules.Rules = map[int64][]*models.AlertRule{ + orgID: {rule}, + } + loki.ruleStore = rules + loki.ac = ac + return loki + } + + t.Run("when rule UID is specified", func(t *testing.T) { + t.Run("should bypass authorization if user can read all rules", func(t *testing.T) { + ac := &acfakes.FakeRuleService{} + ac.CanReadAllRulesFunc = func(ctx context.Context, requester identity.Requester) (bool, error) { + return true, nil + } + result, err := createLoki(ac).getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, RuleUID: rule.UID, SignedInUser: usr}) + assert.NoError(t, err) + assert.Empty(t, result) + + assert.Len(t, ac.Calls, 1) + assert.Equal(t, "CanReadAllRules", ac.Calls[0].MethodName) + assert.Equal(t, usr, ac.Calls[0].Arguments[1]) + + t.Run("even if rule does not exist", func(t *testing.T) { + result, err := createLoki(ac).getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, RuleUID: "not-found", SignedInUser: usr}) + assert.NoError(t, err) + assert.Empty(t, result) + }) + }) + + t.Run("should authorize access to the rule", func(t *testing.T) { + ac := &acfakes.FakeRuleService{} + ac.CanReadAllRulesFunc = func(ctx context.Context, requester identity.Requester) (bool, error) { + return false, nil + } + ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced models.Namespaced) error { + return nil + } + loki := createLoki(ac) + + result, err := loki.getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, RuleUID: rule.UID, SignedInUser: usr}) + assert.NoError(t, err) + assert.Empty(t, result) + + assert.Len(t, ac.Calls, 2) + assert.Equal(t, "CanReadAllRules", ac.Calls[0].MethodName) + assert.Equal(t, usr, ac.Calls[0].Arguments[1]) + assert.Equal(t, "AuthorizeAccessInFolder", ac.Calls[1].MethodName) + assert.Equal(t, usr, ac.Calls[1].Arguments[1]) + assert.Equal(t, rule, ac.Calls[1].Arguments[2]) + + t.Run("should fail if unauthorized", func(t *testing.T) { + authzErr := errors.New("generic error") + ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced models.Namespaced) error { + return authzErr + } + result, err = loki.getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, RuleUID: rule.UID, SignedInUser: usr}) + require.ErrorIs(t, err, authzErr) + }) + + t.Run("should fail if rule does not exist", func(t *testing.T) { + result, err = loki.getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, RuleUID: "not-found", SignedInUser: usr}) + require.ErrorIs(t, err, models.ErrAlertRuleNotFound) + }) + }) + }) + + t.Run("when rule UID is empty", func(t *testing.T) { + t.Run("should bypass authorization if user can read all rules", func(t *testing.T) { + ac := &acfakes.FakeRuleService{} + ac.CanReadAllRulesFunc = func(ctx context.Context, requester identity.Requester) (bool, error) { + return true, nil + } + result, err := createLoki(ac).getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, SignedInUser: usr}) + assert.NoError(t, err) + assert.Empty(t, result) + + assert.Len(t, ac.Calls, 1) + assert.Equal(t, "CanReadAllRules", ac.Calls[0].MethodName) + assert.Equal(t, usr, ac.Calls[0].Arguments[1]) + }) + + t.Run("should return only folders user has access to", func(t *testing.T) { + ac := &acfakes.FakeRuleService{} + ac.CanReadAllRulesFunc = func(ctx context.Context, requester identity.Requester) (bool, error) { + return false, nil + } + ac.HasAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced models.Namespaced) (bool, error) { + return true, nil + } + loki := createLoki(ac) + + result, err := loki.getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, SignedInUser: usr}) + assert.NoError(t, err) + assert.EqualValues(t, folders, result) + + assert.Len(t, ac.Calls, len(folders)+1) + assert.Equal(t, "CanReadAllRules", ac.Calls[0].MethodName) + assert.Equal(t, usr, ac.Calls[0].Arguments[1]) + for i, folderUID := range folders { + assert.Equal(t, "HasAccessInFolder", ac.Calls[i+1].MethodName) + assert.Equal(t, usr, ac.Calls[i+1].Arguments[1]) + assert.Equal(t, folderUID, ac.Calls[i+1].Arguments[2].(models.Namespaced).GetNamespaceUID()) + } + + t.Run("should fail if no folders to read", func(t *testing.T) { + loki := createLoki(ac) + loki.ruleStore = fakes.NewRuleStore(t) + + result, err = loki.getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, SignedInUser: usr}) + require.ErrorIs(t, err, rulesAuthz.ErrAuthorizationBase) + require.Empty(t, result) + }) + + t.Run("should fail if no folders to read alert rules in", func(t *testing.T) { + ac.HasAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced models.Namespaced) (bool, error) { + return false, nil + } + result, err = loki.getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, SignedInUser: usr}) + require.ErrorIs(t, err, rulesAuthz.ErrAuthorizationBase) + require.Empty(t, result) + }) + }) + }) +} + +func createTestLokiBackend(t *testing.T, req client.Requester, met *metrics.Historian) *RemoteLokiBackend { url, _ := url.Parse("http://some.url") cfg := LokiConfig{ WritePathURL: url, @@ -546,7 +897,9 @@ func createTestLokiBackend(req client.Requester, met *metrics.Historian) *Remote ExternalLabels: map[string]string{"externalLabelKey": "externalLabelValue"}, } lokiBackendLogger := log.New("ngalert.state.historian", "backend", "loki") - return NewRemoteLokiBackend(lokiBackendLogger, cfg, req, met, tracing.InitializeTracerForTest()) + rules := fakes.NewRuleStore(t) + ac := &acfakes.FakeRuleService{} + return NewRemoteLokiBackend(lokiBackendLogger, cfg, req, met, tracing.InitializeTracerForTest(), rules, ac) } func singleFromNormal(st *state.State) []state.StateTransition { @@ -602,3 +955,11 @@ func readBody(t *testing.T, req *http.Request) []byte { require.NoError(t, err) return val } + +func toJson[T any](entry T) json.RawMessage { + b, err := json.Marshal(entry) + if err != nil { + panic(err) + } + return b +} diff --git a/pkg/services/ngalert/state/manager_bench_test.go b/pkg/services/ngalert/state/manager_bench_test.go index df9bed60699..14505e8aeee 100644 --- a/pkg/services/ngalert/state/manager_bench_test.go +++ b/pkg/services/ngalert/state/manager_bench_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -25,7 +26,8 @@ func BenchmarkProcessEvalResults(b *testing.B) { metrics := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) store := historian.NewAnnotationStore(&as, nil, metrics) annotationBackendLogger := log.New("ngalert.state.historian", "backend", "annotations") - hist := historian.NewAnnotationBackend(annotationBackendLogger, store, nil, metrics) + ac := &fakes.FakeRuleService{} + hist := historian.NewAnnotationBackend(annotationBackendLogger, store, nil, metrics, ac) cfg := state.ManagerCfg{ Historian: hist, Tracer: tracing.InitializeTracerForTest(), diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index e008edc811e..a15a2a5da27 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/dashboards" + acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -235,7 +236,8 @@ func TestDashboardAnnotations(t *testing.T) { historianMetrics := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) store := historian.NewAnnotationStore(fakeAnnoRepo, &dashboards.FakeDashboardService{}, historianMetrics) annotationBackendLogger := log.New("ngalert.state.historian", "backend", "annotations") - hist := historian.NewAnnotationBackend(annotationBackendLogger, store, nil, historianMetrics) + ac := &acfakes.FakeRuleService{} + hist := historian.NewAnnotationBackend(annotationBackendLogger, store, nil, historianMetrics, ac) cfg := state.ManagerCfg{ Metrics: metrics.NewNGAlert(prometheus.NewPedanticRegistry()).GetStateMetrics(), ExternalURL: nil, @@ -1386,7 +1388,8 @@ func TestProcessEvalResults(t *testing.T) { m := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) store := historian.NewAnnotationStore(fakeAnnoRepo, &dashboards.FakeDashboardService{}, m) annotationBackendLogger := log.New("ngalert.state.historian", "backend", "annotations") - hist := historian.NewAnnotationBackend(annotationBackendLogger, store, nil, m) + ac := &acfakes.FakeRuleService{} + hist := historian.NewAnnotationBackend(annotationBackendLogger, store, nil, m, ac) clk := clock.NewMock() cfg := state.ManagerCfg{ Metrics: stateMetrics, From e6e163eaf5906607952675a47a6b340267c296b6 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Wed, 26 Jun 2024 12:06:10 -0300 Subject: [PATCH 04/11] ShareDrawer: Share Snapshot menu item condition (#89733) --- .../sharing/ShareButton/ShareMenu.test.tsx | 52 +++++++++++++++++-- .../sharing/ShareButton/ShareMenu.tsx | 16 +++--- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx index 0da03f45dda..525ece5f205 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx @@ -2,10 +2,11 @@ import { render, screen } from '@testing-library/react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { contextSrv } from 'app/core/services/context_srv'; import { config } from '../../../../core/config'; import { DashboardGridItem } from '../../scene/DashboardGridItem'; -import { DashboardScene } from '../../scene/DashboardScene'; +import { DashboardScene, DashboardSceneState } from '../../scene/DashboardScene'; import ShareMenu from './ShareMenu'; @@ -16,26 +17,66 @@ jest.mock('app/core/utils/shortLinks', () => ({ })); const selector = e2eSelectors.pages.Dashboard.DashNav.newShareButton.menu; + describe('ShareMenu', () => { + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + it('should render menu items', async () => { + Object.defineProperty(contextSrv, 'isSignedIn', { + value: true, + }); config.featureToggles.publicDashboards = true; config.publicDashboardsEnabled = true; - setup(); + config.snapshotEnabled = true; + setup({ meta: { canEdit: true } }); expect(await screen.findByTestId(selector.shareInternally)).toBeInTheDocument(); expect(await screen.findByTestId(selector.shareExternally)).toBeInTheDocument(); expect(await screen.findByTestId(selector.shareSnapshot)).toBeInTheDocument(); }); - it('should no share externally when public dashboard is disabled', async () => { + it('should not share externally when public dashboard is disabled', async () => { config.featureToggles.publicDashboards = false; config.publicDashboardsEnabled = false; setup(); - expect(await screen.queryByTestId(selector.shareExternally)).not.toBeInTheDocument(); + expect(screen.queryByTestId(selector.shareExternally)).not.toBeInTheDocument(); + }); + + describe('ShareSnapshot', () => { + it('should not share snapshot when user is not signed in', async () => { + config.snapshotEnabled = true; + Object.defineProperty(contextSrv, 'isSignedIn', { + value: false, + }); + setup({ meta: { canEdit: true } }); + + expect(screen.queryByTestId(selector.shareSnapshot)).not.toBeInTheDocument(); + }); + it('should not share snapshot when snapshot is not enabled', async () => { + Object.defineProperty(contextSrv, 'isSignedIn', { + value: true, + }); + config.snapshotEnabled = false; + setup({ meta: { canEdit: true } }); + + expect(screen.queryByTestId(selector.shareSnapshot)).not.toBeInTheDocument(); + }); + it('should not share snapshot when dashboard cannot edit', async () => { + Object.defineProperty(contextSrv, 'isSignedIn', { + value: true, + }); + config.snapshotEnabled = true; + setup({ meta: { canEdit: false } }); + + expect(screen.queryByTestId(selector.shareSnapshot)).not.toBeInTheDocument(); + }); }); }); -function setup() { +function setup(overrides?: Partial) { const panel = new VizPanel({ title: 'Panel A', pluginId: 'table', @@ -58,6 +99,7 @@ function setup() { }), ], }), + ...overrides, }); render(); diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx index 9ccf3be0654..6177ca3e94b 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx @@ -1,6 +1,8 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; import { VizPanel } from '@grafana/scenes'; import { Menu } from '@grafana/ui'; +import { contextSrv } from 'app/core/core'; import { t } from 'app/core/internationalization'; import { isPublicDashboardsEnabled } from '../../../dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; @@ -58,12 +60,14 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc onClick={onShareExternallyClick} /> )} - + {contextSrv.isSignedIn && config.snapshotEnabled && dashboard.canEditDashboard() && ( + + )} ); } From 7f4faaa45b032f20140027e97dd7189a1e40ccdc Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Wed, 26 Jun 2024 17:25:26 +0200 Subject: [PATCH 05/11] ExtJWT: Remove test (#89665) Remove test --- pkg/services/authn/clients/ext_jwt_test.go | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 691295409ab..7f072a6b5e8 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -141,43 +141,26 @@ func TestExtendedJWT_Test(t *testing.T) { }, { name: "should return true when Authorization header contains Bearer prefix", - cfg: nil, authHeaderFunc: func() string { return "Bearer " + generateToken(validAccessTokenClaims, pk, jose.RS256) }, want: true, }, { name: "should return true when Authorization header only contains the token", - cfg: nil, authHeaderFunc: func() string { return generateToken(validAccessTokenClaims, pk, jose.RS256) }, want: true, }, { name: "should return false when Authorization header is empty", - cfg: nil, authHeaderFunc: func() string { return "" }, want: false, }, { name: "should return false when jwt.ParseSigned fails", - cfg: nil, authHeaderFunc: func() string { return "invalid token" }, want: false, }, - { - name: "should return false when the issuer does not match the configured issuer", - cfg: &setting.Cfg{ - ExtJWTAuth: setting.ExtJWTSettings{ - ExpectIssuer: "http://localhost:3000", - }, - }, - authHeaderFunc: func() string { - payload := validAccessTokenClaims - payload.Issuer = "http://unknown-issuer" - return generateToken(payload, pk, jose.RS256) - }, - want: false, - }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { env := setupTestCtx(tc.cfg) From 40207c53ae8a4ed69bbb16f24eddad4b12b98530 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 26 Jun 2024 17:29:17 +0200 Subject: [PATCH 06/11] Plugins: Expose functions to plugins for checking RBAC permissions (#89047) * feat(grafana-data): create rbac functions for checking permissions * feat(grafana-runtime): pass current user to runtime * feat(grafana-runtime): expose rbac functions to check permissions against current user * refactor(contextsrv): use functions from grafana/data to check rbac permissions against user * Apply suggestions from code review Co-authored-by: Marcus Andersson * chore(rbac): fix missing types imports * refactor(rbac): make exposed functions return boolean --------- Co-authored-by: Marcus Andersson --- packages/grafana-data/src/index.ts | 7 +++++ packages/grafana-data/src/rbac/rbac.ts | 19 ++++++++++++ packages/grafana-runtime/src/index.ts | 1 + .../grafana-runtime/src/services/index.ts | 1 + packages/grafana-runtime/src/services/user.ts | 29 +++++++++++++++++++ packages/grafana-runtime/src/utils/rbac.ts | 18 ++++++++++++ public/app/app.ts | 2 ++ public/app/core/services/context_srv.ts | 16 +++++++--- 8 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 packages/grafana-data/src/rbac/rbac.ts create mode 100644 packages/grafana-runtime/src/services/user.ts create mode 100644 packages/grafana-runtime/src/utils/rbac.ts diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 8ce1be9fe18..d0f7dc5015d 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -50,3 +50,10 @@ export { CircularVector } from './vector/CircularVector'; export { vectorator } from './vector/FunctionalVector'; export { ArrayVector } from './vector/ArrayVector'; export * from './dataframe/CircularDataFrame'; +export { + type CurrentUser, + userHasPermission, + userHasPermissionInMetadata, + userHasAllPermissions, + userHasAnyPermission, +} from './rbac/rbac'; diff --git a/packages/grafana-data/src/rbac/rbac.ts b/packages/grafana-data/src/rbac/rbac.ts new file mode 100644 index 00000000000..90e87522caa --- /dev/null +++ b/packages/grafana-data/src/rbac/rbac.ts @@ -0,0 +1,19 @@ +import { CurrentUserDTO, WithAccessControlMetadata } from '../types'; + +export interface CurrentUser extends Omit {} + +export function userHasPermission(action: string, user: CurrentUser): boolean { + return !!user.permissions?.[action]; +} + +export function userHasPermissionInMetadata(action: string, object: WithAccessControlMetadata): boolean { + return !!object.accessControl?.[action]; +} + +export function userHasAllPermissions(actions: string[], user: CurrentUser) { + return actions.every((action) => userHasPermission(action, user)); +} + +export function userHasAnyPermission(actions: string[], user: CurrentUser) { + return actions.some((action) => userHasPermission(action, user)); +} diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index f5fc864e640..b7bf8246a93 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -52,3 +52,4 @@ export { export { usePluginInteractionReporter } from './analytics/plugins/usePluginInteractionReporter'; export { setReturnToPreviousHook, useReturnToPrevious } from './utils/returnToPrevious'; export { type EmbeddedDashboardProps, EmbeddedDashboard, setEmbeddedDashboard } from './components/EmbeddedDashboard'; +export { hasPermission, hasPermissionInMetadata, hasAllPermissions, hasAnyPermission } from './utils/rbac'; diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index a0dc7db5ce5..1a2cf79cc51 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -33,3 +33,4 @@ export { export { setPluginComponentHook, usePluginComponent } from './pluginExtensions/usePluginComponent'; export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils'; +export { setCurrentUser } from './user'; diff --git a/packages/grafana-runtime/src/services/user.ts b/packages/grafana-runtime/src/services/user.ts new file mode 100644 index 00000000000..1ca4e43b301 --- /dev/null +++ b/packages/grafana-runtime/src/services/user.ts @@ -0,0 +1,29 @@ +import { CurrentUser } from '@grafana/data'; + +let singletonInstance: CurrentUser | null = null; + +/** + * Used during startup by Grafana to set the current user so it is available + * for rbac checks. + * + * @internal + */ +export function setCurrentUser(instance: CurrentUser) { + if (singletonInstance) { + throw new Error('User should only be set once, when Grafana is starting.'); + } + singletonInstance = instance; +} + +/** + * Used to retrieve the current user. + * + * @internal + * + */ +export function getCurrentUser(): CurrentUser { + if (!singletonInstance) { + throw new Error('User can only be used after Grafana instance has started.'); + } + return singletonInstance; +} diff --git a/packages/grafana-runtime/src/utils/rbac.ts b/packages/grafana-runtime/src/utils/rbac.ts new file mode 100644 index 00000000000..12ffabff4ee --- /dev/null +++ b/packages/grafana-runtime/src/utils/rbac.ts @@ -0,0 +1,18 @@ +import { + userHasPermission, + userHasPermissionInMetadata, + userHasAllPermissions, + userHasAnyPermission, + WithAccessControlMetadata, +} from '@grafana/data'; + +import { getCurrentUser } from '../services/user'; + +export const hasPermission = (action: string) => userHasPermission(action, getCurrentUser()); + +export const hasPermissionInMetadata = (action: string, object: WithAccessControlMetadata) => + userHasPermissionInMetadata(action, object); + +export const hasAllPermissions = (actions: string[]) => userHasAllPermissions(actions, getCurrentUser()); + +export const hasAnyPermission = (actions: string[]) => userHasAnyPermission(actions, getCurrentUser()); diff --git a/public/app/app.ts b/public/app/app.ts index bc19500502b..89460a9fc03 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -38,6 +38,7 @@ import { setReturnToPreviousHook, setPluginExtensionsHook, setPluginComponentHook, + setCurrentUser, } from '@grafana/runtime'; import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView'; import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; @@ -144,6 +145,7 @@ export class GrafanaApp { setEmbeddedDashboard(EmbeddedDashboardLazy); setTimeZoneResolver(() => config.bootData.user.timezone); initGrafanaLive(); + setCurrentUser(contextSrv.user); initAuthConfig(); diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index ab1016fd71a..25f4ed13e1c 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -1,6 +1,14 @@ import { extend } from 'lodash'; -import { AnalyticsSettings, OrgRole, rangeUtil, WithAccessControlMetadata } from '@grafana/data'; +import { + AnalyticsSettings, + OrgRole, + rangeUtil, + WithAccessControlMetadata, + userHasPermission, + userHasPermissionInMetadata, + userHasAnyPermission, +} from '@grafana/data'; import { featureEnabled, getBackendSrv } from '@grafana/runtime'; import { getSessionExpiry } from 'app/core/utils/auth'; import { AccessControlAction, UserPermission } from 'app/types'; @@ -131,12 +139,12 @@ export class ContextSrv { // Checks whether user has required permission hasPermissionInMetadata(action: AccessControlAction | string, object: WithAccessControlMetadata): boolean { - return !!object.accessControl?.[action]; + return userHasPermissionInMetadata(action, object); } // Checks whether user has required permission hasPermission(action: AccessControlAction | string): boolean { - return !!this.user.permissions?.[action]; + return userHasPermission(action, this.user); } isGrafanaVisible() { @@ -171,7 +179,7 @@ export class ContextSrv { // evaluates access control permissions, granting access if the user has any of them evaluatePermission(actions: string[]) { - if (actions.some((action) => this.hasPermission(action))) { + if (userHasAnyPermission(actions, this.user)) { return []; } // Hack to reject when user does not have permission From 3ac109e759c61f2d8dd2b2b849a8aed944f8bb64 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Wed, 26 Jun 2024 11:56:39 -0400 Subject: [PATCH 07/11] AWS: Update deprecated aws-sdk functions from env variable versions (#89643) --- go.mod | 2 +- go.sum | 3 +-- go.work.sum | 4 +-- .../http_client_provider.go | 18 ++++++++++++- pkg/registry/apis/datasource/middleware.go | 3 ++- pkg/tsdb/cloudwatch/cloudwatch.go | 9 +++---- pkg/tsdb/cloudwatch/cloudwatch_test.go | 7 +++-- pkg/tsdb/cloudwatch/models/settings.go | 3 ++- pkg/tsdb/cloudwatch/test_utils.go | 12 ++++----- pkg/tsdb/cloudwatch/time_series_query_test.go | 26 ++++++++++++------- 10 files changed, 54 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index be519c7bd83..3e0d4f4dd67 100644 --- a/go.mod +++ b/go.mod @@ -87,7 +87,7 @@ require ( github.com/grafana/dskit v0.0.0-20240104111617-ea101a3b86eb // @grafana/grafana-backend-group github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 // @grafana/grafana-operator-experience-squad - github.com/grafana/grafana-aws-sdk v0.26.1 // @grafana/aws-datasources + github.com/grafana/grafana-aws-sdk v0.28.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.0.4 // @grafana/partner-datasources github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group diff --git a/go.sum b/go.sum index d6616ff8998..94093f963fb 100644 --- a/go.sum +++ b/go.sum @@ -2317,8 +2317,7 @@ github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQU github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 h1:/of8Z8taCPftShATouOrBVy6GaTTjgQd/VfNiZp/VXQ= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= -github.com/grafana/grafana-aws-sdk v0.26.1 h1:Q9Bb3t1UKiRU3sso4d9gLcZo/r1JxJqIF5dKohsRRWY= -github.com/grafana/grafana-aws-sdk v0.26.1/go.mod h1:ZSVPU7IIJSi5lEg+K3Js+EUpZLXxUaBdaQWH+As1ihI= +github.com/grafana/grafana-aws-sdk v0.28.0 h1:ShdA+msLPGJGWWS1SFUYnF+ch1G3gUOlAdGJi6h4sgU= github.com/grafana/grafana-azure-sdk-go/v2 v2.0.4 h1:z6amQ286IJSBctHf6c+ibJq/v0+TvmEjVkrdMNBd4uY= github.com/grafana/grafana-azure-sdk-go/v2 v2.0.4/go.mod h1:aKlFPE36IDa8qccRg3KbgZX3MQ5xymS3RelT4j6kkVU= github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA= diff --git a/go.work.sum b/go.work.sum index 7eec5551ec7..439079c2cb9 100644 --- a/go.work.sum +++ b/go.work.sum @@ -480,7 +480,6 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4= github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4= -github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= @@ -887,7 +886,6 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= -github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= @@ -945,6 +943,8 @@ github.com/grafana/dataplane/sdata v0.0.7 h1:CImITypIyS1jxijCR6xqKx71JnYAxcwpH9C github.com/grafana/dataplane/sdata v0.0.7/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b h1:Ha+kSIoTutf4ytlVw/SaEclDUloYx0+FXDKJWKhNbE4= github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY= +github.com/grafana/grafana-aws-sdk v0.28.0 h1:ShdA+msLPGJGWWS1SFUYnF+ch1G3gUOlAdGJi6h4sgU= +github.com/grafana/grafana-aws-sdk v0.28.0/go.mod h1:ZSVPU7IIJSi5lEg+K3Js+EUpZLXxUaBdaQWH+As1ihI= github.com/grafana/grafana-plugin-sdk-go v0.212.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= github.com/grafana/grafana-plugin-sdk-go v0.215.0/go.mod h1:nBsh3jRItKQUXDF2BQkiQCPxqrsSQeb+7hiFyJTO1RE= github.com/grafana/grafana-plugin-sdk-go v0.216.0/go.mod h1:FdvSvOliqpVLnytM7e89zCFyYPDE6VOn9SIjVQRvVxM= diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go index 13ef9674c1c..c40dc894ee9 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go @@ -4,7 +4,9 @@ import ( "net/http" "time" + "github.com/grafana/grafana-aws-sdk/pkg/awsds" awssdk "github.com/grafana/grafana-aws-sdk/pkg/sigv4" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/mwitkow/go-conntrack" @@ -42,7 +44,21 @@ func New(cfg *setting.Cfg, validator validations.PluginRequestValidator, tracer // SigV4 signing should be performed after all headers are added if cfg.SigV4AuthEnabled { - middlewares = append(middlewares, awssdk.SigV4Middleware(cfg.SigV4VerboseLogging)) + authSettings := awsds.AuthSettings{ + AllowedAuthProviders: cfg.AWSAllowedAuthProviders, + AssumeRoleEnabled: cfg.AWSAssumeRoleEnabled, + ExternalID: cfg.AWSExternalId, + ListMetricsPageLimit: cfg.AWSListMetricsPageLimit, + SecureSocksDSProxyEnabled: cfg.SecureSocksDSProxy.Enabled, + } + if cfg.AWSSessionDuration != "" { + sessionDuration, err := gtime.ParseDuration(cfg.AWSSessionDuration) + if err == nil { + authSettings.SessionDuration = &sessionDuration + } + } + + middlewares = append(middlewares, awssdk.SigV4MiddlewareWithAuthSettings(cfg.SigV4VerboseLogging, authSettings)) } setDefaultTimeoutOptions(cfg) diff --git a/pkg/registry/apis/datasource/middleware.go b/pkg/registry/apis/datasource/middleware.go index 95d8082320a..b0a4bbc312a 100644 --- a/pkg/registry/apis/datasource/middleware.go +++ b/pkg/registry/apis/datasource/middleware.go @@ -16,7 +16,8 @@ func contextualMiddlewares(ctx context.Context) context.Context { sigv4Settings := awsds.ReadSigV4Settings(ctx) if sigv4Settings.Enabled { - ctx = httpclient.WithContextualMiddleware(ctx, sigv4.SigV4Middleware(sigv4Settings.VerboseLogging)) + authSettings, _ := awsds.ReadAuthSettingsFromContext(ctx) + ctx = httpclient.WithContextualMiddleware(ctx, sigv4.SigV4MiddlewareWithAuthSettings(sigv4Settings.VerboseLogging, *authSettings)) } return ctx diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 11a83b05572..9772e11de9d 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -80,7 +80,7 @@ type CloudWatchService struct { } type SessionCache interface { - GetSession(c awsds.SessionConfig) (*session.Session, error) + GetSessionWithAuthSettings(c awsds.GetSessionConfig, as awsds.AuthSettings) (*session.Session, error) } func newExecutor(im instancemgmt.InstanceManager, logger log.Logger) *cloudWatchExecutor { @@ -299,7 +299,7 @@ func (ds *DataSource) newSession(region string) (*session.Session, error) { } region = ds.Settings.Region } - sess, err := ds.sessions.GetSession(awsds.SessionConfig{ + sess, err := ds.sessions.GetSessionWithAuthSettings(awsds.GetSessionConfig{ // https://github.com/grafana/grafana/issues/46365 // HTTPClient: instance.HTTPClient, Settings: awsds.AWSDatasourceSettings{ @@ -313,9 +313,8 @@ func (ds *DataSource) newSession(region string) (*session.Session, error) { AccessKey: ds.Settings.AccessKey, SecretKey: ds.Settings.SecretKey, }, - UserAgentName: aws.String("Cloudwatch"), - AuthSettings: &ds.Settings.GrafanaSettings, - }) + UserAgentName: aws.String("Cloudwatch")}, + ds.Settings.GrafanaSettings) if err != nil { return nil, err } diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go index c2287c279c5..5e36adcda4c 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go @@ -191,7 +191,7 @@ func Test_CheckHealth(t *testing.T) { im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { return DataSource{ Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "us-east-1"}}, - sessions: &fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) { + sessions: &fakeSessionCache{getSessionWithAuthSettings: func(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error) { return nil, fmt.Errorf("some sessions error") }}, }, nil @@ -229,9 +229,8 @@ func TestNewSession_passes_authSettings(t *testing.T) { }, GrafanaSettings: expectedSettings, }, - sessions: &fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) { - assert.NotNil(t, c.AuthSettings) - assert.Equal(t, expectedSettings, *c.AuthSettings) + sessions: &fakeSessionCache{getSessionWithAuthSettings: func(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error) { + assert.Equal(t, expectedSettings, a) return &session.Session{ Config: &aws.Config{}, }, nil diff --git a/pkg/tsdb/cloudwatch/models/settings.go b/pkg/tsdb/cloudwatch/models/settings.go index 9221fe42fb4..4941684917d 100644 --- a/pkg/tsdb/cloudwatch/models/settings.go +++ b/pkg/tsdb/cloudwatch/models/settings.go @@ -43,7 +43,8 @@ func LoadCloudWatchSettings(ctx context.Context, config backend.DataSourceInstan instance.LogsTimeout = Duration{30 * time.Minute} } - instance.GrafanaSettings = *awsds.ReadAuthSettings(ctx) + authSettings, _ := awsds.ReadAuthSettingsFromContext(ctx) + instance.GrafanaSettings = *authSettings return instance, nil } diff --git a/pkg/tsdb/cloudwatch/test_utils.go b/pkg/tsdb/cloudwatch/test_utils.go index 9e7f531e194..5d56ee96222 100644 --- a/pkg/tsdb/cloudwatch/test_utils.go +++ b/pkg/tsdb/cloudwatch/test_utils.go @@ -224,21 +224,21 @@ type mockSessionCache struct { mock.Mock } -func (c *mockSessionCache) GetSession(config awsds.SessionConfig) (*session.Session, error) { +func (c *mockSessionCache) GetSessionWithAuthSettings(config awsds.GetSessionConfig, auth awsds.AuthSettings) (*session.Session, error) { args := c.Called(config) return args.Get(0).(*session.Session), args.Error(1) } type fakeSessionCache struct { - getSession func(c awsds.SessionConfig) (*session.Session, error) - calledRegions []string + getSessionWithAuthSettings func(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error) + calledRegions []string } -func (s *fakeSessionCache) GetSession(c awsds.SessionConfig) (*session.Session, error) { +func (s *fakeSessionCache) GetSessionWithAuthSettings(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error) { s.calledRegions = append(s.calledRegions, c.Settings.Region) - if s.getSession != nil { - return s.getSession(c) + if s.getSessionWithAuthSettings != nil { + return s.getSessionWithAuthSettings(c, a) } return &session.Session{ Config: &aws.Config{}, diff --git a/pkg/tsdb/cloudwatch/time_series_query_test.go b/pkg/tsdb/cloudwatch/time_series_query_test.go index d1a1d13b47c..dcc6ba0cfb1 100644 --- a/pkg/tsdb/cloudwatch/time_series_query_test.go +++ b/pkg/tsdb/cloudwatch/time_series_query_test.go @@ -147,10 +147,12 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe return &mockMetricClient } - t.Run("Queries with the same region should call GetSession with that region 1 time and call GetMetricDataWithContext 1 time", func(t *testing.T) { + t.Run("Queries with the same region should call GetSessionWithAuthSettings with that region 1 time and call GetMetricDataWithContext 1 time", func(t *testing.T) { mockSessionCache := &mockSessionCache{} - mockSessionCache.On("GetSession", mock.MatchedBy( - func(config awsds.SessionConfig) bool { return config.Settings.Region == "us-east-1" })). // region from queries is asserted here + mockSessionCache.On("GetSessionWithAuthSettings", mock.MatchedBy( + func(config awsds.GetSessionConfig) bool { + return config.Settings.Region == "us-east-1" + })). // region from queries is asserted here Return(&session.Session{Config: &aws.Config{}}, nil).Once() im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { return DataSource{Settings: models.CloudWatchSettings{}, sessions: mockSessionCache}, nil @@ -198,14 +200,18 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe // GetMetricData is asserted to have been called 1 time for the 1 region present in the queries }) - t.Run("3 queries with 2 regions calls GetSession 2 times and calls GetMetricDataWithContext 2 times", func(t *testing.T) { + t.Run("3 queries with 2 regions calls GetSessionWithAuthSettings 2 times and calls GetMetricDataWithContext 2 times", func(t *testing.T) { sessionCache := &mockSessionCache{} - sessionCache.On("GetSession", mock.MatchedBy( - func(config awsds.SessionConfig) bool { return config.Settings.Region == "us-east-1" })). - Return(&session.Session{Config: &aws.Config{}}, nil, nil).Once() - sessionCache.On("GetSession", mock.MatchedBy( - func(config awsds.SessionConfig) bool { return config.Settings.Region == "us-east-2" })). - Return(&session.Session{Config: &aws.Config{}}, nil, nil).Once() + sessionCache.On("GetSessionWithAuthSettings", mock.MatchedBy( + func(config awsds.GetSessionConfig) bool { + return config.Settings.Region == "us-east-1" + })). + Return(&session.Session{Config: &aws.Config{}}, nil).Once() + sessionCache.On("GetSessionWithAuthSettings", mock.MatchedBy( + func(config awsds.GetSessionConfig) bool { + return config.Settings.Region == "us-east-2" + })). + Return(&session.Session{Config: &aws.Config{}}, nil).Once() im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { return DataSource{Settings: models.CloudWatchSettings{}, sessions: sessionCache}, nil From c575c06d63135939fe9dd7ecfd674971df054405 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Wed, 26 Jun 2024 11:12:44 -0500 Subject: [PATCH 08/11] Table Panel: Fix Image hover without datalinks (#89751) * Fix image hover * Prettier * Fix height offset --- packages/grafana-ui/src/components/Table/ImageCell.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Table/ImageCell.tsx b/packages/grafana-ui/src/components/Table/ImageCell.tsx index 3f507aff232..83b8b888911 100644 --- a/packages/grafana-ui/src/components/Table/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/ImageCell.tsx @@ -16,7 +16,14 @@ export const ImageCell = (props: TableCellProps) => { return (
- {!hasLinks && } + {!hasLinks && ( + + )} {hasLinks && ( Date: Wed, 26 Jun 2024 13:03:25 -0400 Subject: [PATCH 09/11] Alerting: Receiver API (#89707) --- apps/alerting/notifications/receiver.cue | 32 +++ .../v0alpha1/receiver_spec.go | 21 ++ .../v0alpha1/register.go | 7 + .../alerting_notifications/v0alpha1/types.go | 81 +++++++ .../v0alpha1/zz_generated.deepcopy.go | 121 ++++++++++ .../v0alpha1/zz_generated.openapi.go | 198 +++++++++++++++ ...enerated.openapi_violation_exceptions.list | 1 + .../v0alpha1/integration.go | 69 ++++++ .../v0alpha1/receiver.go | 196 +++++++++++++++ .../v0alpha1/receiverspec.go | 39 +++ pkg/generated/applyconfiguration/utils.go | 6 + .../v0alpha1/alerting_notifications_client.go | 5 + .../fake_alerting_notifications_client.go | 4 + .../v0alpha1/fake/fake_receiver.go | 140 +++++++++++ .../v0alpha1/generated_expansion.go | 2 + .../v0alpha1/receiver.go | 194 +++++++++++++++ .../v0alpha1/interface.go | 7 + .../v0alpha1/receiver.go | 76 ++++++ .../informers/externalversions/generic.go | 2 + .../v0alpha1/expansion_generated.go | 8 + .../v0alpha1/receiver.go | 85 +++++++ .../notifications/receiver/authorize.go | 53 +++++ .../notifications/receiver/conversions.go | 101 ++++++++ .../notifications/receiver/legacy_storage.go | 225 ++++++++++++++++++ .../notifications/receiver/storage.go | 80 +++++++ .../apis/alerting/notifications/register.go | 9 + 26 files changed, 1762 insertions(+) create mode 100644 apps/alerting/notifications/receiver.cue create mode 100644 pkg/apis/alerting_notifications/v0alpha1/receiver_spec.go create mode 100644 pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/integration.go create mode 100644 pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/receiver.go create mode 100644 pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/receiverspec.go create mode 100644 pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_receiver.go create mode 100644 pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/receiver.go create mode 100644 pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/receiver.go create mode 100644 pkg/generated/listers/alerting_notifications/v0alpha1/receiver.go create mode 100644 pkg/registry/apis/alerting/notifications/receiver/authorize.go create mode 100644 pkg/registry/apis/alerting/notifications/receiver/conversions.go create mode 100644 pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go create mode 100644 pkg/registry/apis/alerting/notifications/receiver/storage.go diff --git a/apps/alerting/notifications/receiver.cue b/apps/alerting/notifications/receiver.cue new file mode 100644 index 00000000000..3fe31a26684 --- /dev/null +++ b/apps/alerting/notifications/receiver.cue @@ -0,0 +1,32 @@ +package core + +receiver: { + kind: "Receiver" + group: "notifications" + apiResource: { + groupOverride: "notifications.alerting.grafana.app" + } + codegen: { + frontend: false + backend: true + } + pluralName: "Receivers" + current: "v0alpha1" + versions: { + "v0alpha1": { + schema: { + #Integration: { + uid?: string + type: string + disableResolveMessage?: bool + settings: bytes + secureFields?: [string]: bool + } + spec: { + title: string + integrations : [...#Integration] + } + } + } + } +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/receiver_spec.go b/pkg/apis/alerting_notifications/v0alpha1/receiver_spec.go new file mode 100644 index 00000000000..9adcef30e3f --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/receiver_spec.go @@ -0,0 +1,21 @@ +package v0alpha1 + +// Integration defines model for Integration. +// +k8s:openapi-gen=true +type Integration struct { + DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"` + // +mapType=atomic + SecureFields map[string]bool `json:"SecureFields,omitempty"` + // +listType=atomic + Settings []byte `json:"settings"` + Type string `json:"type"` + Uid *string `json:"uid,omitempty"` +} + +// ReceiverSpec defines model for Spec. +// +k8s:openapi-gen=true +type ReceiverSpec struct { + // +listType=atomic + Integrations []Integration `json:"integrations"` + Title string `json:"title"` +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/register.go b/pkg/apis/alerting_notifications/v0alpha1/register.go index a6be83e04ef..cbf36e4aab4 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/register.go +++ b/pkg/apis/alerting_notifications/v0alpha1/register.go @@ -24,6 +24,11 @@ var ( func() runtime.Object { return &TimeInterval{} }, func() runtime.Object { return &TimeIntervalList{} }, ) + ReceiverResourceInfo = common.NewResourceInfo(GROUP, VERSION, + "receivers", "receiver", "Receiver", + func() runtime.Object { return &Receiver{} }, + func() runtime.Object { return &ReceiverList{} }, + ) // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} // SchemaBuilder is used by standard codegen @@ -42,6 +47,8 @@ func AddKnownTypesGroup(scheme *runtime.Scheme, g schema.GroupVersion) error { scheme.AddKnownTypes(g, &TimeInterval{}, &TimeIntervalList{}, + &Receiver{}, + &ReceiverList{}, ) metav1.AddToGroupVersion(scheme, g) return nil diff --git a/pkg/apis/alerting_notifications/v0alpha1/types.go b/pkg/apis/alerting_notifications/v0alpha1/types.go index 6a970ad4516..ec5edaf6e6e 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/types.go +++ b/pkg/apis/alerting_notifications/v0alpha1/types.go @@ -85,3 +85,84 @@ type TimeIntervalList struct { metav1.ListMeta `json:"metadata"` Items []TimeInterval `json:"items"` } + +// Receivers --------------------------------- + +// +genclient +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type Receiver struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec ReceiverSpec `json:"spec"` +} + +func (o *Receiver) GetSpec() any { + return o.Spec +} + +func (o *Receiver) SetSpec(spec any) error { + cast, ok := spec.(ReceiverSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Receiver) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Receiver) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Receiver) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Receiver) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Receiver) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Receiver) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ReceiverList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []Receiver `json:"items"` +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go index 918c3d81cb2..69f57b975f8 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go @@ -11,6 +11,44 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Integration) DeepCopyInto(out *Integration) { + *out = *in + if in.DisableResolveMessage != nil { + in, out := &in.DisableResolveMessage, &out.DisableResolveMessage + *out = new(bool) + **out = **in + } + if in.SecureFields != nil { + in, out := &in.SecureFields, &out.SecureFields + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.Uid != nil { + in, out := &in.Uid, &out.Uid + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Integration. +func (in *Integration) DeepCopy() *Integration { + if in == nil { + return nil + } + out := new(Integration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Interval) DeepCopyInto(out *Interval) { *out = *in @@ -57,6 +95,89 @@ func (in *Interval) DeepCopy() *Interval { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Receiver) DeepCopyInto(out *Receiver) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Receiver. +func (in *Receiver) DeepCopy() *Receiver { + if in == nil { + return nil + } + out := new(Receiver) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Receiver) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReceiverList) DeepCopyInto(out *ReceiverList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Receiver, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReceiverList. +func (in *ReceiverList) DeepCopy() *ReceiverList { + if in == nil { + return nil + } + out := new(ReceiverList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ReceiverList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReceiverSpec) DeepCopyInto(out *ReceiverSpec) { + *out = *in + if in.Integrations != nil { + in, out := &in.Integrations, &out.Integrations + *out = make([]Integration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReceiverSpec. +func (in *ReceiverSpec) DeepCopy() *ReceiverSpec { + if in == nil { + return nil + } + out := new(ReceiverSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TimeInterval) DeepCopyInto(out *TimeInterval) { *out = *in diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go index 8d5ff04f0a0..b0125bb8ca1 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go @@ -16,7 +16,11 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Integration": schema_pkg_apis_alerting_notifications_v0alpha1_Integration(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval": schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Receiver": schema_pkg_apis_alerting_notifications_v0alpha1_Receiver(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverList": schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverList(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverSpec": schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverSpec(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeInterval": schema_pkg_apis_alerting_notifications_v0alpha1_TimeInterval(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalList": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalList(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalSpec(ref), @@ -24,6 +28,70 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA } } +func schema_pkg_apis_alerting_notifications_v0alpha1_Integration(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Integration defines model for Integration.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "disableResolveMessage": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "SecureFields": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + "settings": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "byte", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"settings", "type"}, + }, + }, + } +} + func schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -144,6 +212,136 @@ func schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref common.Referen } } +func schema_pkg_apis_alerting_notifications_v0alpha1_Receiver(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Receiver"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Receiver", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ReceiverSpec defines model for Spec.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "integrations": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Integration"), + }, + }, + }, + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"integrations", "title"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Integration"}, + } +} + func schema_pkg_apis_alerting_notifications_v0alpha1_TimeInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list index 7b756e64dfd..ac3e0fa0bf0 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,3 +1,4 @@ +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Integration,SecureFields API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Interval,DaysOfMonth API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeIntervalSpec,TimeIntervals API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeRange,EndTime diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/integration.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/integration.go new file mode 100644 index 00000000000..b1bfb5e1be5 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/integration.go @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// IntegrationApplyConfiguration represents an declarative configuration of the Integration type for use +// with apply. +type IntegrationApplyConfiguration struct { + DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"` + SecureFields map[string]bool `json:"SecureFields,omitempty"` + Settings []byte `json:"settings,omitempty"` + Type *string `json:"type,omitempty"` + Uid *string `json:"uid,omitempty"` +} + +// IntegrationApplyConfiguration constructs an declarative configuration of the Integration type for use with +// apply. +func Integration() *IntegrationApplyConfiguration { + return &IntegrationApplyConfiguration{} +} + +// WithDisableResolveMessage sets the DisableResolveMessage field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DisableResolveMessage field is set to the value of the last call. +func (b *IntegrationApplyConfiguration) WithDisableResolveMessage(value bool) *IntegrationApplyConfiguration { + b.DisableResolveMessage = &value + return b +} + +// WithSecureFields puts the entries into the SecureFields field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the SecureFields field, +// overwriting an existing map entries in SecureFields field with the same key. +func (b *IntegrationApplyConfiguration) WithSecureFields(entries map[string]bool) *IntegrationApplyConfiguration { + if b.SecureFields == nil && len(entries) > 0 { + b.SecureFields = make(map[string]bool, len(entries)) + } + for k, v := range entries { + b.SecureFields[k] = v + } + return b +} + +// WithSettings adds the given value to the Settings field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Settings field. +func (b *IntegrationApplyConfiguration) WithSettings(values ...byte) *IntegrationApplyConfiguration { + for i := range values { + b.Settings = append(b.Settings, values[i]) + } + return b +} + +// WithType sets the Type field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Type field is set to the value of the last call. +func (b *IntegrationApplyConfiguration) WithType(value string) *IntegrationApplyConfiguration { + b.Type = &value + return b +} + +// WithUid sets the Uid field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Uid field is set to the value of the last call. +func (b *IntegrationApplyConfiguration) WithUid(value string) *IntegrationApplyConfiguration { + b.Uid = &value + return b +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/receiver.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/receiver.go new file mode 100644 index 00000000000..94565883187 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/receiver.go @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// ReceiverApplyConfiguration represents an declarative configuration of the Receiver type for use +// with apply. +type ReceiverApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *ReceiverSpecApplyConfiguration `json:"spec,omitempty"` +} + +// Receiver constructs an declarative configuration of the Receiver type for use with +// apply. +func Receiver(name, namespace string) *ReceiverApplyConfiguration { + b := &ReceiverApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("Receiver") + b.WithAPIVersion("notifications.alerting.grafana.app/v0alpha1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithKind(value string) *ReceiverApplyConfiguration { + b.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithAPIVersion(value string) *ReceiverApplyConfiguration { + b.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithName(value string) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithGenerateName(value string) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithNamespace(value string) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithUID(value types.UID) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithResourceVersion(value string) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithGeneration(value int64) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithCreationTimestamp(value metav1.Time) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *ReceiverApplyConfiguration) WithLabels(entries map[string]string) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *ReceiverApplyConfiguration) WithAnnotations(entries map[string]string) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *ReceiverApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.OwnerReferences = append(b.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *ReceiverApplyConfiguration) WithFinalizers(values ...string) *ReceiverApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.Finalizers = append(b.Finalizers, values[i]) + } + return b +} + +func (b *ReceiverApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *ReceiverApplyConfiguration) WithSpec(value *ReceiverSpecApplyConfiguration) *ReceiverApplyConfiguration { + b.Spec = value + return b +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/receiverspec.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/receiverspec.go new file mode 100644 index 00000000000..d7fc1ba3039 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/receiverspec.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// ReceiverSpecApplyConfiguration represents an declarative configuration of the ReceiverSpec type for use +// with apply. +type ReceiverSpecApplyConfiguration struct { + Integrations []IntegrationApplyConfiguration `json:"integrations,omitempty"` + Title *string `json:"title,omitempty"` +} + +// ReceiverSpecApplyConfiguration constructs an declarative configuration of the ReceiverSpec type for use with +// apply. +func ReceiverSpec() *ReceiverSpecApplyConfiguration { + return &ReceiverSpecApplyConfiguration{} +} + +// WithIntegrations adds the given value to the Integrations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Integrations field. +func (b *ReceiverSpecApplyConfiguration) WithIntegrations(values ...*IntegrationApplyConfiguration) *ReceiverSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithIntegrations") + } + b.Integrations = append(b.Integrations, *values[i]) + } + return b +} + +// WithTitle sets the Title field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Title field is set to the value of the last call. +func (b *ReceiverSpecApplyConfiguration) WithTitle(value string) *ReceiverSpecApplyConfiguration { + b.Title = &value + return b +} diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go index 561bf3385e5..19f42b751e2 100644 --- a/pkg/generated/applyconfiguration/utils.go +++ b/pkg/generated/applyconfiguration/utils.go @@ -17,8 +17,14 @@ import ( func ForKind(kind schema.GroupVersionKind) interface{} { switch kind { // Group=notifications.alerting.grafana.app, Version=v0alpha1 + case v0alpha1.SchemeGroupVersion.WithKind("Integration"): + return &alertingnotificationsv0alpha1.IntegrationApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("Interval"): return &alertingnotificationsv0alpha1.IntervalApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("Receiver"): + return &alertingnotificationsv0alpha1.ReceiverApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("ReceiverSpec"): + return &alertingnotificationsv0alpha1.ReceiverSpecApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("TimeInterval"): return &alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("TimeIntervalSpec"): diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go index 5c81a66d6ec..53dfb5ad0d8 100644 --- a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go @@ -14,6 +14,7 @@ import ( type NotificationsV0alpha1Interface interface { RESTClient() rest.Interface + ReceiversGetter TimeIntervalsGetter } @@ -22,6 +23,10 @@ type NotificationsV0alpha1Client struct { restClient rest.Interface } +func (c *NotificationsV0alpha1Client) Receivers(namespace string) ReceiverInterface { + return newReceivers(c, namespace) +} + func (c *NotificationsV0alpha1Client) TimeIntervals(namespace string) TimeIntervalInterface { return newTimeIntervals(c, namespace) } diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go index ece678250d8..84bcadcd8e2 100644 --- a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go @@ -14,6 +14,10 @@ type FakeNotificationsV0alpha1 struct { *testing.Fake } +func (c *FakeNotificationsV0alpha1) Receivers(namespace string) v0alpha1.ReceiverInterface { + return &FakeReceivers{c, namespace} +} + func (c *FakeNotificationsV0alpha1) TimeIntervals(namespace string) v0alpha1.TimeIntervalInterface { return &FakeTimeIntervals{c, namespace} } diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_receiver.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_receiver.go new file mode 100644 index 00000000000..bf508c34049 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_receiver.go @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeReceivers implements ReceiverInterface +type FakeReceivers struct { + Fake *FakeNotificationsV0alpha1 + ns string +} + +var receiversResource = v0alpha1.SchemeGroupVersion.WithResource("receivers") + +var receiversKind = v0alpha1.SchemeGroupVersion.WithKind("Receiver") + +// Get takes name of the receiver, and returns the corresponding receiver object, and an error if there is any. +func (c *FakeReceivers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v0alpha1.Receiver, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(receiversResource, c.ns, name), &v0alpha1.Receiver{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.Receiver), err +} + +// List takes label and field selectors, and returns the list of Receivers that match those selectors. +func (c *FakeReceivers) List(ctx context.Context, opts v1.ListOptions) (result *v0alpha1.ReceiverList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(receiversResource, receiversKind, c.ns, opts), &v0alpha1.ReceiverList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v0alpha1.ReceiverList{ListMeta: obj.(*v0alpha1.ReceiverList).ListMeta} + for _, item := range obj.(*v0alpha1.ReceiverList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested receivers. +func (c *FakeReceivers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(receiversResource, c.ns, opts)) + +} + +// Create takes the representation of a receiver and creates it. Returns the server's representation of the receiver, and an error, if there is any. +func (c *FakeReceivers) Create(ctx context.Context, receiver *v0alpha1.Receiver, opts v1.CreateOptions) (result *v0alpha1.Receiver, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(receiversResource, c.ns, receiver), &v0alpha1.Receiver{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.Receiver), err +} + +// Update takes the representation of a receiver and updates it. Returns the server's representation of the receiver, and an error, if there is any. +func (c *FakeReceivers) Update(ctx context.Context, receiver *v0alpha1.Receiver, opts v1.UpdateOptions) (result *v0alpha1.Receiver, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(receiversResource, c.ns, receiver), &v0alpha1.Receiver{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.Receiver), err +} + +// Delete takes name of the receiver and deletes it. Returns an error if one occurs. +func (c *FakeReceivers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(receiversResource, c.ns, name, opts), &v0alpha1.Receiver{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeReceivers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(receiversResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v0alpha1.ReceiverList{}) + return err +} + +// Patch applies the patch and returns the patched receiver. +func (c *FakeReceivers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.Receiver, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(receiversResource, c.ns, name, pt, data, subresources...), &v0alpha1.Receiver{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.Receiver), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied receiver. +func (c *FakeReceivers) Apply(ctx context.Context, receiver *alertingnotificationsv0alpha1.ReceiverApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.Receiver, err error) { + if receiver == nil { + return nil, fmt.Errorf("receiver provided to Apply must not be nil") + } + data, err := json.Marshal(receiver) + if err != nil { + return nil, err + } + name := receiver.Name + if name == nil { + return nil, fmt.Errorf("receiver.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(receiversResource, c.ns, *name, types.ApplyPatchType, data), &v0alpha1.Receiver{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.Receiver), err +} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go index cf6e218f4ae..c30d1403330 100644 --- a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go @@ -4,4 +4,6 @@ package v0alpha1 +type ReceiverExpansion interface{} + type TimeIntervalExpansion interface{} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/receiver.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/receiver.go new file mode 100644 index 00000000000..1f99044e245 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/receiver.go @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + "context" + json "encoding/json" + "fmt" + "time" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1" + scheme "github.com/grafana/grafana/pkg/generated/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// ReceiversGetter has a method to return a ReceiverInterface. +// A group's client should implement this interface. +type ReceiversGetter interface { + Receivers(namespace string) ReceiverInterface +} + +// ReceiverInterface has methods to work with Receiver resources. +type ReceiverInterface interface { + Create(ctx context.Context, receiver *v0alpha1.Receiver, opts v1.CreateOptions) (*v0alpha1.Receiver, error) + Update(ctx context.Context, receiver *v0alpha1.Receiver, opts v1.UpdateOptions) (*v0alpha1.Receiver, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v0alpha1.Receiver, error) + List(ctx context.Context, opts v1.ListOptions) (*v0alpha1.ReceiverList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.Receiver, err error) + Apply(ctx context.Context, receiver *alertingnotificationsv0alpha1.ReceiverApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.Receiver, err error) + ReceiverExpansion +} + +// receivers implements ReceiverInterface +type receivers struct { + client rest.Interface + ns string +} + +// newReceivers returns a Receivers +func newReceivers(c *NotificationsV0alpha1Client, namespace string) *receivers { + return &receivers{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the receiver, and returns the corresponding receiver object, and an error if there is any. +func (c *receivers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v0alpha1.Receiver, err error) { + result = &v0alpha1.Receiver{} + err = c.client.Get(). + Namespace(c.ns). + Resource("receivers"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of Receivers that match those selectors. +func (c *receivers) List(ctx context.Context, opts v1.ListOptions) (result *v0alpha1.ReceiverList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v0alpha1.ReceiverList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("receivers"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested receivers. +func (c *receivers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("receivers"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a receiver and creates it. Returns the server's representation of the receiver, and an error, if there is any. +func (c *receivers) Create(ctx context.Context, receiver *v0alpha1.Receiver, opts v1.CreateOptions) (result *v0alpha1.Receiver, err error) { + result = &v0alpha1.Receiver{} + err = c.client.Post(). + Namespace(c.ns). + Resource("receivers"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(receiver). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a receiver and updates it. Returns the server's representation of the receiver, and an error, if there is any. +func (c *receivers) Update(ctx context.Context, receiver *v0alpha1.Receiver, opts v1.UpdateOptions) (result *v0alpha1.Receiver, err error) { + result = &v0alpha1.Receiver{} + err = c.client.Put(). + Namespace(c.ns). + Resource("receivers"). + Name(receiver.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(receiver). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the receiver and deletes it. Returns an error if one occurs. +func (c *receivers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("receivers"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *receivers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("receivers"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched receiver. +func (c *receivers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.Receiver, err error) { + result = &v0alpha1.Receiver{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("receivers"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied receiver. +func (c *receivers) Apply(ctx context.Context, receiver *alertingnotificationsv0alpha1.ReceiverApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.Receiver, err error) { + if receiver == nil { + return nil, fmt.Errorf("receiver provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(receiver) + if err != nil { + return nil, err + } + name := receiver.Name + if name == nil { + return nil, fmt.Errorf("receiver.Name must be provided to Apply") + } + result = &v0alpha1.Receiver{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("receivers"). + Name(*name). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go index 2095a04954a..64c33c7fc5e 100644 --- a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go +++ b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go @@ -10,6 +10,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // Receivers returns a ReceiverInformer. + Receivers() ReceiverInformer // TimeIntervals returns a TimeIntervalInformer. TimeIntervals() TimeIntervalInformer } @@ -25,6 +27,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// Receivers returns a ReceiverInformer. +func (v *version) Receivers() ReceiverInformer { + return &receiverInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // TimeIntervals returns a TimeIntervalInformer. func (v *version) TimeIntervals() TimeIntervalInformer { return &timeIntervalInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/receiver.go b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/receiver.go new file mode 100644 index 00000000000..194cb68a80d --- /dev/null +++ b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/receiver.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by informer-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + "context" + time "time" + + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + versioned "github.com/grafana/grafana/pkg/generated/clientset/versioned" + internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces" + v0alpha1 "github.com/grafana/grafana/pkg/generated/listers/alerting_notifications/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ReceiverInformer provides access to a shared informer and lister for +// Receivers. +type ReceiverInformer interface { + Informer() cache.SharedIndexInformer + Lister() v0alpha1.ReceiverLister +} + +type receiverInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewReceiverInformer constructs a new informer for Receiver type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewReceiverInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredReceiverInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredReceiverInformer constructs a new informer for Receiver type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredReceiverInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NotificationsV0alpha1().Receivers(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NotificationsV0alpha1().Receivers(namespace).Watch(context.TODO(), options) + }, + }, + &alertingnotificationsv0alpha1.Receiver{}, + resyncPeriod, + indexers, + ) +} + +func (f *receiverInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredReceiverInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *receiverInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&alertingnotificationsv0alpha1.Receiver{}, f.defaultInformer) +} + +func (f *receiverInformer) Lister() v0alpha1.ReceiverLister { + return v0alpha1.NewReceiverLister(f.Informer().GetIndexer()) +} diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go index 137fbb60117..36b920d2c47 100644 --- a/pkg/generated/informers/externalversions/generic.go +++ b/pkg/generated/informers/externalversions/generic.go @@ -40,6 +40,8 @@ func (f *genericInformer) Lister() cache.GenericLister { func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=notifications.alerting.grafana.app, Version=v0alpha1 + case v0alpha1.SchemeGroupVersion.WithResource("receivers"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().Receivers().Informer()}, nil case v0alpha1.SchemeGroupVersion.WithResource("timeintervals"): return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().TimeIntervals().Informer()}, nil diff --git a/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go b/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go index 65bad473129..ed4d4f239ae 100644 --- a/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go +++ b/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go @@ -4,6 +4,14 @@ package v0alpha1 +// ReceiverListerExpansion allows custom methods to be added to +// ReceiverLister. +type ReceiverListerExpansion interface{} + +// ReceiverNamespaceListerExpansion allows custom methods to be added to +// ReceiverNamespaceLister. +type ReceiverNamespaceListerExpansion interface{} + // TimeIntervalListerExpansion allows custom methods to be added to // TimeIntervalLister. type TimeIntervalListerExpansion interface{} diff --git a/pkg/generated/listers/alerting_notifications/v0alpha1/receiver.go b/pkg/generated/listers/alerting_notifications/v0alpha1/receiver.go new file mode 100644 index 00000000000..990dbf51ce5 --- /dev/null +++ b/pkg/generated/listers/alerting_notifications/v0alpha1/receiver.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by lister-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// ReceiverLister helps list Receivers. +// All objects returned here must be treated as read-only. +type ReceiverLister interface { + // List lists all Receivers in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v0alpha1.Receiver, err error) + // Receivers returns an object that can list and get Receivers. + Receivers(namespace string) ReceiverNamespaceLister + ReceiverListerExpansion +} + +// receiverLister implements the ReceiverLister interface. +type receiverLister struct { + indexer cache.Indexer +} + +// NewReceiverLister returns a new ReceiverLister. +func NewReceiverLister(indexer cache.Indexer) ReceiverLister { + return &receiverLister{indexer: indexer} +} + +// List lists all Receivers in the indexer. +func (s *receiverLister) List(selector labels.Selector) (ret []*v0alpha1.Receiver, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v0alpha1.Receiver)) + }) + return ret, err +} + +// Receivers returns an object that can list and get Receivers. +func (s *receiverLister) Receivers(namespace string) ReceiverNamespaceLister { + return receiverNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ReceiverNamespaceLister helps list and get Receivers. +// All objects returned here must be treated as read-only. +type ReceiverNamespaceLister interface { + // List lists all Receivers in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v0alpha1.Receiver, err error) + // Get retrieves the Receiver from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v0alpha1.Receiver, error) + ReceiverNamespaceListerExpansion +} + +// receiverNamespaceLister implements the ReceiverNamespaceLister +// interface. +type receiverNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all Receivers in the indexer for a given namespace. +func (s receiverNamespaceLister) List(selector labels.Selector) (ret []*v0alpha1.Receiver, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v0alpha1.Receiver)) + }) + return ret, err +} + +// Get retrieves the Receiver from the indexer for a given namespace and name. +func (s receiverNamespaceLister) Get(name string) (*v0alpha1.Receiver, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v0alpha1.Resource("receiver"), name) + } + return obj.(*v0alpha1.Receiver), nil +} diff --git a/pkg/registry/apis/alerting/notifications/receiver/authorize.go b/pkg/registry/apis/alerting/notifications/receiver/authorize.go new file mode 100644 index 00000000000..f06c6862c17 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/receiver/authorize.go @@ -0,0 +1,53 @@ +package receiver + +import ( + "context" + + "k8s.io/apiserver/pkg/authorization/authorizer" + + "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +func Authorize(ctx context.Context, ac accesscontrol.AccessControl, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if attr.GetResource() != resourceInfo.GroupResource().Resource { + return authorizer.DecisionNoOpinion, "", nil + } + user, err := appcontext.User(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + var action accesscontrol.Evaluator + switch attr.GetVerb() { + case "patch": + fallthrough + case "create": + fallthrough // TODO: Add alert.notifications.receivers:create permission + case "update": + action = accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), // TODO: Add alert.notifications.receivers:write permission + ) + case "deletecollection": + fallthrough + case "delete": + action = accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), // TODO: Add alert.notifications.receivers:delete permission + ) + } + + eval := accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversRead), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversReadSecrets), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsRead), + ) + if action != nil { + eval = accesscontrol.EvalAll(eval, action) + } + + ok, err := ac.Evaluate(ctx, user, eval) + if ok { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", err +} diff --git a/pkg/registry/apis/alerting/notifications/receiver/conversions.go b/pkg/registry/apis/alerting/notifications/receiver/conversions.go new file mode 100644 index 00000000000..cd38ad5e805 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/receiver/conversions.go @@ -0,0 +1,101 @@ +package receiver + +import ( + "fmt" + "hash/fnv" + + "github.com/prometheus/alertmanager/config" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +func getUID(t definitions.GettableApiReceiver) string { + sum := fnv.New64() + _, _ = sum.Write([]byte(t.Name)) + return fmt.Sprintf("%016x", sum.Sum64()) +} + +func convertToK8sResources(orgID int64, receivers []definitions.GettableApiReceiver, namespacer request.NamespaceMapper) (*model.ReceiverList, error) { + result := &model.ReceiverList{ + Items: make([]model.Receiver, 0, len(receivers)), + } + for _, receiver := range receivers { + k8sResource, err := convertToK8sResource(orgID, receiver, namespacer) + if err != nil { + return nil, err + } + result.Items = append(result.Items, *k8sResource) + } + return result, nil +} + +func convertToK8sResource(orgID int64, receiver definitions.GettableApiReceiver, namespacer request.NamespaceMapper) (*model.Receiver, error) { + spec := model.ReceiverSpec{ + Title: receiver.Receiver.Name, + } + provenance := definitions.Provenance(models.ProvenanceNone) + for _, integration := range receiver.GrafanaManagedReceivers { + if integration.Provenance != receiver.GrafanaManagedReceivers[0].Provenance { + return nil, fmt.Errorf("all integrations must have the same provenance") + } + provenance = integration.Provenance + spec.Integrations = append(spec.Integrations, model.Integration{ + Uid: &integration.UID, + Type: integration.Type, + DisableResolveMessage: &integration.DisableResolveMessage, + Settings: integration.Settings, + SecureFields: integration.SecureFields, + }) + } + + uid := getUID(receiver) // TODO replace to stable UID when we switch to normal storage + return &model.Receiver{ + TypeMeta: resourceInfo.TypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID(uid), // This is needed to make PATCH work + Name: uid, // TODO replace to stable UID when we switch to normal storage + Namespace: namespacer(orgID), + Annotations: map[string]string{ // TODO find a better place for provenance? + "grafana.com/provenance": string(provenance), + }, + ResourceVersion: "", // TODO: Implement optimistic concurrency. + }, + Spec: spec, + }, nil +} + +func convertToDomainModel(receiver *model.Receiver) (definitions.GettableApiReceiver, error) { + // TODO: Using GettableApiReceiver instead of PostableApiReceiver so that SecureFields type matches. + gettable := definitions.GettableApiReceiver{ + Receiver: config.Receiver{ + Name: receiver.Spec.Title, + }, + GettableGrafanaReceivers: definitions.GettableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.GettableGrafanaReceiver{}, + }, + } + + for _, integration := range receiver.Spec.Integrations { + grafanaIntegration := definitions.GettableGrafanaReceiver{ + Name: receiver.Spec.Title, + Type: integration.Type, + Settings: integration.Settings, + SecureFields: integration.SecureFields, + //Provenance: "", //TODO: Convert provenance? + } + if integration.Uid != nil { + grafanaIntegration.UID = *integration.Uid + } + if integration.DisableResolveMessage != nil { + grafanaIntegration.DisableResolveMessage = *integration.DisableResolveMessage + } + gettable.GettableGrafanaReceivers.GrafanaManagedReceivers = append(gettable.GettableGrafanaReceivers.GrafanaManagedReceivers, &grafanaIntegration) + } + + return gettable, nil +} diff --git a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go new file mode 100644 index 00000000000..8d7d7b641f4 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go @@ -0,0 +1,225 @@ +package receiver + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + notifications "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +var ( + _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) +) + +var resourceInfo = notifications.ReceiverResourceInfo + +type ReceiverService interface { + GetReceiver(ctx context.Context, q models.GetReceiverQuery, user identity.Requester) (definitions.GettableApiReceiver, error) + GetReceivers(ctx context.Context, q models.GetReceiversQuery, user identity.Requester) ([]definitions.GettableApiReceiver, error) + CreateReceiver(ctx context.Context, r definitions.GettableApiReceiver, orgID int64) (definitions.GettableApiReceiver, error) // TODO: Uses Gettable for Write, consider creating new struct. + UpdateReceiver(ctx context.Context, r definitions.GettableApiReceiver, orgID int64) (definitions.GettableApiReceiver, error) // TODO: Uses Gettable for Write, consider creating new struct. + DeleteReceiver(ctx context.Context, name string, orgID int64, provenance definitions.Provenance, version string) error +} + +type legacyStorage struct { + service ReceiverService + namespacer request.NamespaceMapper + tableConverter rest.TableConvertor +} + +func (s *legacyStorage) New() runtime.Object { + return resourceInfo.NewFunc() +} + +func (s *legacyStorage) Destroy() {} + +func (s *legacyStorage) NamespaceScoped() bool { + return true // namespace == org +} + +func (s *legacyStorage) GetSingularName() string { + return resourceInfo.GetSingularName() +} + +func (s *legacyStorage) NewList() runtime.Object { + return resourceInfo.NewListFunc() +} + +func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) { + orgId, err := request.OrgIDForList(ctx) + if err != nil { + return nil, err + } + + q := models.GetReceiversQuery{ + OrgID: orgId, + //Names: ctx.QueryStrings("names"), // TODO: Query params. + //Limit: ctx.QueryInt("limit"), + //Offset: ctx.QueryInt("offset"), + //Decrypt: ctx.QueryBool("decrypt"), + } + + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + res, err := s.service.GetReceivers(ctx, q, user) + if err != nil { + return nil, err + } + + return convertToK8sResources(orgId, res, s.namespacer) +} + +func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOptions) (runtime.Object, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + q := models.GetReceiverQuery{ + OrgID: info.OrgID, + Name: uid, // TODO: Name/UID mapping or change signature of service. + //Decrypt: ctx.QueryBool("decrypt"), // TODO: Query params. + } + + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + res, err := s.service.GetReceiver(ctx, q, user) + if err != nil { + return nil, err + } + + return convertToK8sResource(info.OrgID, res, s.namespacer) +} + +func (s *legacyStorage) Create(ctx context.Context, + obj runtime.Object, + createValidation rest.ValidateObjectFunc, + _ *metav1.CreateOptions, +) (runtime.Object, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + if createValidation != nil { + if err := createValidation(ctx, obj.DeepCopyObject()); err != nil { + return nil, err + } + } + p, ok := obj.(*notifications.Receiver) + if !ok { + return nil, fmt.Errorf("expected receiver but got %s", obj.GetObjectKind().GroupVersionKind()) + } + if p.ObjectMeta.Name != "" { // TODO remove when metadata.name can be defined by user + return nil, errors.NewBadRequest("object's metadata.name should be empty") + } + model, err := convertToDomainModel(p) + if err != nil { + return nil, err + } + out, err := s.service.CreateReceiver(ctx, model, info.OrgID) + if err != nil { + return nil, err + } + return convertToK8sResource(info.OrgID, out, s.namespacer) +} + +func (s *legacyStorage) Update(ctx context.Context, + uid string, + objInfo rest.UpdatedObjectInfo, + createValidation rest.ValidateObjectFunc, + updateValidation rest.ValidateObjectUpdateFunc, + _ bool, + _ *metav1.UpdateOptions, +) (runtime.Object, bool, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + old, err := s.Get(ctx, uid, nil) + if err != nil { + return old, false, err + } + obj, err := objInfo.UpdatedObject(ctx, old) + if err != nil { + return old, false, err + } + if updateValidation != nil { + if err := updateValidation(ctx, obj, old); err != nil { + return nil, false, err + } + } + p, ok := obj.(*notifications.Receiver) + if !ok { + return nil, false, fmt.Errorf("expected receiver but got %s", obj.GetObjectKind().GroupVersionKind()) + } + model, err := convertToDomainModel(p) + if err != nil { + return old, false, err + } + + if p.ObjectMeta.Name != getUID(model) { + return nil, false, errors.NewBadRequest("title cannot be changed. Consider creating a new resource.") + } + + updated, err := s.service.UpdateReceiver(ctx, model, info.OrgID) + if err != nil { + return nil, false, err + } + + r, err := convertToK8sResource(info.OrgID, updated, s.namespacer) + return r, false, err +} + +// GracefulDeleter +func (s *legacyStorage) Delete(ctx context.Context, uid string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + old, err := s.Get(ctx, uid, nil) + if err != nil { + return old, false, err + } + if deleteValidation != nil { + if err = deleteValidation(ctx, old); err != nil { + return nil, false, err + } + } + version := "" + if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil { + version = *options.Preconditions.ResourceVersion + } + p, ok := old.(*notifications.Receiver) + if !ok { + return nil, false, fmt.Errorf("expected receiver but got %s", old.GetObjectKind().GroupVersionKind()) + } + + err = s.service.DeleteReceiver(ctx, p.Spec.Title, info.OrgID, definitions.Provenance(models.ProvenanceNone), version) // TODO add support for dry-run option + return old, false, err // false - will be deleted async +} + +func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { + return nil, errors.NewMethodNotSupported(resourceInfo.GroupResource(), "deleteCollection") +} diff --git a/pkg/registry/apis/alerting/notifications/receiver/storage.go b/pkg/registry/apis/alerting/notifications/receiver/storage.go new file mode 100644 index 00000000000..80ec2d0bf9b --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/receiver/storage.go @@ -0,0 +1,80 @@ +package receiver + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/prometheus/client_golang/prometheus" + + model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/apiserver/utils" +) + +var _ grafanarest.Storage = (*storage)(nil) + +type storage struct { + *genericregistry.Store +} + +func (s storage) Compare(storageObj, legacyObj runtime.Object) bool { + // TODO implement when supported dual write mode is not Mode0 + return false +} + +func NewStorage( + legacySvc ReceiverService, + namespacer request.NamespaceMapper, + scheme *runtime.Scheme, + desiredMode grafanarest.DualWriterMode, + optsGetter generic.RESTOptionsGetter, + reg prometheus.Registerer) (rest.Storage, error) { + legacyStore := &legacyStorage{ + service: legacySvc, + namespacer: namespacer, + tableConverter: utils.NewTableConverter( + resourceInfo.GroupResource(), + []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Format: "string", Description: "The receiver name"}, // TODO: Add integration types. + }, + func(obj any) ([]interface{}, error) { + r, ok := obj.(*model.Receiver) + if ok { + return []interface{}{ + r.Name, + r.Spec.Title, + // r.Spec, //TODO implement formatting for Spec, same as UI? + }, nil + } + return nil, fmt.Errorf("expected resource or info") + }), + } + if optsGetter != nil && desiredMode != grafanarest.Mode0 { + strategy := grafanaregistry.NewStrategy(scheme) + s := &genericregistry.Store{ + NewFunc: resourceInfo.NewFunc, + NewListFunc: resourceInfo.NewListFunc, + PredicateFunc: grafanaregistry.Matcher, + DefaultQualifiedResource: resourceInfo.GroupResource(), + SingularQualifiedResource: resourceInfo.SingularGroupResource(), + TableConvertor: legacyStore.tableConverter, + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + } + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs} + if err := s.CompleteWithOptions(options); err != nil { + return nil, err + } + return grafanarest.NewDualWriter(desiredMode, legacyStore, storage{Store: s}, reg), nil + } + return legacyStore, nil +} diff --git a/pkg/registry/apis/alerting/notifications/register.go b/pkg/registry/apis/alerting/notifications/register.go index cb09efaa456..459e6451e82 100644 --- a/pkg/registry/apis/alerting/notifications/register.go +++ b/pkg/registry/apis/alerting/notifications/register.go @@ -18,6 +18,7 @@ import ( notificationsModels "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + receiver "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/receiver" timeInterval "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/timeinterval" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -86,8 +87,14 @@ func (t NotificationsAPIBuilder) GetAPIGroupInfo( return nil, fmt.Errorf("failed to initialize time-interval storage: %w", err) } + recvStorage, err := receiver.NewStorage(nil, t.namespacer, scheme, desiredMode, optsGetter, reg) // TODO: add receiver service + if err != nil { + return nil, fmt.Errorf("failed to initialize receiver storage: %w", err) + } + apiGroupInfo.VersionedResourcesStorageMap[notificationsModels.VERSION] = map[string]rest.Storage{ notificationsModels.TimeIntervalResourceInfo.StoragePath(): intervals, + notificationsModels.ReceiverResourceInfo.StoragePath(): recvStorage, } return &apiGroupInfo, nil } @@ -106,6 +113,8 @@ func (t NotificationsAPIBuilder) GetAuthorizer() authorizer.Authorizer { switch a.GetResource() { case notificationsModels.TimeIntervalResourceInfo.GroupResource().Resource: return timeInterval.Authorize(ctx, t.authz, a) + case notificationsModels.ReceiverResourceInfo.GroupResource().Resource: + return receiver.Authorize(ctx, t.authz, a) } return authorizer.DecisionNoOpinion, "", nil }) From f894c22b218833e69fce26892a2b0dbd2df9e5dc Mon Sep 17 00:00:00 2001 From: Andreas Zilly Date: Wed, 26 Jun 2024 21:22:37 +0200 Subject: [PATCH 10/11] Chore: Add missing build elements to Dockerfile (#89714) Several go.* files that are referenced in go.work were missing in Dockerfile. --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 37d5609de7a..b9d1bb255e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,8 +57,10 @@ COPY .bingo .bingo COPY pkg/util/xorm/go.* pkg/util/xorm/ COPY pkg/apiserver/go.* pkg/apiserver/ COPY pkg/apimachinery/go.* pkg/apimachinery/ +COPY pkg/build/go.* pkg/build/ COPY pkg/build/wire/go.* pkg/build/wire/ COPY pkg/promlib/go.* pkg/promlib/ +COPY pkg/storage/unified/resource/go.* pkg/storage/unified/resource/ RUN go mod download RUN if [[ "$BINGO" = "true" ]]; then \ From ed13959e336ebee1c582ada14b72f4b815e4eb8b Mon Sep 17 00:00:00 2001 From: Jeff Levin Date: Wed, 26 Jun 2024 12:03:13 -0800 Subject: [PATCH 11/11] Optimize memory allocations in permissions cache (#89645) This PR reduces the number of allocations made while caching permissions from the database, fixes the hierarchy of spans and adds new spans for tracing. --------- Signed-off-by: Dave Henderson Co-authored-by: Dave Henderson --- pkg/api/common_test.go | 2 + pkg/api/dashboard.go | 21 ++++---- pkg/api/dashboard_test.go | 28 +++++++---- pkg/services/accesscontrol/acimpl/service.go | 52 ++++++++++---------- pkg/services/authn/clients/basic.go | 4 +- 5 files changed, 60 insertions(+), 47 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 4d8b651f568..9e7c2f6756c 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -274,6 +274,7 @@ func setupSimpleHTTPServer(features featuremgmt.FeatureToggles) *HTTPServer { authInfoService: &authinfotest.FakeService{ ExpectedLabels: map[int64]string{int64(1): login.GetAuthProviderLabel(login.LDAPAuthModule)}, }, + tracer: tracing.InitializeTracerForTest(), } } @@ -299,6 +300,7 @@ func SetupAPITestServer(t *testing.T, opts ...APITestServerOption) *webtest.Serv Features: featuremgmt.WithFeatures(), QuotaService: quotatest.New(false, nil), searchUsersService: &searchusers.OSSService{}, + tracer: tracing.InitializeTracerForTest(), } for _, opt := range opts { diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 631b74a5bf6..f9f231db381 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -50,7 +50,6 @@ func (hs *HTTPServer) isDashboardStarredByUser(c *contextmodel.ReqContext, dashI } userID, err := identity.IntIdentifier(namespaceID, userIDstr) - if err != nil { return false, err } @@ -83,8 +82,11 @@ func dashboardGuardianResponse(err error) response.Response { // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response { + ctx, span := hs.tracer.Start(c.Req.Context(), "httpserver.GetDashboard") + defer span.End() + uid := web.Params(c.Req)[":uid"] - dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, uid) + dash, rsp := hs.getDashboardHelper(ctx, c.SignedInUser.GetOrgID(), 0, uid) if rsp != nil { return rsp } @@ -96,7 +98,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response // If public dashboards is enabled and we have a public dashboard, update meta values if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) && hs.Cfg.PublicDashboardsEnabled { - publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.SignedInUser.GetOrgID(), dash.UID) + publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(ctx, c.SignedInUser.GetOrgID(), dash.UID) if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) { return response.Error(http.StatusInternalServerError, "Error while retrieving public dashboards", err) } @@ -119,7 +121,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response return response.Error(http.StatusInternalServerError, "Error while loading dashboard, dashboard data is invalid", nil) } } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) + guardian, err := guardian.NewByDashboard(ctx, dash, c.SignedInUser.GetOrgID(), c.SignedInUser) if err != nil { return response.Err(err) } @@ -139,14 +141,14 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response // Finding creator and last updater of the dashboard updater, creator := anonString, anonString if dash.UpdatedBy > 0 { - updater = hs.getUserLogin(c.Req.Context(), dash.UpdatedBy) + updater = hs.getUserLogin(ctx, dash.UpdatedBy) } if dash.CreatedBy > 0 { - creator = hs.getUserLogin(c.Req.Context(), dash.CreatedBy) + creator = hs.getUserLogin(ctx, dash.CreatedBy) } annotationPermissions := &dashboardsV0.AnnotationPermission{} - if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { + if hs.Features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID)) } else { hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard) @@ -182,7 +184,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response // nolint:staticcheck query := dashboards.GetDashboardQuery{ID: dash.FolderID, OrgID: c.SignedInUser.GetOrgID()} metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetDashboard).Inc() - queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) + queryResult, err := hs.DashboardService.GetDashboard(ctx, &query) if err != nil { if errors.Is(err, dashboards.ErrFolderNotFound) { return response.Error(http.StatusNotFound, "Folder not found", err) @@ -194,7 +196,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response meta.FolderUrl = queryResult.GetURL() } - provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), dash.ID) + provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(ctx, dash.ID) if err != nil { return response.Error(http.StatusInternalServerError, "Error while checking if dashboard is provisioned", err) } @@ -1004,7 +1006,6 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) respons newData := newVersionRes.Data result, err := dashdiffs.CalculateDiff(c.Req.Context(), &options, baseData, newData) - if err != nil { if errors.Is(err, dashver.ErrDashboardVersionNotFound) { return response.Error(http.StatusNotFound, "Dashboard version not found", err) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 7b724e6209c..e249765952b 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -81,6 +81,7 @@ func TestGetHomeDashboard(t *testing.T) { preferenceService: prefService, dashboardVersionService: dashboardVersionService, log: log.New("test-logger"), + tracer: tracing.InitializeTracerForTest(), } tests := []struct { @@ -585,7 +586,8 @@ func TestDashboardAPIEndpoint(t *testing.T) { DashboardID: 2, Version: 1, Data: fakeDash.Data, - }} + }, + } mockSQLStore := dbtest.NewFakeDB() origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) @@ -622,7 +624,8 @@ func TestDashboardAPIEndpoint(t *testing.T) { DashboardID: 2, Version: 1, Data: fakeDash.Data, - }} + }, + } cmd := dtos.RestoreDashboardVersionCommand{ Version: 1, @@ -680,6 +683,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { DashboardService: dashboardService, Features: featuremgmt.WithFeatures(), starService: startest.NewStarServiceFake(), + tracer: tracing.InitializeTracerForTest(), } hs.callGetDashboard(sc) @@ -718,6 +722,7 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { userService: userSvc, CacheService: localcache.New(5*time.Minute, 10*time.Minute), log: log.New(), + tracer: tracing.InitializeTracerForTest(), } } @@ -855,6 +860,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr DashboardService: dashboardService, Features: featuremgmt.WithFeatures(), starService: startest.NewStarServiceFake(), + tracer: tracing.InitializeTracerForTest(), } hs.callGetDashboard(sc) @@ -908,6 +914,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s Features: featuremgmt.WithFeatures(), accesscontrolService: actest.FakeService{}, log: log.New("test-logger"), + tracer: tracing.InitializeTracerForTest(), } sc := setupScenarioContext(t, url) @@ -927,7 +934,8 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s } func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, - role org.RoleType, fn scenarioFunc, sqlmock db.DB, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService) { + role org.RoleType, fn scenarioFunc, sqlmock db.DB, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService, +) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { cfg := setting.NewCfg() @@ -943,6 +951,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string dashboardVersionService: fakeDashboardVersionService, Features: featuremgmt.WithFeatures(), DashboardService: dashSvc, + tracer: tracing.InitializeTracerForTest(), } sc := setupScenarioContext(t, url) @@ -967,7 +976,8 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService, - cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore db.DB) { + cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore db.DB, +) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { cfg := setting.NewCfg() folderSvc := foldertest.NewFakeService() @@ -986,6 +996,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout dashboardVersionService: fakeDashboardVersionService, accesscontrolService: actest.FakeService{}, folderService: folderSvc, + tracer: tracing.InitializeTracerForTest(), } sc := setupScenarioContext(t, url) @@ -1022,12 +1033,12 @@ type mockDashboardProvisioningService struct { } func (s mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) ( - *dashboards.DashboardProvisioning, error) { + *dashboards.DashboardProvisioning, error, +) { return nil, nil } -type mockLibraryPanelService struct { -} +type mockLibraryPanelService struct{} var _ librarypanels.Service = (*mockLibraryPanelService)(nil) @@ -1039,8 +1050,7 @@ func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Cont return nil } -type mockLibraryElementService struct { -} +type mockLibraryElementService struct{} func (l *mockLibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { return model.LibraryElementDTO{}, nil diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index eb9010513aa..04c5c790aa3 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -9,6 +9,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/attribute" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -45,8 +46,7 @@ var SharedWithMeFolderPermission = accesscontrol.Permission{ var OSSRolesPrefixes = []string{accesscontrol.ManagedRolePrefix, accesscontrol.ExternalServiceRolePrefix} -func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService, - accessControl accesscontrol.AccessControl, actionResolver accesscontrol.ActionResolver, features featuremgmt.FeatureToggles, tracer tracing.Tracer) (*Service, error) { +func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService, accessControl accesscontrol.AccessControl, actionResolver accesscontrol.ActionResolver, features featuremgmt.FeatureToggles, tracer tracing.Tracer) (*Service, error) { service := ProvideOSSService(cfg, database.ProvideService(db), actionResolver, cache, features, tracer) api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints() @@ -151,9 +151,9 @@ func (s *Service) getBasicRolePermissions(ctx context.Context, role string, orgI ctx, span := s.tracer.Start(ctx, "authz.getBasicRolePermissions") defer span.End() - permissions := make([]accesscontrol.Permission, 0) + var permissions []accesscontrol.Permission if basicRole, ok := s.roles[role]; ok { - permissions = append(permissions, basicRole.Permissions...) + permissions = basicRole.Permissions } // Fetch managed role permissions assigned to basic roles @@ -209,7 +209,6 @@ func (s *Service) getUserDirectPermissions(ctx context.Context, user identity.Re UserID: userID, RolePrefixes: OSSRolesPrefixes, }) - if err != nil { return nil, err } @@ -225,12 +224,16 @@ func (s *Service) getUserDirectPermissions(ctx context.Context, user identity.Re } func (s *Service) getCachedUserPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) { - basicRolesPermissions, err := s.getCachedBasicRolesPermissions(ctx, user, options) + ctx, span := s.tracer.Start(ctx, "authz.getCachedUserPermissions") + defer span.End() + + permissions := []accesscontrol.Permission{} + permissions, err := s.getCachedBasicRolesPermissions(ctx, user, options, permissions) if err != nil { return nil, err } - teamsPermissions, err := s.getCachedTeamsPermissions(ctx, user, options) + permissions, err = s.getCachedTeamsPermissions(ctx, user, options, permissions) if err != nil { return nil, err } @@ -240,32 +243,33 @@ func (s *Service) getCachedUserPermissions(ctx context.Context, user identity.Re return nil, err } - permissions := make([]accesscontrol.Permission, 0, len(basicRolesPermissions)+len(teamsPermissions)+len(userPermissions)) - permissions = append(permissions, basicRolesPermissions...) - permissions = append(permissions, teamsPermissions...) permissions = append(permissions, userPermissions...) + span.SetAttributes(attribute.Int("num_permissions", len(permissions))) + return permissions, nil } -func (s *Service) getCachedBasicRolesPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) { +func (s *Service) getCachedBasicRolesPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options, permissions []accesscontrol.Permission) ([]accesscontrol.Permission, error) { ctx, span := s.tracer.Start(ctx, "authz.getCachedBasicRolesPermissions") defer span.End() basicRoles := accesscontrol.GetOrgRoles(user) - basicRolesPermissions := make([]accesscontrol.Permission, 0) for _, role := range basicRoles { - permissions, err := s.getCachedBasicRolePermissions(ctx, role, user.GetOrgID(), options) + perms, err := s.getCachedBasicRolePermissions(ctx, role, user.GetOrgID(), options) if err != nil { return nil, err } - basicRolesPermissions = append(basicRolesPermissions, permissions...) + permissions = append(permissions, perms...) } - return basicRolesPermissions, nil + return permissions, nil } func (s *Service) getCachedBasicRolePermissions(ctx context.Context, role string, orgID int64, options accesscontrol.Options) ([]accesscontrol.Permission, error) { + ctx, span := s.tracer.Start(ctx, "authz.getCachedBasicRolePermissions") + defer span.End() + key := accesscontrol.GetBasicRolePermissionCacheKey(role, orgID) - getPermissionsFn := func() ([]accesscontrol.Permission, error) { + getPermissionsFn := func(ctx context.Context) ([]accesscontrol.Permission, error) { return s.getBasicRolePermissions(ctx, role, orgID) } return s.getCachedPermissions(ctx, key, getPermissionsFn, options) @@ -276,17 +280,17 @@ func (s *Service) getCachedUserDirectPermissions(ctx context.Context, user ident defer span.End() key := accesscontrol.GetUserDirectPermissionCacheKey(user) - getUserPermissionsFn := func() ([]accesscontrol.Permission, error) { + getUserPermissionsFn := func(ctx context.Context) ([]accesscontrol.Permission, error) { return s.getUserDirectPermissions(ctx, user) } return s.getCachedPermissions(ctx, key, getUserPermissionsFn, options) } -type GetPermissionsFn = func() ([]accesscontrol.Permission, error) +type getPermissionsFunc = func(ctx context.Context) ([]accesscontrol.Permission, error) // Generic method for getting various permissions from cache -func (s *Service) getCachedPermissions(ctx context.Context, key string, getPermissionsFn GetPermissionsFn, options accesscontrol.Options) ([]accesscontrol.Permission, error) { - _, span := s.tracer.Start(ctx, "authz.getCachedTeamsPermissions") +func (s *Service) getCachedPermissions(ctx context.Context, key string, getPermissionsFn getPermissionsFunc, options accesscontrol.Options) ([]accesscontrol.Permission, error) { + _, span := s.tracer.Start(ctx, "authz.getCachedPermissions") defer span.End() if !options.ReloadCache { @@ -299,7 +303,7 @@ func (s *Service) getCachedPermissions(ctx context.Context, key string, getPermi span.AddEvent("cache miss") metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc() - permissions, err := getPermissionsFn() + permissions, err := getPermissionsFn(ctx) if err != nil { return nil, err } @@ -308,13 +312,12 @@ func (s *Service) getCachedPermissions(ctx context.Context, key string, getPermi return permissions, nil } -func (s *Service) getCachedTeamsPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) { +func (s *Service) getCachedTeamsPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options, permissions []accesscontrol.Permission) ([]accesscontrol.Permission, error) { ctx, span := s.tracer.Start(ctx, "authz.getCachedTeamsPermissions") defer span.End() teams := user.GetTeams() orgID := user.GetOrgID() - permissions := make([]accesscontrol.Permission, 0) miss := teams if !options.ReloadCache { @@ -433,8 +436,7 @@ func GetActionFilter(options accesscontrol.SearchOptions) func(action string) bo } // SearchUsersPermissions returns all users' permissions filtered by action prefixes -func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Requester, - options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) { +func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Requester, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) { // Limit roles to available in OSS options.RolePrefixes = OSSRolesPrefixes if options.NamespacedID != "" { diff --git a/pkg/services/authn/clients/basic.go b/pkg/services/authn/clients/basic.go index f27d15672e8..1aa4db44f4f 100644 --- a/pkg/services/authn/clients/basic.go +++ b/pkg/services/authn/clients/basic.go @@ -7,9 +7,7 @@ import ( "github.com/grafana/grafana/pkg/services/authn" ) -var ( - errDecodingBasicAuthHeader = errutil.BadRequest("basic-auth.invalid-header", errutil.WithPublicMessage("Invalid Basic Auth Header")) -) +var errDecodingBasicAuthHeader = errutil.BadRequest("basic-auth.invalid-header", errutil.WithPublicMessage("Invalid Basic Auth Header")) var _ authn.ContextAwareClient = new(Basic)