config: WatchConfig, WatchRules

These are representations of user defined rules and contextual
information, which can be used to handle notify events.

Specifically, we allow this user to provide (and later access via .Cxt)
arbitrary data under the watch.context key, while providing us rules
under watch.rules.

Each rule consists of an 'Exec' template, and three conditions for
running it (State, Type, Instance). Omitted conditions are ignored.

The Exec value consists of a golang templated command to be executed
based on the conditions listed above.

It has access to two keys, '.Event', and '.Cxt'. .Event contains the
'.Instance', '.State' and '.Type' of the event which triggered this
Exec, while '.Cxt' refers to the arbitrary watch.context passed in.
This commit is contained in:
Paul Stemmet 2022-12-09 12:12:47 +00:00
parent 6ab808c6f5
commit c1afc0d510
Signed by: Paul Stemmet
GPG Key ID: EDEA539F594E7E75
1 changed files with 175 additions and 0 deletions

175
config/config.go Normal file
View File

@ -0,0 +1,175 @@
/*
* 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}}
}
}