Paul Stemmet
c1afc0d510
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.
176 lines
4.1 KiB
Go
176 lines
4.1 KiB
Go
/*
|
|
* 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}}
|
|
}
|
|
}
|