Reload guard.yaml without losing the bounties
systemctl reload guard, guard reload or SIGHUP reads the configuration again: a watch keeps its bounties and counts under the same name and takes its new settings at once; its source starts again only if it changed; watches gone stop, new ones start; a file with a mistake is refused. The Housecarl connection, state, socket and log wait for a restart, and guard reload says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c655abae2
commit
ecf4129877
11 files changed
+607
-93
No files matched your search
@@ -0,0 +1,185 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"guard/internal/bounty"
|
||||
"guard/internal/config"
|
||||
"guard/internal/match"
|
||||
)
|
||||
|
||||
// Reloaded is what a reload changed.
|
||||
type Reloaded struct {
|
||||
Changes []string `json:"changes"`
|
||||
// Restart are the changed settings a reload can't apply: they wait
|
||||
// for a restart.
|
||||
Restart []string `json:"restart,omitempty"`
|
||||
}
|
||||
|
||||
// ReloadFile reads the configuration again (Load) and reloads it.
|
||||
func (d *Daemon) ReloadFile() (*Reloaded, error) {
|
||||
if d.Load == nil {
|
||||
return nil, errors.New("this Guard can't read its configuration again")
|
||||
}
|
||||
cfg, err := d.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Reload(cfg)
|
||||
}
|
||||
|
||||
// Reload puts a new configuration to work without losing what Guard
|
||||
// counted: a watch keeps its bounties and counts under the same name,
|
||||
// and its source runs on unless it changed. Watches gone stop, new ones
|
||||
// start. The housecarl connection, state, socket and log wait for a
|
||||
// restart. A configuration with no watch at work is refused, and the
|
||||
// old one stays.
|
||||
func (d *Daemon) Reload(cfg *config.Config) (*Reloaded, error) {
|
||||
if d.ctx == nil {
|
||||
return nil, errors.New("Guard isn't running")
|
||||
}
|
||||
type next struct {
|
||||
cfg *config.Watch
|
||||
m *match.Matcher
|
||||
}
|
||||
var want []next
|
||||
for _, w := range cfg.Watch {
|
||||
if w.Disabled {
|
||||
continue
|
||||
}
|
||||
m, err := match.Compile(w.Match, w.Ignore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("watch %s: %v", w.Name, err)
|
||||
}
|
||||
want = append(want, next{w, m})
|
||||
}
|
||||
if len(want) == 0 {
|
||||
return nil, errors.New("every watch is disabled: nothing to do (the configuration stays as it was)")
|
||||
}
|
||||
|
||||
res := &Reloaded{}
|
||||
d.mu.Lock()
|
||||
old := d.cfg
|
||||
oh, nh := old.Housecarl, cfg.Housecarl
|
||||
oh.List, nh.List = "", "" // the default list only fills in watches
|
||||
if oh != nh {
|
||||
res.Restart = append(res.Restart, "housecarl")
|
||||
}
|
||||
if old.State != cfg.State {
|
||||
res.Restart = append(res.Restart, "state")
|
||||
}
|
||||
if old.Socket != cfg.Socket {
|
||||
res.Restart = append(res.Restart, "socket")
|
||||
}
|
||||
if old.Log != cfg.Log {
|
||||
res.Restart = append(res.Restart, "log")
|
||||
}
|
||||
cfg.Housecarl.URL, cfg.Housecarl.Socket, cfg.Housecarl.TokenFile, cfg.Housecarl.Token = old.Housecarl.URL, old.Housecarl.Socket, old.Housecarl.TokenFile, old.Housecarl.Token
|
||||
cfg.Housecarl.CACert, cfg.Housecarl.Insecure = old.Housecarl.CACert, old.Housecarl.Insecure
|
||||
cfg.State, cfg.Socket, cfg.Log = old.State, old.Socket, old.Log
|
||||
|
||||
if !slices.Equal(old.NeverBan, cfg.NeverBan) {
|
||||
n := NewNeverBan(cfg.NeverBan, d.log)
|
||||
n.Lookup = d.never.Lookup
|
||||
d.never.mu.RLock()
|
||||
for _, h := range n.names {
|
||||
if a, ok := d.never.resolved[h]; ok {
|
||||
n.resolved[h] = a
|
||||
}
|
||||
}
|
||||
d.never.mu.RUnlock()
|
||||
d.neverStop()
|
||||
d.never = n
|
||||
d.startNever()
|
||||
res.Changes = append(res.Changes, "never_ban")
|
||||
}
|
||||
if old.Defaults.Repeat != cfg.Defaults.Repeat {
|
||||
res.Changes = append(res.Changes, "defaults.repeat: for the next bans")
|
||||
}
|
||||
|
||||
byName := map[string]*Watch{}
|
||||
for _, w := range d.watches {
|
||||
byName[strings.ToLower(w.Cfg.Name)] = w
|
||||
}
|
||||
var watches []*Watch
|
||||
var stopped []chan struct{}
|
||||
for _, n := range want {
|
||||
w := byName[strings.ToLower(n.cfg.Name)]
|
||||
delete(byName, strings.ToLower(n.cfg.Name))
|
||||
if w == nil {
|
||||
w = &Watch{Cfg: n.cfg, Matcher: n.m, Tracker: bounty.New(n.cfg.Hits, n.cfg.Within.D())}
|
||||
d.start(w)
|
||||
watches = append(watches, w)
|
||||
res.Changes = append(res.Changes, "watch "+n.cfg.Name+": added")
|
||||
continue
|
||||
}
|
||||
watches = append(watches, w)
|
||||
oc, _ := w.setup()
|
||||
if reflect.DeepEqual(oc, n.cfg) {
|
||||
continue
|
||||
}
|
||||
var what []string
|
||||
if !slices.Equal(oc.Match, n.cfg.Match) || !slices.Equal(oc.Ignore, n.cfg.Ignore) {
|
||||
what = append(what, "patterns")
|
||||
}
|
||||
if oc.Hits != n.cfg.Hits || oc.Within != n.cfg.Within {
|
||||
what = append(what, fmt.Sprintf("bounty %d in %s", n.cfg.Hits, n.cfg.Within))
|
||||
}
|
||||
if !reflect.DeepEqual(oc.Ban, n.cfg.Ban) {
|
||||
what = append(what, "ban")
|
||||
}
|
||||
w.Tracker.Set(n.cfg.Hits, n.cfg.Within.D())
|
||||
w.mu.Lock()
|
||||
w.Cfg, w.Matcher = n.cfg, n.m
|
||||
w.mu.Unlock()
|
||||
if sourceKey(oc) != sourceKey(n.cfg) {
|
||||
w.stop()
|
||||
stopped = append(stopped, w.done)
|
||||
w.Source = d.newSource(n.cfg)
|
||||
d.start(w)
|
||||
what = append(what, "source (started again)")
|
||||
}
|
||||
if len(what) == 0 {
|
||||
what = append(what, "changed")
|
||||
}
|
||||
res.Changes = append(res.Changes, "watch "+n.cfg.Name+": "+strings.Join(what, ", "))
|
||||
}
|
||||
for _, w := range d.watches {
|
||||
if byName[strings.ToLower(w.Cfg.Name)] == w {
|
||||
w.stop()
|
||||
stopped = append(stopped, w.done)
|
||||
res.Changes = append(res.Changes, "watch "+w.Cfg.Name+": removed")
|
||||
}
|
||||
}
|
||||
d.watches = watches
|
||||
d.cfg = cfg
|
||||
d.mu.Unlock()
|
||||
|
||||
// A stopped watch may be handling a line, which needs d.mu.
|
||||
for _, done := range stopped {
|
||||
<-done
|
||||
}
|
||||
d.loadGroups(d.ctx)
|
||||
if len(res.Changes) == 0 {
|
||||
res.Changes = []string{}
|
||||
}
|
||||
d.log.Info("reloaded", "changes", strings.Join(res.Changes, "; "))
|
||||
if len(res.Restart) > 0 {
|
||||
d.log.Warn("changed settings that wait for a restart", "settings", strings.Join(res.Restart, ", "))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// sourceKey is what a watch's source is made from: when it changes, the
|
||||
// source starts again.
|
||||
func sourceKey(w *config.Watch) string {
|
||||
j := ""
|
||||
if w.Journal != nil {
|
||||
j = fmt.Sprint(w.Journal.Unit, w.Journal.Identifier)
|
||||
}
|
||||
return fmt.Sprint(w.Kind(), "|", w.File, "|", j, "|", w.Source, "|", w.Protocol, "|", w.Shadow)
|
||||
}
|
||||
Reference in new issue
Block a user