mirror of https://github.com/grafana/grafana
Grafana Advisor: Datasource checks (#99313)
parent
7d2eb83cbd
commit
b066a63131
@ -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, |
||||
), |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -0,0 +1,95 @@ |
||||
package app |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
|
||||
claims "github.com/grafana/authlib/types" |
||||
"github.com/grafana/grafana-app-sdk/resource" |
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" |
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks" |
||||
"github.com/grafana/grafana/pkg/apimachinery/identity" |
||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
||||
"github.com/grafana/grafana/pkg/services/user" |
||||
) |
||||
|
||||
func getCheck(obj resource.Object, checks map[string]checks.Check) (checks.Check, error) { |
||||
labels := obj.GetLabels() |
||||
objTypeLabel, ok := labels[typeLabel] |
||||
if !ok { |
||||
return nil, errors.New("missing check type as label") |
||||
} |
||||
c, ok := checks[objTypeLabel] |
||||
if !ok { |
||||
supportedTypes := "" |
||||
for k := range checks { |
||||
supportedTypes += k + ", " |
||||
} |
||||
return nil, fmt.Errorf("unknown check type %s. Supported types are: %s", objTypeLabel, supportedTypes) |
||||
} |
||||
|
||||
return c, nil |
||||
} |
||||
|
||||
func getStatusAnnotation(obj resource.Object) string { |
||||
return obj.GetAnnotations()[statusAnnotation] |
||||
} |
||||
|
||||
func setStatusAnnotation(ctx context.Context, client resource.Client, obj resource.Object, status string) error { |
||||
annotations := obj.GetAnnotations() |
||||
annotations[statusAnnotation] = status |
||||
return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ |
||||
Operations: []resource.PatchOperation{{ |
||||
Operation: resource.PatchOpAdd, |
||||
Path: "/metadata/annotations", |
||||
Value: annotations, |
||||
}}, |
||||
}, resource.PatchOptions{}, obj) |
||||
} |
||||
|
||||
func processCheck(ctx context.Context, client resource.Client, obj resource.Object, check checks.Check) error { |
||||
status := getStatusAnnotation(obj) |
||||
if status != "" { |
||||
// Check already processed
|
||||
return nil |
||||
} |
||||
c, ok := obj.(*advisorv0alpha1.Check) |
||||
if !ok { |
||||
return fmt.Errorf("invalid object type") |
||||
} |
||||
// Populate ctx with the user that created the check
|
||||
meta, err := utils.MetaAccessor(obj) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
createdBy := meta.GetCreatedBy() |
||||
typ, uid, err := claims.ParseTypeID(createdBy) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
ctx = identity.WithRequester(ctx, &user.SignedInUser{ |
||||
UserUID: uid, |
||||
FallbackType: typ, |
||||
}) |
||||
// Run the checks
|
||||
report, err := check.Run(ctx, &c.Spec) |
||||
if err != nil { |
||||
setErr := setStatusAnnotation(ctx, client, obj, "error") |
||||
if setErr != nil { |
||||
return setErr |
||||
} |
||||
return err |
||||
} |
||||
err = setStatusAnnotation(ctx, client, obj, "processed") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ |
||||
Operations: []resource.PatchOperation{{ |
||||
Operation: resource.PatchOpAdd, |
||||
Path: "/status/report", |
||||
Value: *report, |
||||
}}, |
||||
}, resource.PatchOptions{}, obj) |
||||
} |
||||
@ -0,0 +1,124 @@ |
||||
package app |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource" |
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" |
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks" |
||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestGetCheck(t *testing.T) { |
||||
obj := &advisorv0alpha1.Check{} |
||||
obj.SetLabels(map[string]string{typeLabel: "testType"}) |
||||
|
||||
checkMap := map[string]checks.Check{ |
||||
"testType": &mockCheck{}, |
||||
} |
||||
|
||||
check, err := getCheck(obj, checkMap) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, check) |
||||
} |
||||
|
||||
func TestGetCheck_MissingLabel(t *testing.T) { |
||||
obj := &advisorv0alpha1.Check{} |
||||
checkMap := map[string]checks.Check{} |
||||
|
||||
_, err := getCheck(obj, checkMap) |
||||
assert.Error(t, err) |
||||
assert.Equal(t, "missing check type as label", err.Error()) |
||||
} |
||||
|
||||
func TestGetCheck_UnknownType(t *testing.T) { |
||||
obj := &advisorv0alpha1.Check{} |
||||
obj.SetLabels(map[string]string{typeLabel: "unknownType"}) |
||||
|
||||
checkMap := map[string]checks.Check{ |
||||
"testType": &mockCheck{}, |
||||
} |
||||
|
||||
_, err := getCheck(obj, checkMap) |
||||
assert.Error(t, err) |
||||
assert.Contains(t, err.Error(), "unknown check type unknownType") |
||||
} |
||||
|
||||
func TestSetStatusAnnotation(t *testing.T) { |
||||
obj := &advisorv0alpha1.Check{} |
||||
obj.SetAnnotations(map[string]string{}) |
||||
client := &mockClient{} |
||||
ctx := context.TODO() |
||||
|
||||
err := setStatusAnnotation(ctx, client, obj, "processed") |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) |
||||
} |
||||
|
||||
func TestProcessCheck(t *testing.T) { |
||||
obj := &advisorv0alpha1.Check{} |
||||
obj.SetAnnotations(map[string]string{}) |
||||
meta, err := utils.MetaAccessor(obj) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
meta.SetCreatedBy("user:1") |
||||
client := &mockClient{} |
||||
ctx := context.TODO() |
||||
check := &mockCheck{} |
||||
|
||||
err = processCheck(ctx, client, obj, check) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) |
||||
} |
||||
|
||||
func TestProcessCheck_AlreadyProcessed(t *testing.T) { |
||||
obj := &advisorv0alpha1.Check{} |
||||
obj.SetAnnotations(map[string]string{statusAnnotation: "processed"}) |
||||
client := &mockClient{} |
||||
ctx := context.TODO() |
||||
check := &mockCheck{} |
||||
|
||||
err := processCheck(ctx, client, obj, check) |
||||
assert.NoError(t, err) |
||||
} |
||||
|
||||
func TestProcessCheck_RunError(t *testing.T) { |
||||
obj := &advisorv0alpha1.Check{} |
||||
obj.SetAnnotations(map[string]string{}) |
||||
meta, err := utils.MetaAccessor(obj) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
meta.SetCreatedBy("user:1") |
||||
client := &mockClient{} |
||||
ctx := context.TODO() |
||||
|
||||
check := &mockCheck{ |
||||
err: errors.New("run error"), |
||||
} |
||||
|
||||
err = processCheck(ctx, client, obj, check) |
||||
assert.Error(t, err) |
||||
assert.Equal(t, "error", obj.GetAnnotations()[statusAnnotation]) |
||||
} |
||||
|
||||
type mockClient struct { |
||||
resource.Client |
||||
} |
||||
|
||||
func (m *mockClient) PatchInto(ctx context.Context, id resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions, obj resource.Object) error { |
||||
return nil |
||||
} |
||||
|
||||
type mockCheck struct { |
||||
checks.Check |
||||
err error |
||||
} |
||||
|
||||
func (m *mockCheck) Run(ctx context.Context, spec *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error) { |
||||
return &advisorv0alpha1.CheckV0alpha1StatusReport{}, m.err |
||||
} |
||||
Loading…
Reference in new issue