github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/aagent/watchers/kvwatcher/kv.go (about) 1 // Copyright (c) 2021-2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package kvwatcher 6 7 import ( 8 "bytes" 9 "context" 10 "encoding/json" 11 "fmt" 12 "sync" 13 "time" 14 15 "github.com/choria-io/go-choria/aagent/model" 16 "github.com/choria-io/go-choria/aagent/util" 17 "github.com/choria-io/go-choria/aagent/watchers/event" 18 "github.com/choria-io/go-choria/aagent/watchers/watcher" 19 iu "github.com/choria-io/go-choria/internal/util" 20 "github.com/choria-io/go-choria/providers/kv" 21 "github.com/google/go-cmp/cmp" 22 "github.com/nats-io/nats.go" 23 ) 24 25 type State int 26 27 const ( 28 Error State = iota 29 Changed 30 Unchanged 31 Skipped 32 33 wtype = "kv" 34 version = "v1" 35 pollMode = "poll" 36 watchMode = "watch" 37 ) 38 39 var stateNames = map[State]string{ 40 Error: "error", 41 Changed: "changed", 42 Unchanged: "unchanged", 43 Skipped: "skipped", 44 } 45 46 type properties struct { 47 Bucket string 48 Key string 49 Mode string 50 TransitionOnSuccessfulGet bool `mapstructure:"on_successful_get"` 51 TransitionOnMatch bool `mapstructure:"on_matching_update"` 52 BucketPrefix bool `mapstructure:"bucket_prefix"` 53 } 54 55 type Watcher struct { 56 *watcher.Watcher 57 properties *properties 58 59 name string 60 machine model.Machine 61 kv nats.KeyValue 62 interval time.Duration 63 64 previousVal any 65 previousSeq uint64 66 previousState State 67 polling bool 68 lastPoll time.Time 69 70 terminate chan struct{} 71 mu *sync.Mutex 72 } 73 74 func New(machine model.Machine, name string, states []string, failEvent string, successEvent string, interval string, ai time.Duration, properties map[string]any) (any, error) { 75 var err error 76 77 tw := &Watcher{ 78 name: name, 79 machine: machine, 80 terminate: make(chan struct{}), 81 mu: &sync.Mutex{}, 82 } 83 84 tw.interval, err = iu.ParseDuration(interval) 85 if err != nil { 86 return nil, err 87 } 88 89 tw.Watcher, err = watcher.NewWatcher(name, wtype, ai, states, machine, failEvent, successEvent) 90 if err != nil { 91 return nil, err 92 } 93 94 err = tw.setProperties(properties) 95 if err != nil { 96 return nil, fmt.Errorf("could not set properties: %s", err) 97 } 98 99 return tw, nil 100 } 101 102 func (w *Watcher) setProperties(props map[string]any) error { 103 if w.properties == nil { 104 w.properties = &properties{ 105 BucketPrefix: true, 106 } 107 } 108 109 err := util.ParseMapStructure(props, w.properties) 110 if err != nil { 111 return err 112 } 113 114 return w.validate() 115 } 116 117 func (w *Watcher) validate() error { 118 if w.properties.Bucket == "" { 119 return fmt.Errorf("bucket is required") 120 } 121 122 if w.properties.Mode == "" { 123 w.properties.Mode = pollMode 124 } 125 126 if w.properties.Mode != pollMode && w.properties.Mode != watchMode { 127 return fmt.Errorf("mode should be '%s' or '%s'", pollMode, watchMode) 128 } 129 130 if w.properties.Mode == pollMode && w.properties.Key == "" { 131 return fmt.Errorf("poll mode requires a key") 132 } 133 134 if w.properties.Mode == watchMode { 135 return fmt.Errorf("watch mode not supported") 136 } 137 138 return nil 139 } 140 141 func (w *Watcher) Delete() { 142 close(w.terminate) 143 } 144 145 func (w *Watcher) stopPolling() { 146 w.mu.Lock() 147 w.polling = false 148 w.mu.Unlock() 149 } 150 151 func (w *Watcher) connectKV() error { 152 w.mu.Lock() 153 defer w.mu.Unlock() 154 155 var err error 156 mgr, err := w.machine.JetStreamConnection() 157 if err != nil { 158 return err 159 } 160 161 w.kv, err = kv.NewKV(mgr.NatsConn(), w.properties.Bucket, false) 162 if err != nil { 163 return err 164 } 165 166 return nil 167 } 168 169 func (w *Watcher) poll() (State, error) { 170 if !w.ShouldWatch() { 171 return Skipped, nil 172 } 173 174 w.mu.Lock() 175 if w.polling { 176 w.mu.Unlock() 177 return Skipped, nil 178 } 179 w.polling = true 180 store := w.kv 181 w.mu.Unlock() 182 183 defer w.stopPolling() 184 185 // we try to bind to the store here on every poll so that if the store does not yet exist 186 // at startup we will keep trying until it does 187 if store == nil { 188 err := w.connectKV() 189 if err != nil { 190 return Error, err 191 } 192 } 193 194 lp := w.lastPoll 195 since := time.Since(lp).Round(time.Second) 196 if since < w.interval { 197 w.Debugf("Skipping watch due to last watch %v ago", since) 198 return Skipped, nil 199 } 200 w.lastPoll = time.Now() 201 202 parsedKey, err := w.ProcessTemplate(w.properties.Key) 203 if err != nil { 204 return 0, fmt.Errorf("could not parse template for key: %v", err) 205 } 206 207 w.Infof("Polling for %s.%s", w.properties.Bucket, parsedKey) 208 209 var parsedValue any 210 211 dk := w.dataKey() 212 if w.previousVal == nil { 213 w.previousVal, _ = w.machine.DataGet(dk) 214 } 215 216 val, err := w.kv.Get(parsedKey) 217 if err == nil { 218 // we try to handle json files into a map[string]interface this means nested lookups can be done 219 // in other machines using the lookup template func and it works just fine, deep compares are done 220 // on the entire structure later 221 v := bytes.TrimSpace(val.Value()) 222 if bytes.HasPrefix(v, []byte("{")) && bytes.HasSuffix(v, []byte("}")) { 223 parsedValue = map[string]any{} 224 err := json.Unmarshal(v, &parsedValue) 225 if err != nil { 226 w.Warnf("unmarshal failed: %s", err) 227 } 228 } else if bytes.HasPrefix(v, []byte("[")) && bytes.HasSuffix(v, []byte("]")) { 229 parsedValue = []any{} 230 err := json.Unmarshal(v, &parsedValue) 231 if err != nil { 232 w.Warnf("unmarshal failed: %s", err) 233 } 234 } 235 236 if parsedValue == nil { 237 parsedValue = string(val.Value()) 238 } 239 } 240 241 switch { 242 // key isn't there, nothing was previously found its unchanged 243 case err == nats.ErrKeyNotFound && w.previousVal == nil: 244 return Unchanged, nil 245 246 // key isn't there, we had a value before its a change due to delete 247 case err == nats.ErrKeyNotFound && w.previousVal != nil: 248 w.Debugf("Removing data from %s", dk) 249 err = w.machine.DataDelete(dk) 250 if err != nil { 251 w.Errorf("Could not delete key %s from machine: %s", dk, err) 252 return Error, err 253 } 254 255 w.previousVal = nil 256 257 return Changed, err 258 259 // get failed in an unknown way 260 case err != nil: 261 w.Errorf("Could not get %s.%s: %s", w.properties.Bucket, parsedKey, err) 262 return Error, err 263 264 // a change 265 case !cmp.Equal(w.previousVal, parsedValue): 266 err = w.machine.DataPut(dk, parsedValue) 267 if err != nil { 268 return Error, err 269 } 270 271 w.previousSeq = val.Revision() 272 w.previousVal = parsedValue 273 return Changed, nil 274 275 // a put that didn't update, but we are asked to transition anyway 276 // we do not trigger this on first start of the machine only once its running (previousSeq is 0) 277 case cmp.Equal(w.previousVal, parsedValue) && w.properties.TransitionOnMatch && w.previousSeq > 0 && val.Revision() > w.previousSeq: 278 w.previousSeq = val.Revision() 279 return Changed, nil 280 281 default: 282 w.previousSeq = val.Revision() 283 if w.properties.TransitionOnSuccessfulGet { 284 return Changed, nil 285 } 286 287 return Unchanged, nil 288 } 289 } 290 291 func (w *Watcher) handleState(s State, err error) error { 292 w.Debugf("handling state for %s.%s: %s: err:%v", w.properties.Bucket, w.properties.Key, stateNames[s], err) 293 294 w.mu.Lock() 295 w.previousState = s 296 w.mu.Unlock() 297 298 switch s { 299 case Error: 300 return w.FailureTransition() 301 case Changed: 302 return w.SuccessTransition() 303 case Unchanged, Skipped: 304 } 305 306 return nil 307 } 308 309 func (w *Watcher) dataKey() string { 310 parsedKey, err := w.ProcessTemplate(w.properties.Key) 311 if err != nil { 312 w.Warnf("Failed to parse key value %s: %v", w.properties.Key, err) 313 return w.properties.Key 314 } 315 316 if w.properties.BucketPrefix { 317 return fmt.Sprintf("%s_%s", w.properties.Bucket, parsedKey) 318 } 319 320 return parsedKey 321 } 322 323 func (w *Watcher) pollKey(ctx context.Context, wg *sync.WaitGroup) { 324 defer wg.Done() 325 326 dk := w.dataKey() 327 w.previousVal, _ = w.machine.DataGet(dk) 328 329 w.handleState(w.poll()) 330 331 ticker := time.NewTicker(w.interval) 332 333 for { 334 select { 335 case <-ticker.C: 336 w.handleState(w.poll()) 337 338 case <-ctx.Done(): 339 return 340 } 341 } 342 } 343 344 func (w *Watcher) Run(ctx context.Context, wg *sync.WaitGroup) { 345 defer wg.Done() 346 347 if w.properties.Key == "" { 348 w.Infof("Key-Value watcher starting with bucket %q in %q mode", w.properties.Bucket, w.properties.Mode) 349 } else { 350 w.Infof("Key-Value watcher starting with bucket %q and key %q in %q mode", w.properties.Bucket, w.properties.Key, w.properties.Mode) 351 } 352 353 watchCtx, watchCancel := context.WithCancel(ctx) 354 defer watchCancel() 355 356 switch w.properties.Mode { 357 case watchMode: 358 // TODO: set up watcher 359 360 case pollMode: 361 wg.Add(1) 362 go w.pollKey(watchCtx, wg) 363 } 364 365 for { 366 select { 367 case <-w.StateChangeC(): 368 w.handleState(w.poll()) 369 370 case <-w.terminate: 371 w.Infof("Handling terminate notification") 372 watchCancel() 373 return 374 case <-ctx.Done(): 375 w.Infof("Stopping on context interrupt") 376 return 377 } 378 } 379 } 380 381 func (w *Watcher) CurrentState() any { 382 w.mu.Lock() 383 defer w.mu.Unlock() 384 385 s := &StateNotification{ 386 Event: event.New(w.name, wtype, version, w.machine), 387 State: stateNames[w.previousState], 388 Key: w.properties.Key, 389 Bucket: w.properties.Bucket, 390 Mode: w.properties.Mode, 391 } 392 393 return s 394 }