v1.0.0 — first release

Bump VERSION to 1.0.0 and cut the first tagged release of vpp-maglev.

Also in this commit:

- maglevc: MAGLEV_SERVER env var as an alternative to the --server
  flag, matching the MAGLEV_CONFIG / MAGLEV_GRPC_ADDR convention on
  the other binaries. The flag takes precedence when both are set.
- Rename cmd/maglevd -> cmd/server and cmd/maglevc -> cmd/client so
  the source directory names are decoupled from binary names (the
  frontend and tester commands already followed this convention).
  Build outputs and the Debian packages are unchanged.
This commit is contained in:
2026-04-15 15:23:46 +02:00
parent 177d81cca1
commit bc6ccaa844
15 changed files with 33 additions and 17 deletions

192
cmd/server/main.go Normal file
View File

@@ -0,0 +1,192 @@
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package main
import (
"context"
"flag"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"syscall"
grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
"git.ipng.ch/ipng/vpp-maglev/internal/checker"
"git.ipng.ch/ipng/vpp-maglev/internal/config"
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
"git.ipng.ch/ipng/vpp-maglev/internal/metrics"
"git.ipng.ch/ipng/vpp-maglev/internal/vpp"
)
func main() {
if err := run(); err != nil {
slog.Error("startup-fatal", "err", err)
os.Exit(1)
}
}
func run() error {
// ---- flags / env --------------------------------------------------------
printVersion := flag.Bool("version", false, "print version and exit")
checkOnly := flag.Bool("check", false, "check config file and exit (0=ok, 1=parse error, 2=semantic error)")
enableReflection := flag.Bool("reflection", true, "enable gRPC server reflection (for grpcurl)")
configPath := stringFlag("config", "/etc/vpp-maglev/maglev.yaml", "MAGLEV_CONFIG", "path to maglev.yaml")
grpcAddr := stringFlag("grpc-addr", ":9090", "MAGLEV_GRPC_ADDR", "gRPC listen address")
metricsAddr := stringFlag("metrics-addr", ":9091", "MAGLEV_METRICS_ADDR", "Prometheus /metrics listen address (empty to disable)")
vppAPIAddr := stringFlag("vpp-api-addr", "/run/vpp/api.sock", "MAGLEV_VPP_API_ADDR", "VPP binary API socket path (empty to disable)")
vppStatsAddr := stringFlag("vpp-stats-addr", "/run/vpp/stats.sock", "MAGLEV_VPP_STATS_ADDR", "VPP stats socket path")
logLevel := stringFlag("log-level", "info", "MAGLEV_LOG_LEVEL", "log level (debug|info|warn|error)")
flag.Parse()
if *printVersion {
fmt.Printf("maglevd %s (commit %s, built %s)\n",
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
return nil
}
if *checkOnly {
_, result := config.Check(*configPath)
if result.OK() {
fmt.Printf("config ok: %s\n", *configPath)
return nil
}
if result.ParseError != "" {
fmt.Fprintf(os.Stderr, "parse error: %s\n", result.ParseError)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "semantic error: %s\n", result.SemanticError)
os.Exit(2)
}
// ---- logging ------------------------------------------------------------
var level slog.Level
if err := level.UnmarshalText([]byte(*logLevel)); err != nil {
return fmt.Errorf("invalid log level %q: %w", *logLevel, err)
}
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})
logBroadcaster := grpcapi.NewLogBroadcaster(jsonHandler)
slog.SetDefault(slog.New(logBroadcaster))
slog.Info("starting", "version", buildinfo.Version(), "commit", buildinfo.Commit(), "date", buildinfo.Date())
// ---- config -------------------------------------------------------------
cfg, err := config.Load(*configPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
slog.Info("config-loaded", "path", *configPath, "frontends", len(cfg.Frontends))
// ---- checker ------------------------------------------------------------
chkr := checker.New(cfg)
ctx, rootCancel := context.WithCancel(context.Background())
defer rootCancel()
go func() {
if err := chkr.Run(ctx); err != nil {
slog.Error("checker-exited", "err", err)
}
}()
// ---- VPP connection -----------------------------------------------------
var vppClient *vpp.Client
if *vppAPIAddr != "" {
vppClient = vpp.New(*vppAPIAddr, *vppStatsAddr)
vppClient.SetStateSource(chkr)
go vppClient.Run(ctx)
// The reconciler subscribes to checker events and pushes per-VIP
// syncs into VPP on every backend state transition. This is the
// single place where transitions translate into dataplane changes.
reconciler := vpp.NewReconciler(vppClient, chkr, chkr)
go reconciler.Run(ctx)
}
// ---- gRPC server --------------------------------------------------------
// Server-side metrics for every RPC: call counters, in-flight gauges,
// and handler-latency histograms, labelled by method and gRPC code.
// Provided by go-grpc-middleware's prometheus adapter; the metric
// families are emitted on the /metrics endpoint alongside ours.
grpcMetrics := grpcprom.NewServerMetrics(
grpcprom.WithServerHandlingTimeHistogram(),
)
lis, err := net.Listen("tcp", *grpcAddr)
if err != nil {
return fmt.Errorf("listen %s: %w", *grpcAddr, err)
}
srv := grpc.NewServer(
grpc.UnaryInterceptor(grpcMetrics.UnaryServerInterceptor()),
grpc.StreamInterceptor(grpcMetrics.StreamServerInterceptor()),
)
maglevServer := grpcapi.NewServer(ctx, chkr, logBroadcaster, *configPath, vppClient)
grpcapi.RegisterMaglevServer(srv, maglevServer)
if *enableReflection {
reflection.Register(srv)
}
// Pre-register every method with 0 so the metric shows up on first
// scrape even before any RPC has been received.
grpcMetrics.InitializeMetrics(srv)
slog.Info("grpc-listening", "addr", *grpcAddr, "reflection", *enableReflection)
go func() {
if err := srv.Serve(lis); err != nil {
slog.Error("grpc-serve-error", "err", err)
}
}()
// ---- Prometheus metrics -------------------------------------------------
if *metricsAddr != "" {
reg := prometheus.DefaultRegisterer
// vppClient may be nil when VPP integration is disabled; the
// collector handles that by skipping the vpp_* gauges.
var vppSrc metrics.VPPSource
if vppClient != nil {
vppSrc = vppClient
}
metrics.Register(reg, chkr, vppSrc)
reg.MustRegister(grpcMetrics)
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
slog.Info("metrics-listening", "addr", *metricsAddr)
go func() {
if err := http.ListenAndServe(*metricsAddr, mux); err != nil && err != http.ErrServerClosed {
slog.Error("metrics-serve-error", "err", err)
}
}()
}
// ---- signal handling ----------------------------------------------------
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
for sig := range sigCh {
switch sig {
case syscall.SIGHUP:
maglevServer.TriggerReload()
case syscall.SIGTERM, syscall.SIGINT:
slog.Info("shutdown", "signal", sig)
rootCancel()
srv.GracefulStop()
return nil
}
}
return nil
}
// stringFlag declares a flag that falls back to an environment variable.
func stringFlag(name, defaultVal, envKey, usage string) *string {
val := defaultVal
if v := os.Getenv(envKey); v != "" {
val = v
}
return flag.String(name, val, fmt.Sprintf("%s (env: %s)", usage, envKey))
}