Public source validation / validate (push) Failing after 3m8s
112 lines
3.7 KiB
Go
112 lines
3.7 KiB
Go
package alertdefaults
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/itworx/pulse/internal/alert"
|
|
"github.com/itworx/pulse/internal/metriccatalog"
|
|
)
|
|
|
|
//go:embed seed.json
|
|
var seedFS embed.FS
|
|
|
|
type seedDocument struct {
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
Rules []alert.Document `json:"rules"`
|
|
}
|
|
|
|
type RuleStore interface {
|
|
Create(context.Context, string, alert.Document, string) (alert.Rule, alert.Version, error)
|
|
Get(context.Context, string) (alert.Rule, error)
|
|
// Update is optional for the seed: stores that also implement it allow the
|
|
// seed to refresh an implementation-owned default that has never been edited
|
|
// (revision 1) when a newer seed corrects it. See Seed.
|
|
}
|
|
|
|
// RuleUpdater is implemented by stores that support versioned updates. The seed
|
|
// uses it only for rules that are still at revision 1 (created by the seed and
|
|
// never changed by an operator), so operator edits are never overwritten.
|
|
type RuleUpdater interface {
|
|
Update(ctx context.Context, id, actor string, expected int64, document alert.Document, changeSummary string) (alert.Rule, error)
|
|
}
|
|
|
|
type Report struct {
|
|
Added int
|
|
Existing int
|
|
Refreshed int
|
|
}
|
|
|
|
func Load(registry metriccatalog.Registry) ([]alert.Document, error) {
|
|
raw, err := seedFS.ReadFile("seed.json")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read embedded alert defaults: %w", err)
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.DisallowUnknownFields()
|
|
var bundle seedDocument
|
|
if err := decoder.Decode(&bundle); err != nil {
|
|
return nil, fmt.Errorf("decode embedded alert defaults: %w", err)
|
|
}
|
|
if bundle.SchemaVersion != 1 || len(bundle.Rules) == 0 {
|
|
return nil, errors.New("embedded alert defaults have an invalid bundle")
|
|
}
|
|
seen := make(map[string]struct{}, len(bundle.Rules))
|
|
for _, rule := range bundle.Rules {
|
|
if _, ok := seen[rule.ID]; ok {
|
|
return nil, fmt.Errorf("embedded alert defaults contain duplicate rule %q", rule.ID)
|
|
}
|
|
seen[rule.ID] = struct{}{}
|
|
if err := rule.Validate(registry); err != nil {
|
|
return nil, fmt.Errorf("validate embedded alert default %q: %w", rule.ID, err)
|
|
}
|
|
}
|
|
return bundle.Rules, nil
|
|
}
|
|
|
|
func Seed(ctx context.Context, store RuleStore, registry metriccatalog.Registry, actor string) (Report, error) {
|
|
if store == nil {
|
|
return Report{}, alert.ErrUnavailable
|
|
}
|
|
rules, err := Load(registry)
|
|
if err != nil {
|
|
return Report{}, err
|
|
}
|
|
report := Report{}
|
|
for _, document := range rules {
|
|
if _, _, err := store.Create(ctx, actor, document, "system default seed v1"); err == nil {
|
|
report.Added++
|
|
} else if errors.Is(err, alert.ErrConflict) {
|
|
existing, getErr := store.Get(ctx, document.ID)
|
|
if getErr != nil {
|
|
return Report{}, fmt.Errorf("verify existing alert default %q: %w", document.ID, getErr)
|
|
}
|
|
if updater, ok := store.(RuleUpdater); ok && existing.Revision == 1 && !sameDocument(existing.Document, document) {
|
|
// The stored default is untouched since the seed created it, but the
|
|
// seed itself changed (for example a corrected metric binding). Refresh
|
|
// it through the normal versioned update path so history is kept.
|
|
if _, updErr := updater.Update(ctx, document.ID, actor, existing.Revision, document, "system default seed v1 refresh"); updErr != nil {
|
|
return Report{}, fmt.Errorf("refresh alert default %q: %w", document.ID, updErr)
|
|
}
|
|
report.Refreshed++
|
|
continue
|
|
}
|
|
report.Existing++
|
|
} else {
|
|
return Report{}, fmt.Errorf("seed alert default %q: %w", document.ID, err)
|
|
}
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
// sameDocument compares two rule documents by their canonical JSON encoding.
|
|
func sameDocument(a, b alert.Document) bool {
|
|
left, errA := json.Marshal(a)
|
|
right, errB := json.Marshal(b)
|
|
return errA == nil && errB == nil && bytes.Equal(left, right)
|
|
}
|