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  }