Skip to content

Commit

Permalink
Grafana Advisor: Datasource checks (grafana#99313)
Browse files Browse the repository at this point in the history
  • Loading branch information
andresmgot authored Jan 23, 2025
1 parent 7d2eb83 commit b066a63
Show file tree
Hide file tree
Showing 11 changed files with 553 additions and 2 deletions.
6 changes: 6 additions & 0 deletions apps/advisor/kinds/check.cue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ check: {
frontend: false
backend: true
}
validation: {
operations: [
"CREATE",
"UPDATE",
]
}
schema: {
spec: {
// Generic data input that a check can receive
Expand Down
10 changes: 9 additions & 1 deletion apps/advisor/pkg/apis/advisor_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ var appManifestData = app.ManifestData{
Conversion: false,
Versions: []app.ManifestKindVersion{
{
Name: "v0alpha1",
Name: "v0alpha1",
Admission: &app.AdmissionCapabilities{
Validation: &app.ValidationCapability{
Operations: []app.AdmissionOperation{
app.AdmissionOperationCreate,
app.AdmissionOperationUpdate,
},
},
},
Schema: &versionSchemaCheckv0alpha1,
},
},
Expand Down
46 changes: 46 additions & 0 deletions apps/advisor/pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,44 @@ package app

import (
"context"
"fmt"

"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/k8s"
"github.com/grafana/grafana-app-sdk/resource"
"github.com/grafana/grafana-app-sdk/simple"
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
)

const (
typeLabel = "advisor.grafana.app/type"
statusAnnotation = "advisor.grafana.app/status"
)

func New(cfg app.Config) (app.App, error) {
// Read config
checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService)
if !ok {
return nil, fmt.Errorf("invalid config type")
}

// Prepare storage client
clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{})
client, err := clientGenerator.ClientFor(advisorv0alpha1.CheckKind())
if err != nil {
return nil, err
}

// Initialize checks
checkMap := map[string]checks.Check{}
for _, c := range checkRegistry.Checks() {
checkMap[c.Type()] = c
}

simpleConfig := simple.AppConfig{
Name: "advisor",
KubeConfig: cfg.KubeConfig,
Expand All @@ -23,6 +51,24 @@ func New(cfg app.Config) (app.App, error) {
ManagedKinds: []simple.AppManagedKind{
{
Kind: advisorv0alpha1.CheckKind(),
Validator: &simple.Validator{
ValidateFunc: func(ctx context.Context, req *app.AdmissionRequest) error {
if req.Object != nil {
_, err := getCheck(req.Object, checkMap)
return err
}
return nil
},
},
Watcher: &simple.Watcher{
AddFunc: func(ctx context.Context, obj resource.Object) error {
check, err := getCheck(obj, checkMap)
if err != nil {
return err
}
return processCheck(ctx, client, obj, check)
},
},
},
},
}
Expand Down
42 changes: 42 additions & 0 deletions apps/advisor/pkg/app/checkregistry/checkregistry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package checkregistry

import (
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)

type CheckService interface {
Checks() []checks.Check
}

type Service struct {
datasourceSvc datasources.DataSourceService
pluginStore pluginstore.Store
pluginContextProvider datasource.PluginContextWrapper
pluginClient plugins.Client
}

func ProvideService(datasourceSvc datasources.DataSourceService, pluginStore pluginstore.Store,
pluginContextProvider datasource.PluginContextWrapper, pluginClient plugins.Client) *Service {
return &Service{
datasourceSvc: datasourceSvc,
pluginStore: pluginStore,
pluginContextProvider: pluginContextProvider,
pluginClient: pluginClient,
}
}

func (s *Service) Checks() []checks.Check {
return []checks.Check{
datasourcecheck.New(
s.datasourceSvc,
s.pluginStore,
s.pluginContextProvider,
s.pluginClient,
),
}
}
96 changes: 96 additions & 0 deletions apps/advisor/pkg/app/checks/datasourcecheck/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package datasourcecheck

import (
"context"
"fmt"

"github.com/grafana/grafana-plugin-sdk-go/backend"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/util"
"k8s.io/klog/v2"
)

func New(
datasourceSvc datasources.DataSourceService,
pluginStore pluginstore.Store,
pluginContextProvider datasource.PluginContextWrapper,
pluginClient plugins.Client,
) checks.Check {
return &check{
DatasourceSvc: datasourceSvc,
PluginStore: pluginStore,
PluginContextProvider: pluginContextProvider,
PluginClient: pluginClient,
}
}

type check struct {
DatasourceSvc datasources.DataSourceService
PluginStore pluginstore.Store
PluginContextProvider datasource.PluginContextWrapper
PluginClient plugins.Client
}

func (c *check) Type() string {
return "datasource"
}

func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.CheckV0alpha1StatusReport, error) {
// Optionally read the check input encoded in the object
// fmt.Println(obj.Data)

dss, err := c.DatasourceSvc.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{})
if err != nil {
return nil, err
}

dsErrs := []advisor.CheckV0alpha1StatusReportErrors{}
for _, ds := range dss {
// Data source UID validation
err := util.ValidateUID(ds.UID)
if err != nil {
dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{
Severity: advisor.CheckStatusSeverityLow,
Reason: fmt.Sprintf("Invalid UID: %s", ds.UID),
Action: "Change UID",
})
}

// Health check execution
pCtx, err := c.PluginContextProvider.PluginContextForDataSource(ctx, &backend.DataSourceInstanceSettings{
Type: ds.Type,
UID: ds.UID,
APIVersion: ds.APIVersion,
})
if err != nil {
klog.ErrorS(err, "Error creating plugin context", "datasource", ds.Name)
continue
}
req := &backend.CheckHealthRequest{
PluginContext: pCtx,
Headers: map[string]string{},
}
resp, err := c.PluginClient.CheckHealth(ctx, req)
if err != nil {
fmt.Println("Error checking health", err)
continue
}
if resp.Status != backend.HealthStatusOk {
dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{
Severity: advisor.CheckStatusSeverityHigh,
Reason: fmt.Sprintf("Health check failed: %s", ds.Name),
Action: "Check datasource",
})
}
}

return &advisor.CheckV0alpha1StatusReport{
Count: int64(len(dss)),
Errors: dsErrs,
}, nil
}
114 changes: 114 additions & 0 deletions apps/advisor/pkg/app/checks/datasourcecheck/check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package datasourcecheck

