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) }