mirror of https://github.com/grafana/grafana
Advisor: Create checks following a schedule (#100282)
parent
2518012569
commit
42170ad23a
@ -0,0 +1,129 @@ |
|||||||
|
package checkscheduler |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/grafana/grafana-app-sdk/app" |
||||||
|
"github.com/grafana/grafana-app-sdk/k8s" |
||||||
|
"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/checkregistry" |
||||||
|
"github.com/grafana/grafana/apps/advisor/pkg/app/checks" |
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||||
|
"k8s.io/klog/v2" |
||||||
|
) |
||||||
|
|
||||||
|
const evaluateChecksInterval = 24 * time.Hour |
||||||
|
|
||||||
|
// Runner is a "runnable" app used to be able to expose and API endpoint
|
||||||
|
// with the existing checks types. This does not need to be a CRUD resource, but it is
|
||||||
|
// the only way existing at the moment to expose the check types.
|
||||||
|
type Runner struct { |
||||||
|
checkRegistry checkregistry.CheckService |
||||||
|
client resource.Client |
||||||
|
} |
||||||
|
|
||||||
|
// NewRunner creates a new Runner.
|
||||||
|
func New(cfg app.Config) (app.Runnable, 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 |
||||||
|
} |
||||||
|
|
||||||
|
return &Runner{ |
||||||
|
checkRegistry: checkRegistry, |
||||||
|
client: client, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Runner) Run(ctx context.Context) error { |
||||||
|
lastCreated, err := r.checkLastCreated(ctx) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// do an initial creation if necessary
|
||||||
|
if lastCreated.IsZero() { |
||||||
|
err = r.createChecks(ctx) |
||||||
|
if err != nil { |
||||||
|
klog.Error("Error creating new check reports", "error", err) |
||||||
|
} else { |
||||||
|
lastCreated = time.Now() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
nextSendInterval := time.Until(lastCreated.Add(evaluateChecksInterval)) |
||||||
|
if nextSendInterval < time.Minute { |
||||||
|
nextSendInterval = 1 * time.Minute |
||||||
|
} |
||||||
|
|
||||||
|
ticker := time.NewTicker(nextSendInterval) |
||||||
|
defer ticker.Stop() |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-ticker.C: |
||||||
|
err = r.createChecks(ctx) |
||||||
|
if err != nil { |
||||||
|
klog.Error("Error creating new check reports", "error", err) |
||||||
|
} |
||||||
|
|
||||||
|
if nextSendInterval != evaluateChecksInterval { |
||||||
|
nextSendInterval = evaluateChecksInterval |
||||||
|
} |
||||||
|
ticker.Reset(nextSendInterval) |
||||||
|
case <-ctx.Done(): |
||||||
|
return ctx.Err() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// checkLastCreated returns the creation time of the last check created
|
||||||
|
// regardless of its ID. This assumes that the checks are created in batches
|
||||||
|
// so a batch will have a similar creation time.
|
||||||
|
func (r *Runner) checkLastCreated(ctx context.Context) (time.Time, error) { |
||||||
|
list, err := r.client.List(ctx, metav1.NamespaceDefault, resource.ListOptions{}) |
||||||
|
if err != nil { |
||||||
|
return time.Time{}, err |
||||||
|
} |
||||||
|
lastCreated := time.Time{} |
||||||
|
for _, item := range list.GetItems() { |
||||||
|
itemCreated := item.GetCreationTimestamp().Time |
||||||
|
if itemCreated.After(lastCreated) { |
||||||
|
lastCreated = itemCreated |
||||||
|
} |
||||||
|
} |
||||||
|
return lastCreated, nil |
||||||
|
} |
||||||
|
|
||||||
|
// createChecks creates a new check for each check type in the registry.
|
||||||
|
func (r *Runner) createChecks(ctx context.Context) error { |
||||||
|
for _, check := range r.checkRegistry.Checks() { |
||||||
|
obj := &advisorv0alpha1.Check{ |
||||||
|
ObjectMeta: metav1.ObjectMeta{ |
||||||
|
GenerateName: "check-", |
||||||
|
Namespace: metav1.NamespaceDefault, |
||||||
|
Labels: map[string]string{ |
||||||
|
checks.TypeLabel: check.ID(), |
||||||
|
}, |
||||||
|
}, |
||||||
|
Spec: advisorv0alpha1.CheckSpec{}, |
||||||
|
} |
||||||
|
id := obj.GetStaticMetadata().Identifier() |
||||||
|
_, err := r.client.Create(ctx, id, obj, resource.CreateOptions{}) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("error creating check: %w", err) |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
@ -0,0 +1,131 @@ |
|||||||
|
package checkscheduler |
||||||
|
|
||||||
|
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/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
type MockCheckService struct { |
||||||
|
checks []checks.Check |
||||||
|
} |
||||||
|
|
||||||
|
func (m *MockCheckService) Checks() []checks.Check { |
||||||
|
return m.checks |
||||||
|
} |
||||||
|
|
||||||
|
type MockClient struct { |
||||||
|
resource.Client |
||||||
|
listFunc func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) |
||||||
|
createFunc func(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) |
||||||
|
} |
||||||
|
|
||||||
|
func (m *MockClient) List(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { |
||||||
|
return m.listFunc(ctx, namespace, options) |
||||||
|
} |
||||||
|
|
||||||
|
func (m *MockClient) Create(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) { |
||||||
|
return m.createFunc(ctx, identifier, obj, options) |
||||||
|
} |
||||||
|
|
||||||
|
type mockCheck struct { |
||||||
|
checks.Check |
||||||
|
|
||||||
|
id string |
||||||
|
steps []checks.Step |
||||||
|
} |
||||||
|
|
||||||
|
func (m *mockCheck) ID() string { |
||||||
|
return m.id |
||||||
|
} |
||||||
|
|
||||||
|
func (m *mockCheck) Steps() []checks.Step { |
||||||
|
return m.steps |
||||||
|
} |
||||||
|
|
||||||
|
func TestRunner_Run_ErrorOnList(t *testing.T) { |
||||||
|
mockCheckService := &MockCheckService{} |
||||||
|
mockClient := &MockClient{ |
||||||
|
listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { |
||||||
|
return nil, errors.New("list error") |
||||||
|
}, |
||||||
|
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { |
||||||
|
return &advisorv0alpha1.Check{}, nil |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
runner := &Runner{ |
||||||
|
checkRegistry: mockCheckService, |
||||||
|
client: mockClient, |
||||||
|
} |
||||||
|
|
||||||
|
err := runner.Run(context.Background()) |
||||||
|
assert.Error(t, err) |
||||||
|
} |
||||||
|
|
||||||
|
func TestRunner_checkLastCreated_ErrorOnList(t *testing.T) { |
||||||
|
mockClient := &MockClient{ |
||||||
|
listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { |
||||||
|
return nil, errors.New("list error") |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
runner := &Runner{ |
||||||
|
client: mockClient, |
||||||
|
} |
||||||
|
|
||||||
|
lastCreated, err := runner.checkLastCreated(context.Background()) |
||||||
|
assert.Error(t, err) |
||||||
|
assert.True(t, lastCreated.IsZero()) |
||||||
|
} |
||||||
|
|
||||||
|
func TestRunner_createChecks_ErrorOnCreate(t *testing.T) { |
||||||
|
mockCheckService := &MockCheckService{ |
||||||
|
checks: []checks.Check{ |
||||||
|
&mockCheck{ |
||||||
|
id: "check-1", |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
mockClient := &MockClient{ |
||||||
|
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { |
||||||
|
return nil, errors.New("create error") |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
runner := &Runner{ |
||||||
|
checkRegistry: mockCheckService, |
||||||
|
client: mockClient, |
||||||
|
} |
||||||
|
|
||||||
|
err := runner.createChecks(context.Background()) |
||||||
|
assert.Error(t, err) |
||||||
|
} |
||||||
|
|
||||||
|
func TestRunner_createChecks_Success(t *testing.T) { |
||||||
|
mockCheckService := &MockCheckService{ |
||||||
|
checks: []checks.Check{ |
||||||
|
&mockCheck{ |
||||||
|
id: "check-1", |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
mockClient := &MockClient{ |
||||||
|
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) { |
||||||
|
return &advisorv0alpha1.Check{}, nil |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
runner := &Runner{ |
||||||
|
checkRegistry: mockCheckService, |
||||||
|
client: mockClient, |
||||||
|
} |
||||||
|
|
||||||
|
err := runner.createChecks(context.Background()) |
||||||
|
assert.NoError(t, err) |
||||||
|
} |
||||||
Loading…
Reference in new issue