influxdb/checks/service.go

570 lines
14 KiB
Go

package checks
import (
"context"
"fmt"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/platform"
"github.com/influxdata/influxdb/v2/kit/platform/errors"
"github.com/influxdata/influxdb/v2/kit/tracing"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/notification/check"
"github.com/influxdata/influxdb/v2/query/fluxlang"
"github.com/influxdata/influxdb/v2/snowflake"
"github.com/influxdata/influxdb/v2/task/taskmodel"
"go.uber.org/zap"
)
var _ influxdb.CheckService = (*Service)(nil)
// Service is a check service
// It provides all the operations needed to manage checks
type Service struct {
kv kv.Store
log *zap.Logger
orgs influxdb.OrganizationService
tasks taskmodel.TaskService
timeGenerator influxdb.TimeGenerator
idGenerator platform.IDGenerator
checkStore *kv.IndexStore
}
// NewService constructs and configures a new checks.Service
func NewService(logger *zap.Logger, store kv.Store, orgs influxdb.OrganizationService, tasks taskmodel.TaskService) *Service {
return &Service{
kv: store,
log: logger,
orgs: orgs,
tasks: tasks,
timeGenerator: influxdb.RealTimeGenerator{},
idGenerator: snowflake.NewIDGenerator(),
checkStore: newCheckStore(),
}
}
func newCheckStore() *kv.IndexStore {
const resource = "check"
var decEndpointEntFn kv.DecodeBucketValFn = func(key, val []byte) ([]byte, interface{}, error) {
ch, err := check.UnmarshalJSON(val)
return key, ch, err
}
var decValToEntFn kv.ConvertValToEntFn = func(_ []byte, v interface{}) (kv.Entity, error) {
ch, ok := v.(influxdb.Check)
if err := kv.IsErrUnexpectedDecodeVal(ok); err != nil {
return kv.Entity{}, err
}
return kv.Entity{
PK: kv.EncID(ch.GetID()),
UniqueKey: kv.Encode(
kv.EncID(ch.GetOrgID()),
kv.EncString(ch.GetName()),
),
Body: ch,
}, nil
}
return &kv.IndexStore{
Resource: resource,
EntStore: kv.NewStoreBase(
resource,
[]byte("checksv1"),
kv.EncIDKey,
kv.EncBodyJSON,
decEndpointEntFn,
decValToEntFn,
),
IndexStore: kv.NewOrgNameKeyStore(resource, []byte("checkindexv1"), false),
}
}
// FindCheckByID retrieves a check by id.
func (s *Service) FindCheckByID(ctx context.Context, id platform.ID) (influxdb.Check, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var c influxdb.Check
err := s.kv.View(ctx, func(tx kv.Tx) error {
chkVal, err := s.findCheckByID(ctx, tx, id)
if err != nil {
return err
}
c = chkVal
return nil
})
if err != nil {
return nil, err
}
return c, nil
}
func (s *Service) findCheckByID(ctx context.Context, tx kv.Tx, id platform.ID) (influxdb.Check, error) {
chkVal, err := s.checkStore.FindEnt(ctx, tx, kv.Entity{PK: kv.EncID(id)})
if err != nil {
return nil, err
}
return chkVal.(influxdb.Check), nil
}
func (s *Service) findCheckByName(ctx context.Context, tx kv.Tx, orgID platform.ID, name string) (influxdb.Check, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
chVal, err := s.checkStore.FindEnt(ctx, tx, kv.Entity{
UniqueKey: kv.Encode(kv.EncID(orgID), kv.EncString(name)),
})
if kv.IsNotFound(err) {
return nil, &errors.Error{
Code: errors.ENotFound,
Err: err,
}
}
if err != nil {
return nil, err
}
return chVal.(influxdb.Check), nil
}
// FindCheck retrieves a check using an arbitrary check filter.
// Filters using ID, or OrganizationID and check Name should be efficient.
// Other filters will do a linear scan across checks until it finds a match.
func (s *Service) FindCheck(ctx context.Context, filter influxdb.CheckFilter) (influxdb.Check, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if filter.ID != nil {
return s.FindCheckByID(ctx, *filter.ID)
}
if filter.Org != nil {
o, err := s.orgs.FindOrganization(ctx, influxdb.OrganizationFilter{Name: filter.Org})
if err != nil {
return nil, err
}
filter.OrgID = &o.ID
}
var c influxdb.Check
err := s.kv.View(ctx, func(tx kv.Tx) error {
if filter.OrgID != nil && filter.Name != nil {
ch, err := s.findCheckByName(ctx, tx, *filter.OrgID, *filter.Name)
c = ch
return err
}
var prefix []byte
if filter.OrgID != nil {
ent := kv.Entity{UniqueKey: kv.EncID(*filter.OrgID)}
prefix, _ = s.checkStore.IndexStore.EntKey(ctx, ent)
}
filterFn := filterChecksFn(filter)
return s.checkStore.Find(ctx, tx, kv.FindOpts{
Prefix: prefix,
Limit: 1,
FilterEntFn: func(k []byte, v interface{}) bool {
ch, ok := v.(influxdb.Check)
if err := kv.IsErrUnexpectedDecodeVal(ok); err != nil {
return false
}
return filterFn(ch)
},
CaptureFn: func(key []byte, decodedVal interface{}) error {
c, _ = decodedVal.(influxdb.Check)
return nil
},
})
})
if err != nil {
return nil, err
}
if c == nil {
return nil, &errors.Error{
Code: errors.ENotFound,
Msg: "check not found",
}
}
return c, nil
}
func filterChecksFn(filter influxdb.CheckFilter) func(c influxdb.Check) bool {
return func(c influxdb.Check) bool {
if filter.ID != nil && c.GetID() != *filter.ID {
return false
}
if filter.OrgID != nil && c.GetOrgID() != *filter.OrgID {
return false
}
if filter.Name != nil && c.GetName() != *filter.Name {
return false
}
return true
}
}
// FindChecks retrieves all checks that match an arbitrary check filter.
// Filters using ID, or OrganizationID and check Name should be efficient.
// Other filters will do a linear scan across all checks searching for a match.
func (s *Service) FindChecks(ctx context.Context, filter influxdb.CheckFilter, opts ...influxdb.FindOptions) ([]influxdb.Check, int, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if filter.ID != nil {
c, err := s.FindCheckByID(ctx, *filter.ID)
if err != nil {
return nil, 0, err
}
return []influxdb.Check{c}, 1, nil
}
if filter.Org != nil {
o, err := s.orgs.FindOrganization(ctx, influxdb.OrganizationFilter{Name: filter.Org})
if err != nil {
return nil, 0, &errors.Error{Err: err}
}
filter.OrgID = &o.ID
}
var checks []influxdb.Check
err := s.kv.View(ctx, func(tx kv.Tx) error {
var opt influxdb.FindOptions
if len(opts) > 0 {
opt = opts[0]
}
filterFn := filterChecksFn(filter)
return s.checkStore.Find(ctx, tx, kv.FindOpts{
Descending: opt.Descending,
Offset: opt.Offset,
Limit: opt.Limit,
FilterEntFn: func(k []byte, v interface{}) bool {
ch, ok := v.(influxdb.Check)
if err := kv.IsErrUnexpectedDecodeVal(ok); err != nil {
return false
}
return filterFn(ch)
},
CaptureFn: func(key []byte, decodedVal interface{}) error {
c, ok := decodedVal.(influxdb.Check)
if err := kv.IsErrUnexpectedDecodeVal(ok); err != nil {
return err
}
checks = append(checks, c)
return nil
},
})
})
if err != nil {
return nil, 0, err
}
return checks, len(checks), nil
}
// CreateCheck creates a influxdb check and sets ID.
func (s *Service) CreateCheck(ctx context.Context, c influxdb.CheckCreate, userID platform.ID) (err error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if err := c.Status.Valid(); err != nil {
return err
}
if c.GetOrgID().Valid() {
if _, err := s.orgs.FindOrganizationByID(ctx, c.GetOrgID()); err != nil {
return &errors.Error{
Code: errors.ENotFound,
Op: influxdb.OpCreateCheck,
Err: err,
}
}
}
c.SetID(s.idGenerator.ID())
c.SetOwnerID(userID)
now := s.timeGenerator.Now()
c.SetCreatedAt(now)
c.SetUpdatedAt(now)
if err := c.Valid(fluxlang.DefaultService); err != nil {
return err
}
// create task initially in inactive state
t, err := s.createCheckTask(ctx, c)
if err != nil {
return &errors.Error{
Code: errors.EInvalid,
Msg: "Could not create task from check",
Err: err,
}
}
c.SetTaskID(t.ID)
err = s.kv.Update(ctx, func(tx kv.Tx) error {
return s.putCheck(ctx, tx, c, kv.PutNew())
})
// something went wrong persisting new check
// so remove associated task
if err != nil {
if derr := s.tasks.DeleteTask(ctx, t.ID); derr != nil {
msg := fmt.Sprintf("error removing task %q for check %q in org %q", t.ID, c.GetName(), c.GetOrgID())
s.log.Error(msg, zap.Error(derr))
}
return err
}
// update task to be in matching state to check
if influxdb.Status(t.Status) != c.Status {
_, err = s.tasks.UpdateTask(ctx, t.ID, taskmodel.TaskUpdate{
Status: strPtr(string(c.Status)),
})
}
return err
}
func (s *Service) createCheckTask(ctx context.Context, c influxdb.CheckCreate) (*taskmodel.Task, error) {
script, err := c.GenerateFlux(fluxlang.DefaultService)
if err != nil {
return nil, err
}
tc := taskmodel.TaskCreate{
Type: c.Type(),
Flux: script,
OwnerID: c.GetOwnerID(),
OrganizationID: c.GetOrgID(),
// task initially in inactive state to ensure it isn't
// scheduled until check is persisted and active
Status: string(influxdb.Inactive),
}
t, err := s.tasks.CreateTask(ctx, tc)
if err != nil {
return nil, err
}
return t, nil
}
// PutCheck will put a check without setting an ID.
func (s *Service) PutCheck(ctx context.Context, c influxdb.Check) error {
if err := c.Valid(fluxlang.DefaultService); err != nil {
return err
}
return s.kv.Update(ctx, func(tx kv.Tx) error {
return s.putCheck(ctx, tx, c)
})
}
func (s *Service) putCheck(ctx context.Context, tx kv.Tx, c influxdb.Check, opts ...kv.PutOptionFn) error {
return s.checkStore.Put(ctx, tx, kv.Entity{
PK: kv.EncID(c.GetID()),
UniqueKey: kv.Encode(kv.EncID(c.GetOrgID()), kv.EncString(c.GetName())),
Body: c,
}, opts...)
}
// PatchCheck updates a check according the parameters set on upd.
func (s *Service) PatchCheck(ctx context.Context, id platform.ID, upd influxdb.CheckUpdate) (influxdb.Check, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var check influxdb.Check
if err := s.kv.Update(ctx, func(tx kv.Tx) error {
c, err := s.findCheckByID(ctx, tx, id)
if err != nil {
return err
}
c, err = s.patchCheck(ctx, tx, c, upd)
if err != nil {
return err
}
check = c
return nil
}); err != nil {
return nil, err
}
if err := s.patchCheckTask(ctx, check.GetTaskID(), upd); err != nil {
return nil, err
}
return check, nil
}
// UpdateCheck updates the check.
func (s *Service) UpdateCheck(ctx context.Context, id platform.ID, chk influxdb.CheckCreate) (influxdb.Check, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var check influxdb.Check
if err := s.kv.Update(ctx, func(tx kv.Tx) error {
c, err := s.updateCheck(ctx, tx, id, chk)
if err != nil {
return err
}
check = c
return nil
}); err != nil {
return nil, err
}
if err := s.updateCheckTask(ctx, chk); err != nil {
return nil, err
}
return check, nil
}
func (s *Service) updateCheckTask(ctx context.Context, chk influxdb.CheckCreate) error {
flux, err := chk.GenerateFlux(fluxlang.DefaultService)
if err != nil {
return err
}
tu := taskmodel.TaskUpdate{
Flux: &flux,
Description: strPtr(chk.GetDescription()),
}
if chk.Status != "" {
tu.Status = strPtr(string(chk.Status))
}
if _, err := s.tasks.UpdateTask(ctx, chk.GetTaskID(), tu); err != nil {
return err
}
return err
}
func (s *Service) patchCheckTask(ctx context.Context, taskID platform.ID, upd influxdb.CheckUpdate) error {
tu := taskmodel.TaskUpdate{
Description: upd.Description,
}
if upd.Status != nil {
tu.Status = strPtr(string(*upd.Status))
}
if _, err := s.tasks.UpdateTask(ctx, taskID, tu); err != nil {
return err
}
return nil
}
func (s *Service) updateCheck(ctx context.Context, tx kv.Tx, id platform.ID, chk influxdb.CheckCreate) (influxdb.Check, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
current, err := s.findCheckByID(ctx, tx, id)
if err != nil {
return nil, err
}
chk.SetTaskID(current.GetTaskID())
if chk.GetName() != current.GetName() {
c0, err := s.findCheckByName(ctx, tx, current.GetOrgID(), chk.GetName())
if err == nil && c0.GetID() != id {
return nil, &errors.Error{
Code: errors.EConflict,
Msg: "check name is not unique",
}
}
ent := kv.Entity{
UniqueKey: kv.Encode(kv.EncID(current.GetOrgID()), kv.EncString(current.GetName())),
}
if err := s.checkStore.IndexStore.DeleteEnt(ctx, tx, ent); err != nil {
return nil, err
}
}
// ID, OrganizationID, and OwnerID can not be updated.
chk.SetID(current.GetID())
chk.SetOrgID(current.GetOrgID())
chk.SetOwnerID(current.GetOwnerID())
chk.SetCreatedAt(current.GetCRUDLog().CreatedAt)
chk.SetUpdatedAt(s.timeGenerator.Now())
if err := chk.Valid(fluxlang.DefaultService); err != nil {
return nil, err
}
if err := chk.Status.Valid(); err != nil {
return nil, err
}
if err := s.putCheck(ctx, tx, chk.Check); err != nil {
return nil, err
}
return chk.Check, nil
}
func (s *Service) patchCheck(ctx context.Context, tx kv.Tx, check influxdb.Check, upd influxdb.CheckUpdate) (influxdb.Check, error) {
if upd.Name != nil {
check.SetName(*upd.Name)
}
if upd.Description != nil {
check.SetDescription(*upd.Description)
}
check.SetUpdatedAt(s.timeGenerator.Now())
if err := check.Valid(fluxlang.DefaultService); err != nil {
return nil, err
}
if err := s.putCheck(ctx, tx, check, kv.PutUpdate()); err != nil {
return nil, err
}
return check, nil
}
// DeleteCheck deletes a check and prunes it from the index.
func (s *Service) DeleteCheck(ctx context.Context, id platform.ID) error {
ch, err := s.FindCheckByID(ctx, id)
if err != nil {
return err
}
if err := s.tasks.DeleteTask(ctx, ch.GetTaskID()); err != nil {
return err
}
return s.kv.Update(ctx, func(tx kv.Tx) error {
return s.checkStore.DeleteEnt(ctx, tx, kv.Entity{
PK: kv.EncID(id),
})
})
}
func strPtr(s string) *string {
ss := new(string)
*ss = s
return ss
}