kdnotify/config/config.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}}
}
}