blob: bc0912056de44ad984bb900ca86b91d4d28b8649 [file] [log] [blame]
package silence
import (
"context"
"encoding/json"
"fmt"
"regexp"
"time"
"cloud.google.com/go/datastore"
"go.skia.org/infra/am/go/note"
"go.skia.org/infra/go/ds"
"go.skia.org/infra/go/human"
"go.skia.org/infra/go/paramtools"
"go.skia.org/infra/go/sklog"
)
const (
SILENCE_PARENT_KEY = "-silence-"
NUM_RECENTLY_ARCHIVED = 500
)
// Silence is a filter that matches Incidents and is used to silence them.
type Silence struct {
Key string `json:"key" datastore:"-"`
Active bool `json:"active" datastore:"active"`
User string `json:"user" datastore:"user"`
ParamSet paramtools.ParamSet `json:"param_set" datastore:"-"`
ParamSetSerial string `json:"-" datastore:"param_set_serial,noindex"`
Created int64 `json:"created" datastore:"created"`
Updated int64 `json:"updated" datastore:"updated"`
Duration string `json:"duration" datastore:"duration"`
Notes []note.Note `json:"notes" datastore:"notes,flatten"`
}
// New creates a new Silence.
//
// user - Email address of the person that created the silence.
func New(user string) *Silence {
now := time.Now().Unix()
return &Silence{
Active: true,
User: user,
ParamSet: paramtools.ParamSet{},
Created: now,
Updated: now,
Duration: "2h",
Notes: []note.Note{},
}
}
// Load converts the JSON paramset back into a paramset.
func (silence *Silence) Load(ps []datastore.Property) error {
if err := datastore.LoadStruct(silence, ps); err != nil {
return err
}
if err := json.Unmarshal([]byte(silence.ParamSetSerial), &silence.ParamSet); err != nil {
return err
}
return nil
}
// Save serializes the paramset as JSON for storing in the Datastore.
func (silence *Silence) Save() ([]datastore.Property, error) {
b, err := json.Marshal(silence.ParamSet)
if err != nil {
return nil, err
}
silence.ParamSetSerial = string(b)
return datastore.SaveStruct(silence)
}
// ValidateRegexes returns an error if the silence is not valid.
func (silence *Silence) ValidateRegexes() error {
for _, vals := range silence.ParamSet {
for _, v := range vals {
if _, err := regexp.Compile(fmt.Sprintf(`^%s$`, v)); err != nil {
return err
}
}
}
return nil
}
// Store saves and updates silences in Cloud Datastore.
type Store struct {
ds *datastore.Client
}
// NewStore creates a new Store from the given Datastore client.
func NewStore(ds *datastore.Client) *Store {
store := &Store{
ds: ds,
}
// Start a go routine that expires old silences.
go func(store *Store) {
for range time.Tick(15 * time.Second) {
now := time.Now()
silences, err := store.GetAll()
if err != nil {
sklog.Errorf("Silence expirer failed to retrieve silences: %s", err)
}
for _, s := range silences {
d, err := human.ParseDuration(s.Duration)
if err != nil {
sklog.Errorf("Silence has invalid duration: %s", err)
continue
}
if time.Unix(s.Created, 0).Add(d).Before(now) {
if _, err := store.Archive(s.Key); err != nil {
sklog.Errorf("Failed to archive expired silence: %s", err)
}
}
}
}
}(store)
return store
}
func (s *Store) Put(silence *Silence) (*Silence, error) {
_, err := human.ParseDuration(silence.Duration)
if err != nil {
return nil, fmt.Errorf("Silence has invalid duration: %s", err)
}
// Key used if this is a create.
ancestor := ds.NewKey(ds.SILENCE_ACTIVE_PARENT_AM)
ancestor.Name = SILENCE_PARENT_KEY
key := ds.NewKey(ds.SILENCE_AM)
key.Parent = ancestor
if silence.Key != "" {
// This is an update, so use the key provided.
var err error
key, err = datastore.DecodeKey(silence.Key)
if err != nil {
return nil, err
}
}
silence.Active = true
var pendingKey *datastore.PendingKey
commit, err := s.ds.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error {
var err error
if pendingKey, err = tx.Put(key, silence); err != nil {
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("Failed to write Silence: %s", err)
}
silence.Key = commit.Key(pendingKey).Encode()
return silence, nil
}
// _mutate is a helper function for updating Silences inside a transaction.
func (s *Store) _mutate(encodedKey string, mutator func(silence *Silence) error) (*Silence, error) {
key, err := datastore.DecodeKey(encodedKey)
if err != nil {
return nil, err
}
var silence Silence
_, err = s.ds.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error {
if err := tx.Get(key, &silence); err != nil {
return err
}
if err := mutator(&silence); err != nil {
return err
}
if _, err := tx.Put(key, &silence); err != nil {
return err
}
return nil
})
silence.Key = encodedKey
return &silence, err
}
func (s *Store) Archive(encodedKey string) (*Silence, error) {
return s._mutate(encodedKey, func(silence *Silence) error {
silence.Active = false
silence.Updated = time.Now().Unix()
return nil
})
}
func (s *Store) Reactivate(encodedKey, duration, user string) (*Silence, error) {
_, err := human.ParseDuration(duration)
if err != nil {
return nil, fmt.Errorf("Silence has invalid duration: %s", err)
}
return s._mutate(encodedKey, func(silence *Silence) error {
now := time.Now().Unix()
silence.Active = true
silence.Created = now
silence.Updated = now
silence.Duration = duration
silence.Notes = append(silence.Notes, note.Note{
Text: fmt.Sprintf("Reactivated by %q.", user),
Author: user,
TS: now,
})
return nil
})
}
func (s *Store) Delete(encodedKey string) error {
key, err := datastore.DecodeKey(encodedKey)
if err != nil {
return err
}
if err := s.ds.Delete(context.Background(), key); err != nil {
return fmt.Errorf("Failed to delete Silence: %s", err)
}
return nil
}
func (s *Store) AddNote(encodedKey string, note note.Note) (*Silence, error) {
return s._mutate(encodedKey, func(silence *Silence) error {
silence.Updated = time.Now().Unix()
silence.Notes = append(silence.Notes, note)
return nil
})
}
func (s *Store) DeleteNote(encodedKey string, index int) (*Silence, error) {
return s._mutate(encodedKey, func(silence *Silence) error {
if index < 0 || index > len(silence.Notes)-1 {
return fmt.Errorf("Index for delete out of range.")
}
silence.Updated = time.Now().Unix()
silence.Notes = append(silence.Notes[:index], silence.Notes[index+1:]...)
return nil
})
}
// GetAll returns a list of all active Silences.
func (s *Store) GetAll() ([]Silence, error) {
var active []Silence
ancestor := ds.NewKey(ds.SILENCE_ACTIVE_PARENT_AM)
ancestor.Name = SILENCE_PARENT_KEY
q := ds.NewQuery(ds.SILENCE_AM).Filter("active=", true).Ancestor(ancestor)
keys, err := s.ds.GetAll(context.Background(), q, &active)
for i, key := range keys {
if active[i].Key == "" {
active[i].Key = key.Encode()
}
}
return active, err
}
// GetRecentlyArchived returns N most recently archived Silences that were
// updated within the specified duration. updatedWithin can be 0 if we want
// all recently archived silences.
func (s *Store) GetRecentlyArchived(updatedWithin time.Duration) ([]Silence, error) {
var archived []Silence
ancestor := ds.NewKey(ds.SILENCE_ACTIVE_PARENT_AM)
ancestor.Name = SILENCE_PARENT_KEY
modifiedAfter := int64(0)
if updatedWithin != 0 {
modifiedAfter = time.Now().Add(-updatedWithin).Unix()
}
q := ds.NewQuery(ds.SILENCE_AM).Filter("active=", false).Filter("updated>", modifiedAfter).Ancestor(ancestor).Order("-updated").Limit(NUM_RECENTLY_ARCHIVED)
keys, err := s.ds.GetAll(context.Background(), q, &archived)
if err != nil {
return nil, fmt.Errorf("Failed to make query: %s", err)
}
for i, key := range keys {
if archived[i].Key == "" {
archived[i].Key = key.Encode()
}
}
return archived, err
}