github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/aagent/watchers/expressionwatcher/expression.go (about)

     1  // Copyright (c) 2024, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package expressionwatcher
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/choria-io/go-choria/aagent/model"
    14  	"github.com/choria-io/go-choria/aagent/util"
    15  	"github.com/choria-io/go-choria/aagent/watchers/event"
    16  	"github.com/choria-io/go-choria/aagent/watchers/watcher"
    17  	iu "github.com/choria-io/go-choria/internal/util"
    18  )
    19  
    20  type State int
    21  
    22  const (
    23  	SuccessWhen State = iota
    24  	FailWhen
    25  	NoMatch
    26  	Skipped
    27  	Error
    28  
    29  	wtype   = "expression"
    30  	version = "v1"
    31  )
    32  
    33  var stateNames = map[State]string{
    34  	SuccessWhen: "success_when",
    35  	FailWhen:    "failed_when",
    36  	NoMatch:     "no_match",
    37  	Skipped:     "skipped",
    38  	Error:       "error",
    39  }
    40  
    41  type properties struct {
    42  	FailWhen    string `mapstructure:"fail_when"`
    43  	SuccessWhen string `mapstructure:"success_when"`
    44  }
    45  
    46  type Watcher struct {
    47  	*watcher.Watcher
    48  	properties *properties
    49  
    50  	interval time.Duration
    51  	name     string
    52  	machine  model.Machine
    53  
    54  	previous  State
    55  	terminate chan struct{}
    56  	mu        *sync.Mutex
    57  }
    58  
    59  func New(machine model.Machine, name string, states []string, failEvent string, successEvent string, interval string, ai time.Duration, properties map[string]any) (any, error) {
    60  	var err error
    61  
    62  	ew := &Watcher{
    63  		name:      name,
    64  		machine:   machine,
    65  		interval:  10 * time.Second,
    66  		terminate: make(chan struct{}),
    67  		previous:  Skipped,
    68  		mu:        &sync.Mutex{},
    69  	}
    70  
    71  	ew.Watcher, err = watcher.NewWatcher(name, wtype, ai, states, machine, failEvent, successEvent)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	err = ew.setProperties(properties)
    77  	if err != nil {
    78  		return nil, fmt.Errorf("could not set properties: %s", err)
    79  	}
    80  
    81  	if interval != "" {
    82  		ew.interval, err = iu.ParseDuration(interval)
    83  		if err != nil {
    84  			return nil, fmt.Errorf("invalid interval: %v", err)
    85  		}
    86  
    87  		if ew.interval < 500*time.Millisecond {
    88  			return nil, fmt.Errorf("interval %v is too small", ew.interval)
    89  		}
    90  	}
    91  
    92  	return ew, nil
    93  }
    94  
    95  func (w *Watcher) Run(ctx context.Context, wg *sync.WaitGroup) {
    96  	defer wg.Done()
    97  
    98  	w.Infof("Expression watcher starting")
    99  
   100  	tick := time.NewTicker(w.interval)
   101  
   102  	for {
   103  		select {
   104  		case <-tick.C:
   105  			w.Debugf("Performing watch due to ticker")
   106  			w.performWatch()
   107  
   108  		case <-w.StateChangeC():
   109  			w.Debugf("Performing watch due to state change")
   110  			w.performWatch()
   111  
   112  		case <-w.terminate:
   113  			w.Infof("Handling terminate notification")
   114  			return
   115  
   116  		case <-ctx.Done():
   117  			w.Infof("Stopping on context interrupt")
   118  			return
   119  		}
   120  	}
   121  }
   122  func (w *Watcher) performWatch() {
   123  	err := w.handleCheck(w.watch())
   124  	if err != nil {
   125  		w.Errorf("could not handle watcher event: %s", err)
   126  	}
   127  }
   128  
   129  func (w *Watcher) handleCheck(state State, err error) error {
   130  	w.mu.Lock()
   131  	previous := w.previous
   132  	w.previous = state
   133  	w.mu.Unlock()
   134  
   135  	// shouldn't happen but just a safety here
   136  	if err != nil {
   137  		state = Error
   138  	}
   139  
   140  	switch state {
   141  	case SuccessWhen:
   142  		w.NotifyWatcherState(w.CurrentState())
   143  
   144  		if previous != SuccessWhen {
   145  			return w.SuccessTransition()
   146  		}
   147  
   148  	case FailWhen:
   149  		w.NotifyWatcherState(w.CurrentState())
   150  
   151  		if previous != FailWhen {
   152  			return w.FailureTransition()
   153  		}
   154  
   155  	case Error:
   156  		if err != nil {
   157  			w.Errorf("Evaluating expressions failed: %v", err)
   158  		}
   159  
   160  		w.NotifyWatcherState(w.CurrentState())
   161  	}
   162  
   163  	return nil
   164  }
   165  
   166  func (w *Watcher) watch() (state State, err error) {
   167  	if !w.ShouldWatch() {
   168  		return Skipped, nil
   169  	}
   170  
   171  	if w.properties.SuccessWhen != "" {
   172  		res, err := w.evaluateExpression(w.properties.SuccessWhen)
   173  		if err != nil {
   174  			return Error, err
   175  		}
   176  
   177  		if res {
   178  			return SuccessWhen, nil
   179  		}
   180  	}
   181  
   182  	if w.properties.FailWhen != "" {
   183  		res, err := w.evaluateExpression(w.properties.FailWhen)
   184  		if err != nil {
   185  			return Error, err
   186  		}
   187  
   188  		if res {
   189  			return FailWhen, nil
   190  		}
   191  	}
   192  
   193  	return NoMatch, nil
   194  }
   195  
   196  func (w *Watcher) CurrentState() any {
   197  	w.mu.Lock()
   198  	defer w.mu.Unlock()
   199  
   200  	return &StateNotification{
   201  		Event:           event.New(w.name, wtype, version, w.machine),
   202  		PreviousOutcome: stateNames[w.previous],
   203  	}
   204  }
   205  
   206  func (w *Watcher) Delete() {
   207  	close(w.terminate)
   208  }
   209  
   210  func (w *Watcher) setProperties(props map[string]any) error {
   211  	if w.properties == nil {
   212  		w.properties = &properties{}
   213  	}
   214  
   215  	err := util.ParseMapStructure(props, w.properties)
   216  	if err != nil {
   217  		return err
   218  	}
   219  
   220  	return w.validate()
   221  }
   222  
   223  func (w *Watcher) validate() error {
   224  	if w.interval < time.Second {
   225  		return fmt.Errorf("interval should be more than 1 second: %v", w.interval)
   226  	}
   227  
   228  	if w.properties.FailWhen == "" && w.properties.SuccessWhen == "" {
   229  		return fmt.Errorf("success_when or fail_when is required")
   230  	}
   231  
   232  	return nil
   233  }