/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package config import ( "fmt" "text/template" "golang.org/x/exp/slices" "gopkg.in/yaml.v3" "git.st8l.com/luxolus/kdnotify/schema/notify" "git.st8l.com/luxolus/kdnotify/schema/notify/state" "git.st8l.com/luxolus/kdnotify/schema/notify/ty" ) // Configuration for WatchHandlers, providing them an evaluation context // and rules for handling notify events type WatchConfig struct { // Arbitrary user input, accessible in WatchMatch.Exec templates Context any `yaml:"context"` // List of rules to use when handling notify events Rules []WatchRule `yaml:"rules"` } // Parse the given yaml, looking for the top level 'watch:' key, returning // the unmarshaled WatchHandle configuration func WatchConfigFromYAML(in []byte) (WatchConfig, error) { var visitor visitWatchConfig err := yaml.Unmarshal(in, &visitor) if err != nil { return WatchConfig{}, fmt.Errorf("while parsing 'watch': %w", err) } return visitor.Watch, nil } // A single rule to evaluate a notify event on. // // Each condition field (State, Type, Instance) is a logical OR // while the fields together are a logical AND. Demonstrated: // // if State.Contains($state) && Type.Contains($type) && Instance.Contains($instance) { // os.Exec(Template) // } // // The one exception is if any check is empty, it is ignored type WatchRule struct { // List of states that apply this rule, ignored if empty State state.State // List of types that apply this rule, ignored if empty Type ty.Type // List of instances that apply this rule, ignored if empty Instance []string // Template to evaluate and execute if the conditions match Exec *template.Template } // Check if the given message matches this WatchRule func (w *WatchRule) Match(msg *notify.VrrpMessage) bool { if w.Type != ty.NULL && !w.Type.Has(msg.Type) { return false } if w.State != state.NULL && !w.State.Has(msg.State) { return false } if len(w.Instance) > 0 && !slices.Contains(w.Instance, msg.Instance) { return false } return true } func (w *WatchRule) UnmarshalYAML(value *yaml.Node) error { var visitor visitWatchRule switch value.Kind { case yaml.AliasNode: return w.UnmarshalYAML(value.Alias) default: err := value.Decode(&visitor) if err != nil { return err } } rule, err := visitor.IntoWatchRule() if err != nil { return fmt.Errorf("while parsing watch rule: %w", err) } *w = rule return nil } type visitWatchConfig struct { Watch WatchConfig `yaml:"watch"` } type visitWatchRule struct { State sslice `yaml:"state,omitempty"` Type sslice `yaml:"type,omitempty"` Instance sslice `yaml:"instance,omitempty"` Exec string `yaml:"exec,omitempty"` } func (v visitWatchRule) IntoWatchRule() (WatchRule, error) { var rule WatchRule for _, val := range v.Type.Value { v := ty.ParseType(val) if v == ty.UNKNOWN { return rule, fmt.Errorf("unknown type: '%s'", val) } rule.Type |= v } for _, val := range v.State.Value { v := state.ParseState(val) if v == state.UNKNOWN { return rule, fmt.Errorf("unknown state: '%s'", val) } rule.State |= v } exec, err := template.New("WatchRule").Parse(v.Exec) if err != nil { return rule, fmt.Errorf("unable to parse exec template: %w", err) } rule.Exec = exec rule.Instance = v.Instance.Value return rule, nil } // Visitor for string or []string Yaml productions type sslice struct { Value []string } func (v *sslice) UnmarshalYAML(value *yaml.Node) error { switch value.Kind { case yaml.AliasNode: return v.UnmarshalYAML(value.Alias) case yaml.SequenceNode: return value.Decode(&v.Value) case yaml.ScalarNode: var single string err := value.Decode(&single) if err != nil { return err } v.Value = append(v.Value, single) return nil default: error := fmt.Sprintf( "unable to parse string or string array at %d:%d from '%s'", value.Line, value.Column, value.Value, ) return &yaml.TypeError{Errors: []string{error}} } }