Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
110 lines
3.1 KiB
Go
110 lines
3.1 KiB
Go
// Package agentwatch reloads BIRD when its active configuration tree changes.
|
|
package agentwatch
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// Control is the subset of BIRD operations required by the watch loop.
|
|
type Control interface {
|
|
ParseCheck(context.Context, string) error
|
|
Configure(context.Context) error
|
|
}
|
|
|
|
// Config sets bounded polling and reconciliation behavior.
|
|
type Config struct {
|
|
ActiveDir string
|
|
Every time.Duration
|
|
ReconcileEvery time.Duration
|
|
CommandTimeout time.Duration
|
|
Logf func(string, ...any)
|
|
}
|
|
|
|
type watcher struct {
|
|
ctl Control
|
|
cfg Config
|
|
hash [sha256.Size]byte
|
|
haveHash bool
|
|
lastConfigured time.Time
|
|
}
|
|
|
|
// Run starts with one check, then polls until ctx is cancelled. A successful
|
|
// birdc exit advances the remembered hash; it is not proof of BIRD ACK.
|
|
func Run(ctx context.Context, ctl Control, cfg Config) error {
|
|
if ctl == nil || cfg.Every <= 0 || cfg.ReconcileEvery <= 0 || cfg.CommandTimeout <= 0 {
|
|
return fmt.Errorf("agentwatch: control and positive intervals/timeouts are required")
|
|
}
|
|
if cfg.ActiveDir == "" {
|
|
cfg.ActiveDir = "/etc/bird"
|
|
}
|
|
if cfg.Logf == nil {
|
|
cfg.Logf = log.Printf
|
|
}
|
|
w := &watcher{ctl: ctl, cfg: cfg}
|
|
if err := w.step(ctx, time.Now()); err != nil && ctx.Err() == nil {
|
|
cfg.Logf("evobgp-agent watch: %v", err)
|
|
}
|
|
ticker := time.NewTicker(cfg.Every)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case now := <-ticker.C:
|
|
if err := w.step(ctx, now); err != nil && ctx.Err() == nil {
|
|
cfg.Logf("evobgp-agent watch: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *watcher) step(ctx context.Context, now time.Time) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
digest, scanErr := hashConfigTree(ctx, w.cfg.ActiveDir)
|
|
if scanErr != nil {
|
|
if errors.Is(scanErr, errMainNotReady) {
|
|
return fmt.Errorf("configuration not ready: %w", scanErr)
|
|
}
|
|
// Unsupported/unhashable trees retain the old every-tick configure cadence.
|
|
w.cfg.Logf("evobgp-agent watch: cannot hash complete config tree (%v); using every-tick parse/configure", scanErr)
|
|
} else if w.haveHash && digest == w.hash && now.Sub(w.lastConfigured) < w.cfg.ReconcileEvery {
|
|
return nil
|
|
}
|
|
main := filepath.Join(w.cfg.ActiveDir, "bird.conf")
|
|
if err := w.command(ctx, func(cctx context.Context) error { return w.ctl.ParseCheck(cctx, main) }); err != nil {
|
|
return fmt.Errorf("parse check: %w", err)
|
|
}
|
|
if scanErr == nil {
|
|
stable, err := hashConfigTree(ctx, w.cfg.ActiveDir)
|
|
if err != nil {
|
|
return fmt.Errorf("configuration changed during parse check: %w", err)
|
|
}
|
|
if stable != digest {
|
|
return fmt.Errorf("configuration changed during parse check; retrying")
|
|
}
|
|
}
|
|
if err := w.command(ctx, w.ctl.Configure); err != nil {
|
|
return fmt.Errorf("configure: %w", err)
|
|
}
|
|
w.haveHash = scanErr == nil
|
|
if w.haveHash {
|
|
w.hash = digest
|
|
}
|
|
w.lastConfigured = now
|
|
return nil
|
|
}
|
|
|
|
func (w *watcher) command(parent context.Context, run func(context.Context) error) error {
|
|
ctx, cancel := context.WithTimeout(parent, w.cfg.CommandTimeout)
|
|
defer cancel()
|
|
return run(ctx)
|
|
}
|