import (
"context"
"testing"

"github.com/grafana/grafana-plugin-sdk-go/backend"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/stretchr/testify/assert"
)

func TestCheck_Run(t *testing.T) {
t.Run("should return no errors when all datasources are healthy", func(t *testing.T) {
datasources := []*datasources.DataSource{
{UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"},
{UID: "valid-uid-2", Type: "mysql", Name: "MySQL"},
}

mockDatasourceSvc := &MockDatasourceSvc{dss: datasources}
mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}}
mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}}

check := &check{
DatasourceSvc: mockDatasourceSvc,
PluginContextProvider: mockPluginContextProvider,
PluginClient: mockPluginClient,
}

report, err := check.Run(context.Background(), &advisor.CheckSpec{})

assert.NoError(t, err)
assert.Equal(t, int64(2), report.Count)
assert.Empty(t, report.Errors)
})

t.Run("should return errors when datasource UID is invalid", func(t *testing.T) {
datasources := []*datasources.DataSource{
{UID: "invalid uid", Type: "prometheus", Name: "Prometheus"},
}

mockDatasourceSvc := &MockDatasourceSvc{dss: datasources}
mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}}
mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}}

check := &check{
DatasourceSvc: mockDatasourceSvc,
PluginContextProvider: mockPluginContextProvider,
PluginClient: mockPluginClient,
}

report, err := check.Run(context.Background(), &advisor.CheckSpec{})

assert.NoError(t, err)
assert.Equal(t, int64(1), report.Count)
assert.Len(t, report.Errors, 1)
assert.Equal(t, "Invalid UID: invalid uid", report.Errors[0].Reason)
})

t.Run("should return errors when datasource health check fails", func(t *testing.T) {
datasources := []*datasources.DataSource{
{UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"},
}

mockDatasourceSvc := &MockDatasourceSvc{dss: datasources}
mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}}
mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusError}}

check := &check{
DatasourceSvc: mockDatasourceSvc,
PluginContextProvider: mockPluginContextProvider,
PluginClient: mockPluginClient,
}

report, err := check.Run(context.Background(), &advisor.CheckSpec{})

assert.NoError(t, err)
assert.Equal(t, int64(1), report.Count)
assert.Len(t, report.Errors, 1)
assert.Equal(t, "Health check failed: Prometheus", report.Errors[0].Reason)
})
}

type MockDatasourceSvc struct {
datasources.DataSourceService

dss []*datasources.DataSource
}

func (m *MockDatasourceSvc) GetAllDataSources(ctx context.Context, query *datasources.GetAllDataSourcesQuery) ([]*datasources.DataSource, error) {
return m.dss, nil
}

type MockPluginContextProvider struct {
datasource.PluginContextWrapper

pCtx backend.PluginContext
}

func (m *MockPluginContextProvider) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) {
return m.pCtx, nil
}

type MockPluginClient struct {
plugins.Client

res *backend.CheckHealthResult
}

func (m *MockPluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
return m.res, nil
}
13 changes: 13 additions & 0 deletions apps/advisor/pkg/app/checks/ifaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package checks

import (
"context"

advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
)

// Check defines the methods that a check must implement to be executed.
type Check interface {
Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error)
Type() string
}
Loading

0 comments on commit b066a63

Please sign in to comment.