feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
924f111e48
commit
f8e0520817
197 files changed
+18655
-1416
No files matched your search
+81
-20
@@ -6,10 +6,14 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/agentserver"
|
||||
"evobgp/internal/agentwatch"
|
||||
"evobgp/internal/birdfmt"
|
||||
)
|
||||
|
||||
@@ -18,19 +22,22 @@ func main() {
|
||||
birdc := flag.String("birdc", "", "path to birdc binary (default: birdc from PATH)")
|
||||
socket := flag.String("socket", "", "optional birdc control socket (-s)")
|
||||
timeout := flag.Duration("timeout", 30*time.Second, "timeout for bird/birdc")
|
||||
watchEvery := flag.Duration("watch-interval", 30*time.Second, "for watch: interval between birdc configure")
|
||||
watchEvery := flag.Duration("watch-interval", 30*time.Second, "for watch: interval between configuration checks")
|
||||
listen := flag.String("listen", "", "for serve: listen address (default :8443 or EVOBGP_AGENT_LISTEN)")
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [flags] <command>\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Commands:\n")
|
||||
fmt.Fprintf(os.Stderr, " parse-check <path/to/bird.conf> run bird -c <path> -p (syntax check)\n")
|
||||
fmt.Fprintf(os.Stderr, " configure run birdc configure (reload running BIRD)\n")
|
||||
fmt.Fprintf(os.Stderr, " watch periodically run birdc configure (compose sidecar)\n")
|
||||
fmt.Fprintf(os.Stderr, " watch reload changed BIRD configuration and periodically reconcile\n")
|
||||
fmt.Fprintf(os.Stderr, " serve Panel→Node HTTP API (POST /v1/agent/sync)\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
args, err := parseAgentArgs(flag.CommandLine, os.Args[1:])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
if len(args) < 1 {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
@@ -47,10 +54,12 @@ func main() {
|
||||
switch args[0] {
|
||||
case "serve":
|
||||
runServe(*listen, *timeout)
|
||||
case "parse-check", "configure", "watch":
|
||||
case "watch":
|
||||
runWatch(ctl, *watchEvery, *timeout)
|
||||
case "parse-check", "configure":
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
defer cancel()
|
||||
runBirdCommand(ctx, args, ctl, *watchEvery, *timeout)
|
||||
runBirdCommand(ctx, args, ctl)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
|
||||
flag.Usage()
|
||||
@@ -58,7 +67,47 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func runBirdCommand(ctx context.Context, args []string, ctl *birdfmt.BirdCtl, watchEvery, timeout time.Duration) {
|
||||
// parseAgentArgs accepts flags before or immediately after the subcommand.
|
||||
// A second parse preserves previously supplied flag values and rejects unknown
|
||||
// flags instead of silently passing them to watch or serve as positional args.
|
||||
func parseAgentArgs(fs *flag.FlagSet, argv []string) ([]string, error) {
|
||||
if err := fs.Parse(argv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args := fs.Args()
|
||||
if len(args) == 0 {
|
||||
return args, nil
|
||||
}
|
||||
command := args[0]
|
||||
globalStop := false
|
||||
for i, consumed := 0, len(argv)-len(args); i < consumed; i++ {
|
||||
if argv[i] == "--" {
|
||||
globalStop = true
|
||||
break
|
||||
}
|
||||
name, _, hasValue := strings.Cut(strings.TrimLeft(argv[i], "-"), "=")
|
||||
if !hasValue {
|
||||
if f := fs.Lookup(name); f != nil {
|
||||
boolean, ok := f.Value.(interface{ IsBoolFlag() bool })
|
||||
if !ok || !boolean.IsBoolFlag() {
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !globalStop {
|
||||
if err := fs.Parse(args[1:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append([]string{command}, fs.Args()...)
|
||||
}
|
||||
if (command == "watch" || command == "serve" || command == "configure") && len(args) != 1 {
|
||||
return nil, fmt.Errorf("%s takes no positional arguments", command)
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func runBirdCommand(ctx context.Context, args []string, ctl *birdfmt.BirdCtl) {
|
||||
switch args[0] {
|
||||
case "parse-check":
|
||||
if len(args) != 2 {
|
||||
@@ -78,21 +127,33 @@ func runBirdCommand(ctx context.Context, args []string, ctl *birdfmt.BirdCtl, wa
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "watch":
|
||||
if watchEvery <= 0 {
|
||||
fmt.Fprintln(os.Stderr, "watch-interval must be > 0")
|
||||
}
|
||||
}
|
||||
|
||||
func runWatch(ctl *birdfmt.BirdCtl, every, timeout time.Duration) {
|
||||
if every <= 0 || timeout <= 0 {
|
||||
fmt.Fprintln(os.Stderr, "watch-interval and timeout must be > 0")
|
||||
os.Exit(2)
|
||||
}
|
||||
reconcile := 5 * time.Minute
|
||||
if raw := os.Getenv("EVOBGP_AGENT_WATCH_RECONCILE_INTERVAL"); raw != "" {
|
||||
var err error
|
||||
reconcile, err = time.ParseDuration(raw)
|
||||
if err != nil || reconcile <= 0 {
|
||||
fmt.Fprintln(os.Stderr, "EVOBGP_AGENT_WATCH_RECONCILE_INTERVAL must be a positive duration")
|
||||
os.Exit(2)
|
||||
}
|
||||
log.Printf("evobgp-agent watch: birdc configure every %s (socket=%q)", watchEvery, ctl.Socket)
|
||||
for {
|
||||
cctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
err := ctl.Configure(cctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Printf("evobgp-agent watch: configure: %v", err)
|
||||
}
|
||||
time.Sleep(watchEvery)
|
||||
}
|
||||
}
|
||||
active := os.Getenv("EVOBGP_BIRD_ACTIVE_DIR")
|
||||
if active == "" {
|
||||
active = "/etc/bird"
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
log.Printf("evobgp-agent watch: checking %s every %s, forced reconciliation every %s (socket=%q)", active, every, reconcile, ctl.Socket)
|
||||
if err := agentwatch.Run(ctx, ctl, agentwatch.Config{ActiveDir: active, Every: every, ReconcileEvery: reconcile, CommandTimeout: timeout}); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAgentFlagsBeforeAndAfterCommand(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
socket, listen string
|
||||
every time.Duration
|
||||
}{
|
||||
{"legacy flags first", []string{"-socket=/custom/bird.ctl", "-watch-interval=45s", "watch"}, []string{"watch"}, "/custom/bird.ctl", "", 45 * time.Second},
|
||||
{"compose command first", []string{"watch", "-socket=/run/bird/bird.ctl", "-watch-interval=30s"}, []string{"watch"}, "/run/bird/bird.ctl", "", 30 * time.Second},
|
||||
{"serve", []string{"serve", "-listen=127.0.0.1:9999"}, []string{"serve"}, "", "127.0.0.1:9999", time.Minute},
|
||||
{"mixed", []string{"-socket=/first", "watch", "-watch-interval=5s"}, []string{"watch"}, "/first", "", 5 * time.Second},
|
||||
{"parse check", []string{"parse-check", "bird.conf"}, []string{"parse-check", "bird.conf"}, "", "", time.Minute},
|
||||
{"global delimiter", []string{"--", "parse-check", "-bird.conf"}, []string{"parse-check", "-bird.conf"}, "", "", time.Minute},
|
||||
{"flags then delimiter", []string{"-socket=/first", "--", "parse-check", "-bird.conf"}, []string{"parse-check", "-bird.conf"}, "/first", "", time.Minute},
|
||||
{"delimiter as value", []string{"-socket", "--", "watch", "-watch-interval=5s"}, []string{"watch"}, "--", "", 5 * time.Second},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fs := flag.NewFlagSet("agent", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
socket := fs.String("socket", "", "")
|
||||
listen := fs.String("listen", "", "")
|
||||
every := fs.Duration("watch-interval", time.Minute, "")
|
||||
args, err := parseAgentArgs(fs, tc.args)
|
||||
if err != nil || !reflect.DeepEqual(args, tc.want) || *socket != tc.socket || *listen != tc.listen || *every != tc.every {
|
||||
t.Fatalf("args=%v socket=%s listen=%s every=%s err=%v", args, *socket, *listen, *every, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestAgentRejectsIgnoredFlagsAndUnexpectedArguments(t *testing.T) {
|
||||
for _, argv := range [][]string{{"watch", "-unknown=1"}, {"serve", "-unknown=1"}, {"watch", "unused"}, {"serve", "unused"}, {"configure", "unused"}} {
|
||||
fs := flag.NewFlagSet("agent", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
if _, err := parseAgentArgs(fs, argv); err == nil {
|
||||
t.Fatalf("ignored argv: %v", argv)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user