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 }