Advisor: Redefine check properties (#100874)

This commit is contained in:
Andres Martinez Gotor
2025-02-19 15:11:55 +01:00
committed by GitHub
parent 9f49f4ce36
commit a24fd1b722
15 changed files with 177 additions and 71 deletions
@@ -2,18 +2,29 @@
package v0alpha1
// +k8s:openapi-gen=true
type CheckErrorLink struct {
// URL to a page with more information about the error
Url string `json:"url"`
// Human readable error message
Message string `json:"message"`
}
// NewCheckErrorLink creates a new CheckErrorLink object.
func NewCheckErrorLink() *CheckErrorLink {
return &CheckErrorLink{}
}
// +k8s:openapi-gen=true
type CheckReportFailure struct {
// Severity of the failure
Severity CheckReportFailureSeverity `json:"severity"`
// Human readable reason for the failure
Reason string `json:"reason"`
// Action to take to resolve the failure
Action string `json:"action"`
// Step ID that the failure is associated with
StepID string `json:"stepID"`
// Item ID that the failure is associated with
ItemID string `json:"itemID"`
// Human readable identifier of the item that failed
Item string `json:"item"`
// Links to actions that can be taken to resolve the failure
Links []CheckErrorLink `json:"links"`
}
// NewCheckReportFailure creates a new CheckReportFailure object.
@@ -7,6 +7,7 @@ type CheckTypeStep struct {
Title string `json:"title"`
Description string `json:"description"`
StepID string `json:"stepID"`
Resolution string `json:"resolution"`
}
// NewCheckTypeStep creates a new CheckTypeStep object.
@@ -13,6 +13,7 @@ import (
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.Check": schema_pkg_apis_advisor_v0alpha1_Check(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckErrorLink": schema_pkg_apis_advisor_v0alpha1_CheckErrorLink(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckList": schema_pkg_apis_advisor_v0alpha1_CheckList(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure": schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref),
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec": schema_pkg_apis_advisor_v0alpha1_CheckSpec(ref),
@@ -75,6 +76,35 @@ func schema_pkg_apis_advisor_v0alpha1_Check(ref common.ReferenceCallback) common
}
}
func schema_pkg_apis_advisor_v0alpha1_CheckErrorLink(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"url": {
SchemaProps: spec.SchemaProps{
Description: "URL to a page with more information about the error",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"message": {
SchemaProps: spec.SchemaProps{
Description: "Human readable error message",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"url", "message"},
},
},
}
}
func schema_pkg_apis_advisor_v0alpha1_CheckList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -137,22 +167,6 @@ func schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref common.ReferenceCal
Format: "",
},
},
"reason": {
SchemaProps: spec.SchemaProps{
Description: "Human readable reason for the failure",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"action": {
SchemaProps: spec.SchemaProps{
Description: "Action to take to resolve the failure",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"stepID": {
SchemaProps: spec.SchemaProps{
Description: "Step ID that the failure is associated with",
@@ -161,18 +175,34 @@ func schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref common.ReferenceCal
Format: "",
},
},
"itemID": {
"item": {
SchemaProps: spec.SchemaProps{
Description: "Item ID that the failure is associated with",
Description: "Human readable identifier of the item that failed",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"links": {
SchemaProps: spec.SchemaProps{
Description: "Links to actions that can be taken to resolve the failure",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckErrorLink"),
},
},
},
},
},
},
Required: []string{"severity", "reason", "action", "stepID", "itemID"},
Required: []string{"severity", "stepID", "item", "links"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckErrorLink"},
}
}
@@ -456,8 +486,15 @@ func schema_pkg_apis_advisor_v0alpha1_CheckTypeStep(ref common.ReferenceCallback
Format: "",
},
},
"resolution": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"title", "description", "stepID"},
Required: []string{"title", "description", "stepID", "resolution"},
},
},
}
+2 -2
View File
@@ -12,10 +12,10 @@ import (
)
var (
rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"action":{"description":"Action to take to resolve the failure","type":"string"},"itemID":{"description":"Item ID that the failure is associated with","type":"string"},"reason":{"description":"Human readable reason for the failure","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","reason","action","stepID","itemID"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`)
rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`)
versionSchemaCheckv0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaCheckv0alpha1, &versionSchemaCheckv0alpha1)
rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}}`)
rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}}`)
versionSchemaCheckTypev0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaCheckTypev0alpha1, &versionSchemaCheckTypev0alpha1)
)
@@ -12,6 +12,7 @@ import (
"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"
)
type check struct {
@@ -72,7 +73,12 @@ func (s *uidValidationStep) Title() string {
}
func (s *uidValidationStep) Description() string {
return "Check if the UID of each data source is valid."
return "Checks if the UID of a data source is valid."
}
func (s *uidValidationStep) Resolution() string {
return "Check the <a href='https://grafana.com/docs/grafana/latest/upgrade-guide/upgrade-v11.2/#grafana-data-source-uid-format-enforcement'" +
"target=_blank>documentation</a> for more information or delete the data source and create a new one."
}
func (s *uidValidationStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any) (*advisor.CheckReportFailure, error) {
@@ -85,10 +91,9 @@ func (s *uidValidationStep) Run(ctx context.Context, obj *advisor.CheckSpec, i a
if err != nil {
return checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityLow,
fmt.Sprintf("Invalid UID '%s' for data source %s", ds.UID, ds.Name),
"Check the <a href='https://grafana.com/docs/grafana/latest/upgrade-guide/upgrade-v11.2/#grafana-data-source-uid-format-enforcement' target=_blank>documentation</a> for more information.",
s.ID(),
ds.UID,
fmt.Sprintf("%s (%s)", ds.Name, ds.UID),
[]advisor.CheckErrorLink{},
), nil
}
return nil, nil
@@ -104,7 +109,11 @@ func (s *healthCheckStep) Title() string {
}
func (s *healthCheckStep) Description() string {
return "Check if all data sources are healthy."
return "Checks if a data sources is healthy."
}
func (s *healthCheckStep) Resolution() string {
return "Go to the data source configuration page and address the issues reported."
}
func (s *healthCheckStep) ID() string {
@@ -124,7 +133,9 @@ func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any
}
pCtx, err := s.PluginContextProvider.GetWithDataSource(ctx, ds.Type, requester, ds)
if err != nil {
return nil, fmt.Errorf("failed to get plugin context: %w", err)
// Unable to check health check
klog.Error("Failed to get plugin context", "datasource_uid", ds.UID, "error", err)
return nil, nil
}
req := &backend.CheckHealthRequest{
PluginContext: pCtx,
@@ -134,12 +145,14 @@ func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any
if err != nil || resp.Status != backend.HealthStatusOk {
return checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityHigh,
fmt.Sprintf("Health check failed for %s", ds.Name),
fmt.Sprintf(
"Go to the <a href='/connections/datasources/edit/%s'>data source configuration</a>"+
" and address the issues reported.", ds.UID),
s.ID(),
ds.UID,
ds.Name,
[]advisor.CheckErrorLink{
{
Message: "Fix me",
Url: fmt.Sprintf("/connections/datasources/edit/%s", ds.UID),
},
},
), nil
}
return nil, nil
@@ -81,7 +81,7 @@ func TestCheck_Run(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 1, len(items))
assert.Len(t, failures, 1)
assert.Equal(t, "Invalid UID 'invalid uid' for data source Prometheus", failures[0].Reason)
assert.Equal(t, "uid-validation", failures[0].StepID)
})
t.Run("should return failures when datasource health check fails", func(t *testing.T) {
@@ -116,7 +116,7 @@ func TestCheck_Run(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 1, len(items))
assert.Len(t, failures, 1)
assert.Equal(t, "Health check failed for Prometheus", failures[0].Reason)
assert.Equal(t, "health-check", failures[0].StepID)
})
}
+2
View File
@@ -24,6 +24,8 @@ type Step interface {
Title() string
// Description returns the description of the step
Description() string
// Explains the action that needs to be taken to resolve the issue
Resolution() string
// Run executes the step for an item and returns a report
Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec, item any) (*advisorv0alpha1.CheckReportFailure, error)
}
@@ -74,6 +74,11 @@ func (s *deprecationStep) Description() string {
return "Check if any installed plugins are deprecated."
}
func (s *deprecationStep) Resolution() string {
return "Check the <a href='https://grafana.com/legal/plugin-deprecation/#a-plugin-i-use-is-deprecated-what-should-i-do'" +
"target=_blank>documentation</a> for recommended steps or delete the plugin."
}
func (s *deprecationStep) ID() string {
return "deprecation"
}
@@ -98,10 +103,14 @@ func (s *deprecationStep) Run(ctx context.Context, _ *advisor.CheckSpec, it any)
if i.Status == "deprecated" {
return checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityHigh,
fmt.Sprintf("Plugin deprecated: %s", p.ID),
"Check the <a href='https://grafana.com/legal/plugin-deprecation/#a-plugin-i-use-is-deprecated-what-should-i-do' target=_blank>documentation</a> for recommended steps.",
s.ID(),
p.ID,
[]advisor.CheckErrorLink{
{
Message: "Admin",
Url: fmt.Sprintf("/plugins/%s", p.ID),
},
},
), nil
}
return nil, nil
@@ -118,7 +127,11 @@ func (s *updateStep) Title() string {
}
func (s *updateStep) Description() string {
return "Check if any installed plugins have a newer version available."
return "Checks if an installed plugins has a newer version available."
}
func (s *updateStep) Resolution() string {
return "Go to the plugin admin page and upgrade to the latest version."
}
func (s *updateStep) ID() string {
@@ -151,12 +164,14 @@ func (s *updateStep) Run(ctx context.Context, _ *advisor.CheckSpec, i any) (*adv
if hasUpdate(p, info) {
return checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityLow,
fmt.Sprintf("New version available for %s", p.ID),
fmt.Sprintf(
"Go to the <a href='/plugins/%s?page=version-history'>plugin admin page</a>"+
" and upgrade to the latest version.", p.ID),
s.ID(),
p.ID,
[]advisor.CheckErrorLink{
{
Message: "Upgrade",
Url: fmt.Sprintf("/plugins/%s?page=version-history", p.ID),
},
},
), nil
}
@@ -42,10 +42,14 @@ func TestRun(t *testing.T) {
expectedFailures: []advisor.CheckReportFailure{
{
Severity: advisor.CheckReportFailureSeverityHigh,
Reason: "Plugin deprecated: plugin1",
Action: "Check the <a href='https://grafana.com/legal/plugin-deprecation/#a-plugin-i-use-is-deprecated-what-should-i-do' target=_blank>documentation</a> for recommended steps.",
StepID: "deprecation",
ItemID: "plugin1",
Item: "plugin1",
Links: []advisor.CheckErrorLink{
{
Url: "/plugins/plugin1",
Message: "Admin",
},
},
},
},
},
@@ -63,10 +67,14 @@ func TestRun(t *testing.T) {
expectedFailures: []advisor.CheckReportFailure{
{
Severity: advisor.CheckReportFailureSeverityLow,
Reason: "New version available for plugin2",
Action: "Go to the <a href='/plugins/plugin2?page=version-history'>plugin admin page</a> and upgrade to the latest version.",
StepID: "update",
ItemID: "plugin2",
Item: "plugin2",
Links: []advisor.CheckErrorLink{
{
Url: "/plugins/plugin2?page=version-history",
Message: "Upgrade",
},
},
},
},
},
@@ -84,10 +92,14 @@ func TestRun(t *testing.T) {
expectedFailures: []advisor.CheckReportFailure{
{
Severity: advisor.CheckReportFailureSeverityLow,
Reason: "New version available for plugin2",
Action: "Go to the <a href='/plugins/plugin2?page=version-history'>plugin admin page</a> and upgrade to the latest version.",
StepID: "update",
ItemID: "plugin2",
Item: "plugin2",
Links: []advisor.CheckErrorLink{
{
Url: "/plugins/plugin2?page=version-history",
Message: "Upgrade",
},
},
},
},
},
+4 -6
View File
@@ -11,16 +11,14 @@ const (
func NewCheckReportFailure(
severity advisor.CheckReportFailureSeverity,
reason string,
action string,
stepID string,
itemID string,
item string,
links []advisor.CheckErrorLink,
) *advisor.CheckReportFailure {
return &advisor.CheckReportFailure{
Severity: severity,
Reason: reason,
Action: action,
StepID: stepID,
ItemID: itemID,
Item: item,
Links: links,
}
}
@@ -52,6 +52,7 @@ func (r *Runner) Run(ctx context.Context) error {
Title: s.Title(),
Description: s.Description(),
StepID: s.ID(),
Resolution: s.Resolution(),
}
}
obj := &advisorv0alpha1.CheckType{
@@ -6,6 +6,7 @@ import (
"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"
k8sErrs "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -134,8 +135,6 @@ func (m *mockCheck) Steps() []checks.Step {
}
type mockStep struct {
checks.Step
id string
title string
description string
@@ -153,6 +152,14 @@ func (m *mockStep) Description() string {
return m.description
}
func (m *mockStep) Resolution() string {
return ""
}
func (m *mockStep) Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec, item any) (*advisorv0alpha1.CheckReportFailure, error) {
return nil, nil
}
type mockClient struct {
resource.Client
+4
View File
@@ -191,6 +191,10 @@ func (m *mockStep) Description() string {
return "mock"
}
func (m *mockStep) Resolution() string {
return "mock"
}
func (m *mockStep) ID() string {
return "mock"
}