Pools, CLI, versioning, Debian packaging, HTTPS fix
- Replaced flat `backends: [...]` list on frontends with an ordered `pools:`
list; each pool has a name and a map of backends with per-pool weights (0–100,
default 100). Pools express priority: first pool with a healthy backend wins.
- Removed global backend weight (was on the backend, now lives in the pool).
- Config validation enforces non-empty pools, non-empty pool names, weight
range, and consistent address families across all pools of a frontend.
- Added `PoolBackendInfo { name, weight }` and changed `PoolInfo.backends` from
`repeated string` to `repeated PoolBackendInfo` so weights are visible over
the API.
- Full interactive shell with readline, tab completion, and `?` inline help.
- Command tree parser (Walk) handles fixed keywords and dynamic slot nodes;
prefix matching with exact-match priority.
- Commands: `show version/frontends/frontend/backends/backend/healthchecks/
healthcheck`, `set backend <name> pause|resume`, `quit`/`exit`.
- `show frontend` output is hierarchical (pools → backends) with per-backend
weights and `[disabled]` notation; pool section uses fixed-width formatting
so ANSI color codes don't corrupt tabwriter alignment.
- `-color` flag (default true) wraps static field labels in dark-blue ANSI;
works correctly with tabwriter because all labels carry identical-length
escape sequences.
- `cmd/version.go` package holds `version`, `commit`, `date` vars set at build
time via `-ldflags -X`.
- `make build` / `make build-amd64` / `make build-arm64` all inject
`VERSION=0.1.1`, `COMMIT_HASH` (from `git rev-parse --short HEAD`), and
`DATE` (UTC ISO-8601).
- `maglevc` prints version on interactive startup and exposes `show version`.
- `maglevd` logs version/commit/date at startup; `-version` flag prints and exits.
- `doHTTPProbe` was building a `https://` target URL even though TLS was already
applied to the connection inside `inNetns`. `http.Transport` then wrapped the
connection in a second TLS layer, producing "http: server gave HTTP response
to HTTPS client". Fixed by always using `http://` in the target URL.
- Added `TestHTTPSProbe` using `httptest.NewTLSServer` to cover the full path.
- New `docs/user-guide.md`: maglevd flags/signals, maglevc commands, shell
completion, and command-tree parser walkthrough.
- New `docs/healthchecks.md`: state machine, rise/fall model, probe intervals,
all transition events with log examples.
- Updated `docs/config-guide.md`: pools design, removed global weight from
backends, updated all examples.
- Updated `README.md`: packaging table, build paths, corrected binary locations
(`/usr/sbin/maglevd`), config filename (`.yaml`).
- `debian/` directory contains `control.in`, `maglevd.service`, `default.maglev`,
`maglev.yaml` (example config), `conffiles`, `postinst`, `prerm`.
- `debian/build-deb.sh` stages a package tree and calls `dpkg-deb`; emits
`build/vpp-maglev_<version>~<commit>_<arch>.deb`.
- Cross-compiles for amd64 and arm64 in one `make pkg-deb` invocation.
- `maglevd` installed to `/usr/sbin/`, `maglevc` to `/usr/bin/`.
- Service reads `MAGLEV_CONFIG` from `/etc/default/maglev`
(default: `/etc/maglev/maglev.yaml`).
- Man pages `maglevd(8)` and `maglevc(1)` live in `docs/` and are gzip'd into
the package.
- All build output goes to `build/<arch>/`; `build/` is gitignored.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
bin/
|
build/
|
||||||
/*.yaml
|
/*.yaml
|
||||||
docs/implementation/
|
docs/implementation/
|
||||||
|
|||||||
31
Makefile
31
Makefile
@@ -4,13 +4,36 @@ PROTO_DIR := proto
|
|||||||
PROTO_FILE := $(PROTO_DIR)/maglev.proto
|
PROTO_FILE := $(PROTO_DIR)/maglev.proto
|
||||||
GEN_FILES := internal/grpcapi/maglev.pb.go internal/grpcapi/maglev_grpc.pb.go
|
GEN_FILES := internal/grpcapi/maglev.pb.go internal/grpcapi/maglev_grpc.pb.go
|
||||||
|
|
||||||
.PHONY: all build test proto lint clean
|
NATIVE_ARCH := $(shell go env GOARCH)
|
||||||
|
VERSION := 0.1.1
|
||||||
|
COMMIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \
|
||||||
|
-X '$(MODULE)/cmd.commit=$(COMMIT_HASH)' \
|
||||||
|
-X '$(MODULE)/cmd.date=$(DATE)'
|
||||||
|
|
||||||
|
.PHONY: all build build-amd64 build-arm64 test proto lint pkg-deb clean
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
build: $(GEN_FILES)
|
build: $(GEN_FILES)
|
||||||
go build -o bin/maglevd ./cmd/maglevd/
|
mkdir -p build/$(NATIVE_ARCH)
|
||||||
go build -o bin/maglevc ./cmd/maglevc/
|
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd ./cmd/maglevd/
|
||||||
|
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevc ./cmd/maglevc/
|
||||||
|
|
||||||
|
build-amd64: $(GEN_FILES)
|
||||||
|
mkdir -p build/amd64
|
||||||
|
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd ./cmd/maglevd/
|
||||||
|
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevc ./cmd/maglevc/
|
||||||
|
|
||||||
|
build-arm64: $(GEN_FILES)
|
||||||
|
mkdir -p build/arm64
|
||||||
|
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevd ./cmd/maglevd/
|
||||||
|
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevc ./cmd/maglevc/
|
||||||
|
|
||||||
|
pkg-deb: build-amd64 build-arm64
|
||||||
|
debian/build-deb.sh amd64 $(VERSION) $(COMMIT_HASH)
|
||||||
|
debian/build-deb.sh arm64 $(VERSION) $(COMMIT_HASH)
|
||||||
|
|
||||||
test: $(GEN_FILES)
|
test: $(GEN_FILES)
|
||||||
go test ./...
|
go test ./...
|
||||||
@@ -27,5 +50,5 @@ lint:
|
|||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(addprefix bin/,$(BINARIES))
|
rm -rf build/
|
||||||
rm -f $(GEN_FILES)
|
rm -f $(GEN_FILES)
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -5,29 +5,58 @@ Health checker and gRPC control plane for VPP Maglev load balancing.
|
|||||||
## Build
|
## Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make # builds bin/maglevd
|
make # builds build/<arch>/maglevd and build/<arch>/maglevc
|
||||||
make test # runs all tests
|
make test # runs all tests
|
||||||
make proto # regenerates gRPC stubs from proto/maglev.proto
|
make proto # regenerates gRPC stubs from proto/maglev.proto
|
||||||
|
make lint # runs golangci-lint
|
||||||
```
|
```
|
||||||
|
|
||||||
Requires Go 1.25+ and (for `make proto`) `protoc` with `protoc-gen-go` and `protoc-gen-go-grpc`.
|
Requires Go 1.25+ and (for `make proto`) `protoc` with `protoc-gen-go` and
|
||||||
|
`protoc-gen-go-grpc`.
|
||||||
|
|
||||||
|
## Debian package
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make pkg-deb
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `vpp-maglev_<version>_amd64.deb` and `vpp-maglev_<version>_arm64.deb`
|
||||||
|
in the project root by cross-compiling with `GOOS=linux GOARCH=<arch>`.
|
||||||
|
Requires `dpkg-deb` (available on any Debian/Ubuntu host).
|
||||||
|
|
||||||
|
The package installs:
|
||||||
|
|
||||||
|
| Path | Content |
|
||||||
|
|---|---|
|
||||||
|
| `/usr/sbin/maglevd` | Health-checker daemon |
|
||||||
|
| `/usr/bin/maglevc` | Interactive CLI client |
|
||||||
|
| `/lib/systemd/system/maglevd.service` | systemd unit |
|
||||||
|
| `/etc/default/maglev` | Environment file for the unit (conffile) |
|
||||||
|
| `/etc/maglev/maglev.yaml` | Example configuration file (conffile) |
|
||||||
|
| `/usr/share/man/man8/maglevd.8.gz` | Man page |
|
||||||
|
| `/usr/share/man/man1/maglevc.1.gz` | Man page |
|
||||||
|
|
||||||
|
After installing, the unit is enabled but not started automatically:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# edit /etc/maglev/maglev.yaml, then:
|
||||||
|
systemctl start maglevd
|
||||||
|
```
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
maglevd --config /etc/maglev/maglev.yaml --grpc-addr :9090
|
maglevd --config /etc/maglev/maglev.yaml --grpc-addr :9090
|
||||||
|
maglevd --version # print version and exit
|
||||||
|
|
||||||
|
maglevc --server localhost:9090 # interactive shell
|
||||||
|
maglevc show backend nginx0-ams # one-shot
|
||||||
|
maglevc -color=false show backends # one-shot, no ANSI color
|
||||||
|
maglevc set backend nginx0-ams pause
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Env | Default |
|
Send `SIGHUP` to `maglevd` to reload config without restarting.
|
||||||
|---|---|---|
|
`maglevd` requires `CAP_NET_RAW` for ICMP health checks.
|
||||||
| `--config` | `MAGLEV_CONFIG` | `/etc/maglev/frontend.yaml` |
|
|
||||||
| `--grpc-addr` | `MAGLEV_GRPC_ADDR` | `:9090` |
|
|
||||||
| `--log-level` | `MAGLEV_LOG_LEVEL` | `info` |
|
|
||||||
|
|
||||||
Send `SIGHUP` to reload the config without restarting. Backends whose health-check config is
|
|
||||||
unchanged continue probing uninterrupted.
|
|
||||||
|
|
||||||
`maglevd` requires `CAP_NET_RAW` to open raw ICMP sockets.
|
|
||||||
|
|
||||||
## Minimal config
|
## Minimal config
|
||||||
|
|
||||||
@@ -51,10 +80,16 @@ maglev:
|
|||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
port: 80
|
port: 80
|
||||||
backends: [web0, web1]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
web0: {}
|
||||||
|
web1: {}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See [docs/user-guide.md](docs/user-guide.md) for flags, signals, and `maglevc` usage.
|
||||||
See [docs/config-guide.md](docs/config-guide.md) for the full configuration reference.
|
See [docs/config-guide.md](docs/config-guide.md) for the full configuration reference.
|
||||||
|
See [docs/healthchecks.md](docs/healthchecks.md) for health state machine details.
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
|||||||
22
cmd/maglevc/color.go
Normal file
22
cmd/maglevc/color.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
const (
|
||||||
|
ansiBlue = "\x1b[34m"
|
||||||
|
ansiReset = "\x1b[0m"
|
||||||
|
)
|
||||||
|
|
||||||
|
// colorEnabled is set by the -color flag in main.
|
||||||
|
var colorEnabled bool
|
||||||
|
|
||||||
|
// label wraps s in dark-blue ANSI when color output is enabled.
|
||||||
|
// Because every label receives the same fixed-length prefix/suffix, tabwriter
|
||||||
|
// alignment is preserved: the extra bytes are equal for all rows so relative
|
||||||
|
// widths remain correct.
|
||||||
|
func label(s string) string {
|
||||||
|
if !colorEnabled {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return ansiBlue + s + ansiReset
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
|
||||||
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ func buildTree() *Node {
|
|||||||
quit := &Node{Word: "quit", Help: "exit the shell", Run: runQuit}
|
quit := &Node{Word: "quit", Help: "exit the shell", Run: runQuit}
|
||||||
exit := &Node{Word: "exit", Help: "exit the shell", Run: runQuit}
|
exit := &Node{Word: "exit", Help: "exit the shell", Run: runQuit}
|
||||||
|
|
||||||
|
// show version
|
||||||
|
showVersion := &Node{Word: "version", Help: "show build version", Run: runShowVersion}
|
||||||
|
|
||||||
// show frontends
|
// show frontends
|
||||||
showFrontends := &Node{Word: "frontends", Help: "list all frontends", Run: runShowFrontends}
|
showFrontends := &Node{Word: "frontends", Help: "list all frontends", Run: runShowFrontends}
|
||||||
// show frontend <name>
|
// show frontend <name>
|
||||||
@@ -70,6 +74,7 @@ func buildTree() *Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show.Children = []*Node{
|
show.Children = []*Node{
|
||||||
|
showVersion,
|
||||||
showFrontends, showFrontend,
|
showFrontends, showFrontend,
|
||||||
showBackends, showBackend,
|
showBackends, showBackend,
|
||||||
showHealthChecks, showHealthCheck,
|
showHealthChecks, showHealthCheck,
|
||||||
@@ -125,6 +130,12 @@ func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient) []string
|
|||||||
|
|
||||||
// ---- run functions ---------------------------------------------------------
|
// ---- run functions ---------------------------------------------------------
|
||||||
|
|
||||||
|
func runShowVersion(_ context.Context, _ grpcapi.MaglevClient, _ []string) error {
|
||||||
|
fmt.Printf("maglevc %s (commit %s, built %s)\n",
|
||||||
|
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func runQuit(_ context.Context, _ grpcapi.MaglevClient, _ []string) error {
|
func runQuit(_ context.Context, _ grpcapi.MaglevClient, _ []string) error {
|
||||||
return errQuit
|
return errQuit
|
||||||
}
|
}
|
||||||
@@ -152,22 +163,53 @@ func runShowFrontend(ctx context.Context, client grpcapi.MaglevClient, args []st
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
fmt.Fprintf(w, "name\t%s\n", info.Name)
|
fmt.Fprintf(w, "%s\t%s\n", label("name"), info.Name)
|
||||||
fmt.Fprintf(w, "address\t%s\n", info.Address)
|
fmt.Fprintf(w, "%s\t%s\n", label("address"), info.Address)
|
||||||
fmt.Fprintf(w, "protocol\t%s\n", info.Protocol)
|
fmt.Fprintf(w, "%s\t%s\n", label("protocol"), info.Protocol)
|
||||||
fmt.Fprintf(w, "port\t%d\n", info.Port)
|
fmt.Fprintf(w, "%s\t%d\n", label("port"), info.Port)
|
||||||
for i, b := range info.BackendNames {
|
|
||||||
if i == 0 {
|
|
||||||
fmt.Fprintf(w, "backends\t%s\n", b)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(w, "\t%s\n", b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if info.Description != "" {
|
if info.Description != "" {
|
||||||
fmt.Fprintf(w, "description\t%s\n", info.Description)
|
fmt.Fprintf(w, "%s\t%s\n", label("description"), info.Description)
|
||||||
}
|
}
|
||||||
return w.Flush()
|
if len(info.Pools) > 0 {
|
||||||
|
fmt.Fprintf(w, "%s\n", label("pools"))
|
||||||
|
}
|
||||||
|
if err := w.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool section uses direct Printf with fixed-width padding so that ANSI
|
||||||
|
// escape codes in labels don't confuse tabwriter's byte-based alignment.
|
||||||
|
// "backends" is always the widest pool label (8 chars); all pool labels
|
||||||
|
// are right-padded to that width, giving a 2+8+2 = 12-char visual indent.
|
||||||
|
const poolLblWidth = len("backends")
|
||||||
|
const poolIndent = " "
|
||||||
|
const poolSep = " "
|
||||||
|
contIndent := strings.Repeat(" ", len(poolIndent)+poolLblWidth+len(poolSep))
|
||||||
|
|
||||||
|
for _, pool := range info.Pools {
|
||||||
|
namePad := strings.Repeat(" ", poolLblWidth-len("name"))
|
||||||
|
fmt.Printf("%s%s%s%s%s\n", poolIndent, label("name"), namePad, poolSep, pool.Name)
|
||||||
|
for i, pb := range pool.Backends {
|
||||||
|
beInfo, beErr := client.GetBackend(ctx, &grpcapi.GetBackendRequest{Name: pb.Name})
|
||||||
|
suffix := ""
|
||||||
|
if beErr == nil && !beInfo.Enabled {
|
||||||
|
suffix = " [disabled]"
|
||||||
|
}
|
||||||
|
weightStr := ""
|
||||||
|
if pb.Weight != 100 {
|
||||||
|
weightStr = fmt.Sprintf(" %s %d", label("weight"), pb.Weight)
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
bePad := strings.Repeat(" ", poolLblWidth-len("backends"))
|
||||||
|
fmt.Printf("%s%s%s%s%s%s%s\n", poolIndent, label("backends"), bePad, poolSep, pb.Name, weightStr, suffix)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s%s%s%s\n", contIndent, pb.Name, weightStr, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runShowBackends(ctx context.Context, client grpcapi.MaglevClient, _ []string) error {
|
func runShowBackends(ctx context.Context, client grpcapi.MaglevClient, _ []string) error {
|
||||||
@@ -194,25 +236,24 @@ func runShowBackend(ctx context.Context, client grpcapi.MaglevClient, args []str
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
fmt.Fprintf(w, "name\t%s\n", info.Name)
|
fmt.Fprintf(w, "%s\t%s\n", label("name"), info.Name)
|
||||||
fmt.Fprintf(w, "address\t%s\n", info.Address)
|
fmt.Fprintf(w, "%s\t%s\n", label("address"), info.Address)
|
||||||
stateDur := ""
|
stateDur := ""
|
||||||
if len(info.Transitions) > 0 {
|
if len(info.Transitions) > 0 {
|
||||||
since := time.Since(time.Unix(0, info.Transitions[0].AtUnixNs))
|
since := time.Since(time.Unix(0, info.Transitions[0].AtUnixNs))
|
||||||
stateDur = " for " + formatDuration(since)
|
stateDur = " for " + formatDuration(since)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "state\t%s%s\n", info.State, stateDur)
|
fmt.Fprintf(w, "%s\t%s%s\n", label("state"), info.State, stateDur)
|
||||||
fmt.Fprintf(w, "enabled\t%v\n", info.Enabled)
|
fmt.Fprintf(w, "%s\t%v\n", label("enabled"), info.Enabled)
|
||||||
fmt.Fprintf(w, "weight\t%d\n", info.Weight)
|
fmt.Fprintf(w, "%s\t%s\n", label("healthcheck"), info.Healthcheck)
|
||||||
fmt.Fprintf(w, "healthcheck\t%s\n", info.Healthcheck)
|
|
||||||
for i, t := range info.Transitions {
|
for i, t := range info.Transitions {
|
||||||
ts := time.Unix(0, t.AtUnixNs)
|
ts := time.Unix(0, t.AtUnixNs)
|
||||||
label := ""
|
lbl := ""
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
label = "transitions"
|
lbl = label("transitions")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "%s\t%s → %s\t%s\t%s\n",
|
fmt.Fprintf(w, "%s\t%s → %s\t%s\t%s\n",
|
||||||
label,
|
lbl,
|
||||||
t.From, t.To,
|
t.From, t.To,
|
||||||
ts.Format("2006-01-02 15:04:05.000"),
|
ts.Format("2006-01-02 15:04:05.000"),
|
||||||
formatAgo(time.Since(ts)),
|
formatAgo(time.Since(ts)),
|
||||||
@@ -245,41 +286,41 @@ func runShowHealthCheck(ctx context.Context, client grpcapi.MaglevClient, args [
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
fmt.Fprintf(w, "name\t%s\n", info.Name)
|
fmt.Fprintf(w, "%s\t%s\n", label("name"), info.Name)
|
||||||
fmt.Fprintf(w, "type\t%s\n", info.Type)
|
fmt.Fprintf(w, "%s\t%s\n", label("type"), info.Type)
|
||||||
if info.Port > 0 {
|
if info.Port > 0 {
|
||||||
fmt.Fprintf(w, "port\t%d\n", info.Port)
|
fmt.Fprintf(w, "%s\t%d\n", label("port"), info.Port)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "interval\t%s\n", time.Duration(info.IntervalNs))
|
fmt.Fprintf(w, "%s\t%s\n", label("interval"), time.Duration(info.IntervalNs))
|
||||||
if info.FastIntervalNs > 0 {
|
if info.FastIntervalNs > 0 {
|
||||||
fmt.Fprintf(w, "fast-interval\t%s\n", time.Duration(info.FastIntervalNs))
|
fmt.Fprintf(w, "%s\t%s\n", label("fast-interval"), time.Duration(info.FastIntervalNs))
|
||||||
}
|
}
|
||||||
if info.DownIntervalNs > 0 {
|
if info.DownIntervalNs > 0 {
|
||||||
fmt.Fprintf(w, "down-interval\t%s\n", time.Duration(info.DownIntervalNs))
|
fmt.Fprintf(w, "%s\t%s\n", label("down-interval"), time.Duration(info.DownIntervalNs))
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "timeout\t%s\n", time.Duration(info.TimeoutNs))
|
fmt.Fprintf(w, "%s\t%s\n", label("timeout"), time.Duration(info.TimeoutNs))
|
||||||
fmt.Fprintf(w, "rise\t%d\n", info.Rise)
|
fmt.Fprintf(w, "%s\t%d\n", label("rise"), info.Rise)
|
||||||
fmt.Fprintf(w, "fall\t%d\n", info.Fall)
|
fmt.Fprintf(w, "%s\t%d\n", label("fall"), info.Fall)
|
||||||
if info.ProbeIpv4Src != "" {
|
if info.ProbeIpv4Src != "" {
|
||||||
fmt.Fprintf(w, "probe-ipv4-src\t%s\n", info.ProbeIpv4Src)
|
fmt.Fprintf(w, "%s\t%s\n", label("probe-ipv4-src"), info.ProbeIpv4Src)
|
||||||
}
|
}
|
||||||
if info.ProbeIpv6Src != "" {
|
if info.ProbeIpv6Src != "" {
|
||||||
fmt.Fprintf(w, "probe-ipv6-src\t%s\n", info.ProbeIpv6Src)
|
fmt.Fprintf(w, "%s\t%s\n", label("probe-ipv6-src"), info.ProbeIpv6Src)
|
||||||
}
|
}
|
||||||
if h := info.Http; h != nil {
|
if h := info.Http; h != nil {
|
||||||
fmt.Fprintf(w, "http.path\t%s\n", h.Path)
|
fmt.Fprintf(w, "%s\t%s\n", label("http.path"), h.Path)
|
||||||
if h.Host != "" {
|
if h.Host != "" {
|
||||||
fmt.Fprintf(w, "http.host\t%s\n", h.Host)
|
fmt.Fprintf(w, "%s\t%s\n", label("http.host"), h.Host)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "http.response-code\t%d-%d\n", h.ResponseCodeMin, h.ResponseCodeMax)
|
fmt.Fprintf(w, "%s\t%d-%d\n", label("http.response-code"), h.ResponseCodeMin, h.ResponseCodeMax)
|
||||||
if h.ResponseRegexp != "" {
|
if h.ResponseRegexp != "" {
|
||||||
fmt.Fprintf(w, "http.response-regexp\t%s\n", h.ResponseRegexp)
|
fmt.Fprintf(w, "%s\t%s\n", label("http.response-regexp"), h.ResponseRegexp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if t := info.Tcp; t != nil {
|
if t := info.Tcp; t != nil {
|
||||||
fmt.Fprintf(w, "tcp.ssl\t%v\n", t.Ssl)
|
fmt.Fprintf(w, "%s\t%v\n", label("tcp.ssl"), t.Ssl)
|
||||||
if t.ServerName != "" {
|
if t.ServerName != "" {
|
||||||
fmt.Fprintf(w, "tcp.server-name\t%s\n", t.ServerName)
|
fmt.Fprintf(w, "%s\t%s\n", label("tcp.server-name"), t.ServerName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return w.Flush()
|
return w.Flush()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
|
||||||
|
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
|
||||||
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +25,9 @@ func main() {
|
|||||||
|
|
||||||
func run() error {
|
func run() error {
|
||||||
serverAddr := flag.String("server", "localhost:9090", "maglev server address")
|
serverAddr := flag.String("server", "localhost:9090", "maglev server address")
|
||||||
|
color := flag.Bool("color", true, "colorize static labels in output")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
colorEnabled = *color
|
||||||
|
|
||||||
conn, err := grpc.NewClient(*serverAddr,
|
conn, err := grpc.NewClient(*serverAddr,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()))
|
grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
@@ -38,7 +41,9 @@ func run() error {
|
|||||||
|
|
||||||
args := flag.Args()
|
args := flag.Args()
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
// Interactive shell.
|
// Interactive shell: announce version on startup.
|
||||||
|
fmt.Printf("maglevc %s (commit %s, built %s)\n",
|
||||||
|
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
|
||||||
return runShell(ctx, client)
|
return runShell(ctx, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
|
||||||
"git.ipng.ch/ipng/vpp-maglev/internal/checker"
|
"git.ipng.ch/ipng/vpp-maglev/internal/checker"
|
||||||
"git.ipng.ch/ipng/vpp-maglev/internal/config"
|
"git.ipng.ch/ipng/vpp-maglev/internal/config"
|
||||||
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
||||||
@@ -28,17 +29,25 @@ func main() {
|
|||||||
|
|
||||||
func run() error {
|
func run() error {
|
||||||
// ---- flags / env --------------------------------------------------------
|
// ---- flags / env --------------------------------------------------------
|
||||||
|
printVersion := flag.Bool("version", false, "print version and exit")
|
||||||
configPath := stringFlag("config", "/etc/maglev/frontend.yaml", "MAGLEV_CONFIG", "path to frontend.yaml")
|
configPath := stringFlag("config", "/etc/maglev/frontend.yaml", "MAGLEV_CONFIG", "path to frontend.yaml")
|
||||||
grpcAddr := stringFlag("grpc-addr", ":9090", "MAGLEV_GRPC_ADDR", "gRPC listen address")
|
grpcAddr := stringFlag("grpc-addr", ":9090", "MAGLEV_GRPC_ADDR", "gRPC listen address")
|
||||||
logLevel := stringFlag("log-level", "info", "MAGLEV_LOG_LEVEL", "log level (debug|info|warn|error)")
|
logLevel := stringFlag("log-level", "info", "MAGLEV_LOG_LEVEL", "log level (debug|info|warn|error)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if *printVersion {
|
||||||
|
fmt.Printf("maglevd %s (commit %s, built %s)\n",
|
||||||
|
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---- logging ------------------------------------------------------------
|
// ---- logging ------------------------------------------------------------
|
||||||
var level slog.Level
|
var level slog.Level
|
||||||
if err := level.UnmarshalText([]byte(*logLevel)); err != nil {
|
if err := level.UnmarshalText([]byte(*logLevel)); err != nil {
|
||||||
return fmt.Errorf("invalid log level %q: %w", *logLevel, err)
|
return fmt.Errorf("invalid log level %q: %w", *logLevel, err)
|
||||||
}
|
}
|
||||||
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})))
|
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})))
|
||||||
|
slog.Info("starting", "version", buildinfo.Version(), "commit", buildinfo.Commit(), "date", buildinfo.Date())
|
||||||
|
|
||||||
// ---- config -------------------------------------------------------------
|
// ---- config -------------------------------------------------------------
|
||||||
cfg, err := config.Load(*configPath)
|
cfg, err := config.Load(*configPath)
|
||||||
|
|||||||
14
cmd/version.go
Normal file
14
cmd/version.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
// Set at build time via -ldflags.
|
||||||
|
var (
|
||||||
|
version = "dev"
|
||||||
|
commit = "unknown"
|
||||||
|
date = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Version() string { return version }
|
||||||
|
func Commit() string { return commit }
|
||||||
|
func Date() string { return date }
|
||||||
58
debian/build-deb.sh
vendored
Executable file
58
debian/build-deb.sh
vendored
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build a vpp-maglev Debian package for one architecture.
|
||||||
|
# Usage: build-deb.sh <amd64|arm64> <version> <commit>
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ARCH="${1:?usage: build-deb.sh <amd64|arm64> <version> <commit>}"
|
||||||
|
VERSION="${2:?usage: build-deb.sh <amd64|arm64> <version> <commit>}"
|
||||||
|
COMMIT="${3:?usage: build-deb.sh <amd64|arm64> <version> <commit>}"
|
||||||
|
|
||||||
|
FULL_VERSION="${VERSION}~${COMMIT}"
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
PKG="vpp-maglev_${FULL_VERSION}_${ARCH}"
|
||||||
|
STAGING="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$STAGING"' EXIT
|
||||||
|
|
||||||
|
echo "Building ${PKG}.deb"
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
install -d "$STAGING/usr/sbin"
|
||||||
|
install -d "$STAGING/usr/bin"
|
||||||
|
install -d "$STAGING/usr/share/man/man1"
|
||||||
|
install -d "$STAGING/usr/share/man/man8"
|
||||||
|
install -d "$STAGING/lib/systemd/system"
|
||||||
|
install -d "$STAGING/etc/default"
|
||||||
|
install -d "$STAGING/etc/maglev"
|
||||||
|
install -d "$STAGING/DEBIAN"
|
||||||
|
|
||||||
|
# Binaries
|
||||||
|
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevd" "$STAGING/usr/sbin/maglevd"
|
||||||
|
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevc" "$STAGING/usr/bin/maglevc"
|
||||||
|
|
||||||
|
# Man pages
|
||||||
|
gzip -9 -c "$REPO_ROOT/docs/maglevd.8" > "$STAGING/usr/share/man/man8/maglevd.8.gz"
|
||||||
|
gzip -9 -c "$REPO_ROOT/docs/maglevc.1" > "$STAGING/usr/share/man/man1/maglevc.1.gz"
|
||||||
|
|
||||||
|
# Systemd unit
|
||||||
|
install -m 644 "$REPO_ROOT/debian/maglevd.service" "$STAGING/lib/systemd/system/maglevd.service"
|
||||||
|
|
||||||
|
# /etc/default/maglev (conffile — dpkg won't overwrite on upgrade)
|
||||||
|
install -m 644 "$REPO_ROOT/debian/default.maglev" "$STAGING/etc/default/maglev"
|
||||||
|
|
||||||
|
# /etc/maglev/maglev.yaml (conffile)
|
||||||
|
install -m 644 "$REPO_ROOT/debian/maglev.yaml" "$STAGING/etc/maglev/maglev.yaml"
|
||||||
|
|
||||||
|
# DEBIAN/control (version field uses full_version including commit)
|
||||||
|
sed "s/@VERSION@/${FULL_VERSION}/;s/@ARCH@/${ARCH}/" \
|
||||||
|
"$REPO_ROOT/debian/control.in" > "$STAGING/DEBIAN/control"
|
||||||
|
|
||||||
|
# DEBIAN/conffiles, postinst, prerm
|
||||||
|
install -m 644 "$REPO_ROOT/debian/conffiles" "$STAGING/DEBIAN/conffiles"
|
||||||
|
install -m 755 "$REPO_ROOT/debian/postinst" "$STAGING/DEBIAN/postinst"
|
||||||
|
install -m 755 "$REPO_ROOT/debian/prerm" "$STAGING/DEBIAN/prerm"
|
||||||
|
|
||||||
|
# Emit package into build/
|
||||||
|
mkdir -p "$REPO_ROOT/build"
|
||||||
|
OUT="$REPO_ROOT/build/${PKG}.deb"
|
||||||
|
dpkg-deb --build --root-owner-group "$STAGING" "$OUT"
|
||||||
|
echo "Built: $OUT"
|
||||||
2
debian/conffiles
vendored
Normal file
2
debian/conffiles
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/etc/default/maglev
|
||||||
|
/etc/maglev/maglev.yaml
|
||||||
14
debian/control.in
vendored
Normal file
14
debian/control.in
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Package: vpp-maglev
|
||||||
|
Version: @VERSION@
|
||||||
|
Architecture: @ARCH@
|
||||||
|
Maintainer: Pim van Pelt <pim@ipng.ch>
|
||||||
|
Section: net
|
||||||
|
Priority: optional
|
||||||
|
Depends: systemd
|
||||||
|
Description: Maglev health-checker daemon and CLI client
|
||||||
|
maglevd monitors backends (HTTP, TCP, ICMP) with a rise/fall counter
|
||||||
|
model and exposes their aggregated state over a gRPC API. Configuration
|
||||||
|
is loaded from a YAML file and supports live reload via SIGHUP.
|
||||||
|
.
|
||||||
|
maglevc is an interactive CLI client for maglevd with tab completion,
|
||||||
|
inline help, and one-shot mode for scripting.
|
||||||
12
debian/default.maglev
vendored
Normal file
12
debian/default.maglev
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Default settings for maglevd.
|
||||||
|
# This file is sourced by /lib/systemd/system/maglevd.service.
|
||||||
|
# After editing, run: systemctl restart maglevd
|
||||||
|
|
||||||
|
# Path to the YAML configuration file.
|
||||||
|
MAGLEV_CONFIG=/etc/maglev/maglev.yaml
|
||||||
|
|
||||||
|
# gRPC listen address (default: :9090)
|
||||||
|
#MAGLEV_GRPC_ADDR=:9090
|
||||||
|
|
||||||
|
# Log level: debug, info, warn, error (default: info)
|
||||||
|
#MAGLEV_LOG_LEVEL=info
|
||||||
56
debian/maglev.yaml
vendored
Normal file
56
debian/maglev.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
maglev:
|
||||||
|
healthchecker:
|
||||||
|
transition-history: 5
|
||||||
|
# netns: dataplane # run probes inside a named network namespace
|
||||||
|
|
||||||
|
healthchecks:
|
||||||
|
http-check:
|
||||||
|
type: http
|
||||||
|
port: 80
|
||||||
|
params:
|
||||||
|
path: /
|
||||||
|
host: www.example.com
|
||||||
|
response-code: "200-301"
|
||||||
|
interval: 5s
|
||||||
|
fast-interval: 1s
|
||||||
|
timeout: 3s
|
||||||
|
rise: 2
|
||||||
|
fall: 6
|
||||||
|
|
||||||
|
tcp-ssl-check:
|
||||||
|
type: tcp
|
||||||
|
port: 443
|
||||||
|
params:
|
||||||
|
ssl: true
|
||||||
|
server-name: www.example.com
|
||||||
|
interval: 10s
|
||||||
|
fast-interval: 1s
|
||||||
|
timeout: 3s
|
||||||
|
rise: 2
|
||||||
|
fall: 6
|
||||||
|
|
||||||
|
backends:
|
||||||
|
web-1:
|
||||||
|
address: 192.0.2.10
|
||||||
|
healthcheck: http-check
|
||||||
|
web-2:
|
||||||
|
address: 192.0.2.11
|
||||||
|
healthcheck: http-check
|
||||||
|
web-3:
|
||||||
|
address: 192.0.2.12
|
||||||
|
healthcheck: http-check
|
||||||
|
|
||||||
|
frontends:
|
||||||
|
http-vip:
|
||||||
|
description: "HTTP VIP"
|
||||||
|
address: 192.0.2.1
|
||||||
|
protocol: tcp
|
||||||
|
port: 80
|
||||||
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
web-1: { weight: 10 }
|
||||||
|
web-2: {}
|
||||||
|
- name: fallback
|
||||||
|
backends:
|
||||||
|
web-3: {}
|
||||||
15
debian/maglevd.service
vendored
Normal file
15
debian/maglevd.service
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Maglev health-checker daemon
|
||||||
|
Documentation=man:maglevd(8)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
EnvironmentFile=/etc/default/maglev
|
||||||
|
ExecStart=/usr/sbin/maglevd --config ${MAGLEV_CONFIG}
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
8
debian/postinst
vendored
Normal file
8
debian/postinst
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
case "$1" in
|
||||||
|
configure)
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
systemctl enable maglevd.service || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
8
debian/prerm
vendored
Normal file
8
debian/prerm
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
case "$1" in
|
||||||
|
remove|purge)
|
||||||
|
systemctl stop maglevd.service || true
|
||||||
|
systemctl disable maglevd.service || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -77,12 +77,13 @@ Common fields (all types):
|
|||||||
* ***probe-ipv6-src***: An optional IPv6 source address used when probing IPv6 backends.
|
* ***probe-ipv6-src***: An optional IPv6 source address used when probing IPv6 backends.
|
||||||
Must be an IPv6 address. When omitted, the OS chooses the source address.
|
Must be an IPv6 address. When omitted, the OS chooses the source address.
|
||||||
* ***interval***: Required. A positive Go duration string (e.g. `2s`, `500ms`) controlling
|
* ***interval***: Required. A positive Go duration string (e.g. `2s`, `500ms`) controlling
|
||||||
how often a probe is sent when the backend is fully healthy or in the initial unknown state.
|
how often a probe is sent when the backend is fully healthy (counter at maximum).
|
||||||
* ***fast-interval***: Optional. A positive duration used instead of `interval` while the
|
* ***fast-interval***: Optional. A positive duration used instead of `interval` while the
|
||||||
backend's health counter is degraded (between down and up). When omitted, `interval` is used.
|
backend's health counter is degraded (between down and up) or in `unknown` state. When
|
||||||
|
omitted, `interval` is used.
|
||||||
* ***down-interval***: Optional. A positive duration used instead of `interval` while the
|
* ***down-interval***: Optional. A positive duration used instead of `interval` while the
|
||||||
backend is fully down. When omitted, `interval` is used. Setting this to a longer value
|
backend is fully down (counter at zero). When omitted, `interval` is used. Setting this to
|
||||||
reduces probe traffic to backends that are known to be offline.
|
a longer value reduces probe traffic to backends that are known to be offline.
|
||||||
* ***timeout***: Required. A positive duration after which an in-flight probe is abandoned
|
* ***timeout***: Required. A positive duration after which an in-flight probe is abandoned
|
||||||
and counted as a failure.
|
and counted as a failure.
|
||||||
* ***rise***: The number of consecutive successes required to transition from down to up.
|
* ***rise***: The number of consecutive successes required to transition from down to up.
|
||||||
@@ -193,9 +194,6 @@ multiple frontends.
|
|||||||
* ***enabled***: A boolean controlling whether this backend participates in any frontend.
|
* ***enabled***: A boolean controlling whether this backend participates in any frontend.
|
||||||
When `false`, the backend is excluded entirely and no probe goroutine is started.
|
When `false`, the backend is excluded entirely and no probe goroutine is started.
|
||||||
Defaults to `true`.
|
Defaults to `true`.
|
||||||
* ***weight***: An integer between 0 and 100 (inclusive) expressing the relative weight of
|
|
||||||
this backend in a frontend's pool. `0` keeps the backend in the pool but assigns it no
|
|
||||||
traffic. Defaults to `100`.
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
```yaml
|
```yaml
|
||||||
@@ -206,7 +204,6 @@ backends:
|
|||||||
nginx0-lon:
|
nginx0-lon:
|
||||||
address: 198.51.100.11
|
address: 198.51.100.11
|
||||||
healthcheck: nginx-http
|
healthcheck: nginx-http
|
||||||
weight: 50
|
|
||||||
nginx0-draining:
|
nginx0-draining:
|
||||||
address: 198.51.100.12
|
address: 198.51.100.12
|
||||||
healthcheck: nginx-http
|
healthcheck: nginx-http
|
||||||
@@ -220,8 +217,8 @@ backends:
|
|||||||
|
|
||||||
## frontends
|
## frontends
|
||||||
|
|
||||||
A named map of virtual IPs (VIPs). Each frontend ties together a listener address with a set
|
A named map of virtual IPs (VIPs). Each frontend ties together a listener address with an
|
||||||
of backends. The gRPC API exposes frontends by name.
|
ordered list of backend pools. The gRPC API exposes frontends by name.
|
||||||
|
|
||||||
* ***description***: An optional free-text string for documentation purposes.
|
* ***description***: An optional free-text string for documentation purposes.
|
||||||
* ***address***: Required. The IPv4 or IPv6 address of the VIP.
|
* ***address***: Required. The IPv4 or IPv6 address of the VIP.
|
||||||
@@ -232,38 +229,50 @@ of backends. The gRPC API exposes frontends by name.
|
|||||||
`protocol` to be set. When omitted, the frontend matches all ports. Note that the
|
`protocol` to be set. When omitted, the frontend matches all ports. Note that the
|
||||||
frontend port is independent of the healthcheck port: a frontend on port 443 may use
|
frontend port is independent of the healthcheck port: a frontend on port 443 may use
|
||||||
a healthcheck that probes port 80.
|
a healthcheck that probes port 80.
|
||||||
* ***backends***: Required. A non-empty list of backend names. All backends in a frontend
|
* ***pools***: Required. A non-empty ordered list of pool objects. Pools express priority:
|
||||||
must have addresses of the same address family (all IPv4 or all IPv6). Every name must
|
the first pool is preferred; subsequent pools act as fallbacks. All backends across all
|
||||||
refer to an existing entry in the `backends` section.
|
pools in a frontend must have addresses of the same address family (all IPv4 or all IPv6).
|
||||||
|
|
||||||
|
Each pool has:
|
||||||
|
|
||||||
|
* ***name***: Required. A non-empty string identifying the pool (e.g. `primary`, `fallback`).
|
||||||
|
* ***backends***: A map of backend names to per-pool backend options. Every name must refer
|
||||||
|
to an existing entry in the `backends` section.
|
||||||
|
|
||||||
|
Per-pool backend options:
|
||||||
|
|
||||||
|
* ***weight***: An integer between 0 and 100 (inclusive) expressing the relative weight of
|
||||||
|
this backend within the pool. `0` keeps the backend in the pool but assigns it no traffic.
|
||||||
|
Defaults to `100`. Weight is per-pool, not global — the same backend can appear with
|
||||||
|
different weights in different frontends.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
```yaml
|
```yaml
|
||||||
frontends:
|
frontends:
|
||||||
nginx-v4-http:
|
nginx-v4-http:
|
||||||
description: "IPv4 HTTP VIP"
|
description: "IPv4 HTTP VIP with fallback"
|
||||||
address: 198.51.100.1
|
address: 198.51.100.1
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
port: 80
|
port: 80
|
||||||
backends: [nginx0-ams, nginx0-lon]
|
pools:
|
||||||
|
- name: primary
|
||||||
nginx-v4-https:
|
backends:
|
||||||
description: "IPv4 HTTPS VIP — reuses the same backends as HTTP"
|
nginx0-ams: { weight: 10 }
|
||||||
address: 198.51.100.1
|
nginx0-lon: {}
|
||||||
protocol: tcp
|
- name: fallback
|
||||||
port: 443
|
backends:
|
||||||
backends: [nginx0-ams, nginx0-lon]
|
nginx0-fra: {}
|
||||||
|
|
||||||
maildrop-imaps:
|
maildrop-imaps:
|
||||||
description: "IMAPS VIP"
|
description: "IMAPS VIP"
|
||||||
address: 2001:db8::1
|
address: 2001:db8::1
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
port: 993
|
port: 993
|
||||||
backends: [maildrop0-ams, maildrop0-lon]
|
pools:
|
||||||
|
- name: primary
|
||||||
catchall:
|
backends:
|
||||||
description: "Match all traffic to this VIP regardless of protocol or port"
|
maildrop0-ams: {}
|
||||||
address: 198.51.100.2
|
maildrop0-lon: {}
|
||||||
backends: [static-backend]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -322,7 +331,6 @@ maglev:
|
|||||||
nginx0-fra:
|
nginx0-fra:
|
||||||
address: 198.51.100.12
|
address: 198.51.100.12
|
||||||
healthcheck: nginx
|
healthcheck: nginx
|
||||||
weight: 50
|
|
||||||
maildrop0-ams:
|
maildrop0-ams:
|
||||||
address: 2001:db8:1::10
|
address: 2001:db8:1::10
|
||||||
healthcheck: dovecot
|
healthcheck: dovecot
|
||||||
@@ -332,23 +340,46 @@ maglev:
|
|||||||
|
|
||||||
frontends:
|
frontends:
|
||||||
nginx-http:
|
nginx-http:
|
||||||
description: "HTTP VIP"
|
description: "HTTP VIP with fallback"
|
||||||
address: 198.51.100.1
|
address: 198.51.100.1
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
port: 80
|
port: 80
|
||||||
backends: [nginx0-ams, nginx0-lon, nginx0-fra]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
nginx0-ams: { weight: 10 }
|
||||||
|
nginx0-lon: {}
|
||||||
|
- name: fallback
|
||||||
|
backends:
|
||||||
|
nginx0-fra: {}
|
||||||
|
|
||||||
nginx-https:
|
nginx-https:
|
||||||
description: "HTTPS VIP — same backends, different port"
|
description: "HTTPS VIP — same backends, different port"
|
||||||
address: 198.51.100.1
|
address: 198.51.100.1
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
port: 443
|
port: 443
|
||||||
backends: [nginx0-ams, nginx0-lon, nginx0-fra]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
nginx0-ams: { weight: 10 }
|
||||||
|
nginx0-lon: {}
|
||||||
|
- name: fallback
|
||||||
|
backends:
|
||||||
|
nginx0-fra: {}
|
||||||
|
|
||||||
maildrop-imaps:
|
maildrop-imaps:
|
||||||
description: "IMAPS VIP"
|
description: "IMAPS VIP"
|
||||||
address: 2001:db8::1
|
address: 2001:db8::1
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
port: 993
|
port: 993
|
||||||
backends: [maildrop0-ams, maildrop0-lon]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
maildrop0-ams: {}
|
||||||
|
maildrop0-lon: {}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For a detailed description of the health state machine, probe intervals, and all
|
||||||
|
transition events, see [healthchecks.md](healthchecks.md).
|
||||||
|
|||||||
178
docs/healthchecks.md
Normal file
178
docs/healthchecks.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Health Checking
|
||||||
|
|
||||||
|
`maglevd` probes each backend independently of how many frontends reference it.
|
||||||
|
Every backend runs exactly one probe goroutine. State changes are broadcast as
|
||||||
|
gRPC events to all connected `WatchBackendEvents` subscribers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## States
|
||||||
|
|
||||||
|
| State | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `unknown` | Initial state; also entered after a resume or backend restart. |
|
||||||
|
| `up` | Backend is healthy and eligible to receive traffic. |
|
||||||
|
| `down` | Backend has failed enough consecutive probes to be considered offline. |
|
||||||
|
| `paused` | Health checking suspended by an operator. Probes fire but results are discarded. |
|
||||||
|
| `removed` | Backend was removed from configuration. No further probes are accepted. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rise / fall counter
|
||||||
|
|
||||||
|
The state machine is driven by HAProxy's single-integer health counter.
|
||||||
|
|
||||||
|
```
|
||||||
|
counter ∈ [0, rise + fall − 1] (called Max below)
|
||||||
|
|
||||||
|
backend is UP when counter ≥ rise
|
||||||
|
backend is DOWN when counter < rise
|
||||||
|
```
|
||||||
|
|
||||||
|
On each probe:
|
||||||
|
- **pass** — counter increments, ceiling at Max.
|
||||||
|
- **fail** — counter decrements, floor at 0.
|
||||||
|
|
||||||
|
This gives **hysteresis**: a backend that is barely up (counter = rise) needs
|
||||||
|
`fall` consecutive failures before it transitions to down. A backend that is
|
||||||
|
fully down (counter = 0) needs `rise` consecutive passes to come back up. A
|
||||||
|
backend that oscillates between passing and failing stays in the degraded range
|
||||||
|
without bouncing between up and down.
|
||||||
|
|
||||||
|
### Expedited unknown resolution
|
||||||
|
|
||||||
|
When a backend enters `unknown` state (new, restarted, or resumed) its counter
|
||||||
|
is pre-loaded to `rise − 1`. This means a single probe result is enough to
|
||||||
|
resolve the state:
|
||||||
|
|
||||||
|
- **1 pass** → `up`
|
||||||
|
- **1 fail** → `down` (also via the special unknown shortcut below)
|
||||||
|
|
||||||
|
In addition, any failure while state is `unknown` transitions immediately to
|
||||||
|
`down`, regardless of the counter value.
|
||||||
|
|
||||||
|
### Example: rise=2, fall=3 (Max=4)
|
||||||
|
|
||||||
|
```
|
||||||
|
counter: 0 1 2 3 4
|
||||||
|
state: DOWN DOWN UP UP UP
|
||||||
|
^
|
||||||
|
rise boundary
|
||||||
|
```
|
||||||
|
|
||||||
|
A backend starting from unknown has counter=1 (rise−1). One pass → counter=2
|
||||||
|
→ up. One fail while unknown → down immediately.
|
||||||
|
|
||||||
|
A backend that just became up sits at counter=2. It needs 3 failures to go down
|
||||||
|
(2→1→0, crossing the rise boundary at 2→1).
|
||||||
|
|
||||||
|
A backend that has been fully healthy for a while sits at counter=4. It needs 3
|
||||||
|
failures to go down (4→3→2→1, crossing the rise boundary at 2→1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Probe intervals
|
||||||
|
|
||||||
|
The interval used between probes depends on the backend's counter state:
|
||||||
|
|
||||||
|
| Condition | Interval used |
|
||||||
|
|---|---|
|
||||||
|
| State is `unknown` | `fast-interval` (falls back to `interval`) |
|
||||||
|
| Counter = Max (fully healthy) | `interval` |
|
||||||
|
| Counter = 0 (fully down) | `down-interval` (falls back to `interval`) |
|
||||||
|
| Counter between 0 and Max (degraded) | `fast-interval` (falls back to `interval`) |
|
||||||
|
|
||||||
|
Using `fast-interval` in degraded and unknown states means a flapping or
|
||||||
|
recovering backend is re-evaluated quickly without waiting a full `interval`.
|
||||||
|
Using `down-interval` for fully down backends reduces probe traffic to servers
|
||||||
|
that are known to be offline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transition events
|
||||||
|
|
||||||
|
Every state change is logged as `backend-transition` and emitted as a gRPC
|
||||||
|
`BackendEvent` to all active `WatchBackendEvents` streams.
|
||||||
|
|
||||||
|
### Backend added (config load or reload)
|
||||||
|
|
||||||
|
```
|
||||||
|
unknown → unknown (code: start)
|
||||||
|
```
|
||||||
|
|
||||||
|
The counter is pre-loaded to `rise − 1`. The first probe fires immediately at
|
||||||
|
`fast-interval` (or `interval` if not configured). One pass produces `unknown →
|
||||||
|
up`; one fail produces `unknown → down`.
|
||||||
|
|
||||||
|
If multiple backends start together they are staggered across the first
|
||||||
|
`interval` to avoid probe bursts.
|
||||||
|
|
||||||
|
### Probe pass
|
||||||
|
|
||||||
|
- Counter increments.
|
||||||
|
- If counter reaches `rise` from below: `down → up` (or `unknown → up`).
|
||||||
|
- If already up: no transition. Next probe at `fast-interval` if degraded,
|
||||||
|
`interval` if fully healthy.
|
||||||
|
|
||||||
|
### Probe fail
|
||||||
|
|
||||||
|
- Counter decrements.
|
||||||
|
- If counter drops below `rise` from above: `up → down`.
|
||||||
|
- If state is `unknown`: transition immediately to `down` regardless of counter.
|
||||||
|
- Next probe at `down-interval` if fully down, `fast-interval` if degraded.
|
||||||
|
|
||||||
|
### Pause
|
||||||
|
|
||||||
|
```
|
||||||
|
<any> → paused (operator action)
|
||||||
|
```
|
||||||
|
|
||||||
|
The counter is reset to 0. Probes continue to fire on their normal schedule but
|
||||||
|
all results are discarded. The backend stays `paused` until explicitly resumed.
|
||||||
|
|
||||||
|
### Resume
|
||||||
|
|
||||||
|
```
|
||||||
|
paused → unknown (operator action)
|
||||||
|
```
|
||||||
|
|
||||||
|
The counter is reset to `rise − 1`. The probe goroutine is woken immediately
|
||||||
|
(no wait for the next scheduled probe). One subsequent pass produces `unknown →
|
||||||
|
up`; one fail produces `unknown → down`.
|
||||||
|
|
||||||
|
### Backend removed (config reload)
|
||||||
|
|
||||||
|
```
|
||||||
|
<any> → removed (code: removed)
|
||||||
|
```
|
||||||
|
|
||||||
|
The probe goroutine stops. No further state changes occur. The removed event is
|
||||||
|
emitted using the frontend map from before the reload so that consumers can
|
||||||
|
correlate it to the correct frontend.
|
||||||
|
|
||||||
|
### Backend healthcheck config changed (config reload)
|
||||||
|
|
||||||
|
The old probe goroutine is stopped (`<any> → removed`) and a new one started
|
||||||
|
(`unknown → unknown`, code: `start`). The new goroutine resolves state on the
|
||||||
|
first probe as described under *Backend added* above.
|
||||||
|
|
||||||
|
### Backend metadata changed without healthcheck change (config reload)
|
||||||
|
|
||||||
|
Weight, enabled flag, and similar fields are updated in place. The probe
|
||||||
|
goroutine is not restarted and no transition event is emitted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Log lines
|
||||||
|
|
||||||
|
All state changes produce a structured log line at `INFO` level:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"level":"INFO","msg":"backend-transition","backend":"nginx0-ams","from":"up","to":"paused"}
|
||||||
|
{"level":"INFO","msg":"backend-transition","backend":"nginx0-ams","from":"paused","to":"unknown"}
|
||||||
|
{"level":"INFO","msg":"backend-transition","backend":"nginx0-ams","from":"unknown","to":"up","code":"L7OK","detail":""}
|
||||||
|
```
|
||||||
|
|
||||||
|
Probe-driven transitions also carry `code` and `detail` fields from the probe
|
||||||
|
result (e.g. `L4CON`, `L7STS`, `connection refused`). Operator-driven
|
||||||
|
transitions (pause, resume) carry empty code and detail.
|
||||||
112
docs/maglevc.1
Normal file
112
docs/maglevc.1
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
.TH MAGLEVC 1 "April 2026" "vpp\-maglev" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
maglevc \- Maglev health\-checker CLI client
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B maglevc
|
||||||
|
[\fB\-server\fR \fIaddr\fR]
|
||||||
|
[\fB\-color\fR[=\fIbool\fR]]
|
||||||
|
[\fIcommand\fR [\fIargs\fR...]]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B maglevc
|
||||||
|
is an interactive CLI client for
|
||||||
|
.BR maglevd (8).
|
||||||
|
Without arguments it opens a readline shell with tab completion and
|
||||||
|
inline help.
|
||||||
|
A command may also be passed directly on the command line for one\-shot use,
|
||||||
|
which is useful for scripting (use
|
||||||
|
.B \-color=false
|
||||||
|
to suppress ANSI codes).
|
||||||
|
.PP
|
||||||
|
When the shell starts it prints the build version and connects to the
|
||||||
|
.B maglevd
|
||||||
|
gRPC server specified by
|
||||||
|
.BR \-server .
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BI \-server " addr"
|
||||||
|
Address of the
|
||||||
|
.B maglevd
|
||||||
|
gRPC server.
|
||||||
|
(default:
|
||||||
|
.IR localhost:9090 )
|
||||||
|
.TP
|
||||||
|
.BR \-color [=\fIbool\fR]
|
||||||
|
Colorize static field labels in output using ANSI dark blue.
|
||||||
|
(default: true)
|
||||||
|
Pass
|
||||||
|
.B \-color=false
|
||||||
|
to disable, e.g.\& when piping output.
|
||||||
|
.SH COMMANDS
|
||||||
|
Commands are entered at the
|
||||||
|
.B maglevc>
|
||||||
|
prompt or passed as arguments on the command line.
|
||||||
|
All static tokens support tab completion; dynamic names (frontend, backend,
|
||||||
|
health\-check names) are completed by querying the server.
|
||||||
|
Type
|
||||||
|
.B ?
|
||||||
|
at any point to list completions without advancing the input.
|
||||||
|
.SS Show commands
|
||||||
|
.TP
|
||||||
|
.B show version
|
||||||
|
Print build version, commit hash, and build date.
|
||||||
|
.TP
|
||||||
|
.B show frontends
|
||||||
|
List all configured frontends.
|
||||||
|
.TP
|
||||||
|
.BI "show frontend " name
|
||||||
|
Show address, protocol, port, description, and pools (with weights and
|
||||||
|
disabled\-backend notation) for the named frontend.
|
||||||
|
.TP
|
||||||
|
.B show backends
|
||||||
|
List all active backends.
|
||||||
|
.TP
|
||||||
|
.BI "show backend " name
|
||||||
|
Show address, current health state (with duration), enabled flag,
|
||||||
|
health\-check name, and recent state transitions with timestamps.
|
||||||
|
.TP
|
||||||
|
.B show healthchecks
|
||||||
|
List all configured health checks.
|
||||||
|
.TP
|
||||||
|
.BI "show healthcheck " name
|
||||||
|
Show the full configuration of the named health check.
|
||||||
|
.SS Set commands
|
||||||
|
.TP
|
||||||
|
.BI "set backend " "name " pause
|
||||||
|
Pause health checking for a backend, freezing its state.
|
||||||
|
.TP
|
||||||
|
.BI "set backend " "name " resume
|
||||||
|
Resume health checking for a backend; state resets to
|
||||||
|
.BR unknown .
|
||||||
|
.SS Shell commands
|
||||||
|
.TP
|
||||||
|
.BR quit ", " exit
|
||||||
|
Exit the interactive shell.
|
||||||
|
.SH COMPLETION
|
||||||
|
In interactive mode, press
|
||||||
|
.B Tab
|
||||||
|
to complete the current token.
|
||||||
|
If more than one completion is possible, all candidates are listed.
|
||||||
|
Type
|
||||||
|
.B ?
|
||||||
|
anywhere on the line to list candidates at that position without consuming
|
||||||
|
the character or advancing the cursor.
|
||||||
|
.SH EXAMPLES
|
||||||
|
One\-shot query (no color, suitable for scripts):
|
||||||
|
.PP
|
||||||
|
.RS
|
||||||
|
.EX
|
||||||
|
maglevc \-color=false show backends
|
||||||
|
.EE
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
Interactive session:
|
||||||
|
.PP
|
||||||
|
.RS
|
||||||
|
.EX
|
||||||
|
maglevc \-server 10.0.0.1:9090
|
||||||
|
.EE
|
||||||
|
.RE
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR maglevd (8)
|
||||||
|
.SH AUTHOR
|
||||||
|
Pim van Pelt <pim@ipng.ch>
|
||||||
85
docs/maglevd.8
Normal file
85
docs/maglevd.8
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
.TH MAGLEVD 8 "April 2026" "vpp\-maglev" "System Administration"
|
||||||
|
.SH NAME
|
||||||
|
maglevd \- Maglev health\-checker daemon
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B maglevd
|
||||||
|
[\fB\-config\fR \fIfile\fR]
|
||||||
|
[\fB\-grpc\-addr\fR \fIaddr\fR]
|
||||||
|
[\fB\-log\-level\fR \fIlevel\fR]
|
||||||
|
[\fB\-version\fR]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B maglevd
|
||||||
|
is a health\-checker daemon that monitors backends (HTTP, TCP, ICMP) and
|
||||||
|
exposes their aggregated state via a gRPC API.
|
||||||
|
Configuration is loaded from a YAML file.
|
||||||
|
A running daemon reloads its configuration when it receives
|
||||||
|
.BR SIGHUP .
|
||||||
|
.PP
|
||||||
|
Backends are tracked with a rise/fall counter model.
|
||||||
|
Each backend cycles through the states
|
||||||
|
.BR unknown ,
|
||||||
|
.BR up ,
|
||||||
|
.BR down ,
|
||||||
|
and
|
||||||
|
.B paused
|
||||||
|
(operator\-set).
|
||||||
|
Health\-check intervals adapt automatically: a faster interval is used when
|
||||||
|
a backend is not fully healthy, and a slower interval when it has been
|
||||||
|
continuously down.
|
||||||
|
.SH OPTIONS
|
||||||
|
Each flag may also be supplied via an environment variable (shown in
|
||||||
|
parentheses); the flag takes precedence.
|
||||||
|
.TP
|
||||||
|
.BI \-config " file"
|
||||||
|
Path to the YAML configuration file.
|
||||||
|
.RI "(default: " /etc/maglev/maglev.conf "; env: " MAGLEV_CONFIG )
|
||||||
|
.TP
|
||||||
|
.BI \-grpc\-addr " addr"
|
||||||
|
TCP address on which the gRPC server listens.
|
||||||
|
.RI "(default: " :9090 "; env: " MAGLEV_GRPC_ADDR )
|
||||||
|
.TP
|
||||||
|
.BI \-log\-level " level"
|
||||||
|
Structured\-log verbosity:
|
||||||
|
.BR debug ,
|
||||||
|
.BR info ,
|
||||||
|
.BR warn ,
|
||||||
|
or
|
||||||
|
.BR error .
|
||||||
|
.RI "(default: " info "; env: " MAGLEV_LOG_LEVEL )
|
||||||
|
.TP
|
||||||
|
.B \-version
|
||||||
|
Print version, commit hash, and build date, then exit.
|
||||||
|
.SH SIGNALS
|
||||||
|
.TP
|
||||||
|
.B SIGHUP
|
||||||
|
Reload the configuration file without restarting.
|
||||||
|
New backends are added, removed backends are stopped, and unchanged
|
||||||
|
backend workers are left running.
|
||||||
|
.TP
|
||||||
|
.BR SIGTERM ", " SIGINT
|
||||||
|
Gracefully shut down: drain active gRPC streams, then exit.
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.I /etc/maglev/maglev.conf
|
||||||
|
Default configuration file (YAML).
|
||||||
|
.TP
|
||||||
|
.I /etc/default/maglev
|
||||||
|
Environment file sourced by the systemd unit before starting
|
||||||
|
.BR maglevd .
|
||||||
|
.SH CONFIGURATION
|
||||||
|
The configuration file uses YAML and has four top\-level sections under the
|
||||||
|
.B maglev
|
||||||
|
key:
|
||||||
|
.BR healthchecker ,
|
||||||
|
.BR healthchecks ,
|
||||||
|
.BR backends ,
|
||||||
|
and
|
||||||
|
.BR frontends .
|
||||||
|
.PP
|
||||||
|
See the example at
|
||||||
|
.I /etc/maglev/maglev.conf
|
||||||
|
and the full reference in the project documentation.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR maglevc (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Pim van Pelt <pim@ipng.ch>
|
||||||
133
docs/user-guide.md
Normal file
133
docs/user-guide.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# User Guide
|
||||||
|
|
||||||
|
## maglevd
|
||||||
|
|
||||||
|
`maglevd` is the health-checker daemon. It probes backends according to the
|
||||||
|
configuration file, maintains their health state, and exposes a gRPC API for
|
||||||
|
inspection and control.
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
| Flag | Environment variable | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--config` | `MAGLEV_CONFIG` | `/etc/maglev/maglev.yaml` | Path to the YAML configuration file. |
|
||||||
|
| `--grpc-addr` | `MAGLEV_GRPC_ADDR` | `:9090` | TCP address on which the gRPC server listens. |
|
||||||
|
| `--log-level` | `MAGLEV_LOG_LEVEL` | `info` | Log verbosity: `debug`, `info`, `warn`, or `error`. |
|
||||||
|
| `--version` | — | — | Print version, commit hash, and build date, then exit. |
|
||||||
|
|
||||||
|
Flags take precedence over environment variables. Both are optional; defaults
|
||||||
|
are used for anything not set.
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
| Signal | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `SIGHUP` | Reload the configuration file. New backends are started, removed backends are stopped, backends whose health-check config is unchanged continue probing without interruption. |
|
||||||
|
| `SIGTERM` / `SIGINT` | Graceful shutdown. Active gRPC streams are closed, the server drains, then the process exits. |
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
`maglevd` requires `CAP_NET_RAW` when any health check uses `type: icmp`.
|
||||||
|
All other check types (`tcp`, `http`) use normal TCP sockets and require no
|
||||||
|
special capabilities.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
All log output is written to stdout as JSON using Go's `log/slog`. The first
|
||||||
|
line logged after the logger is configured is a `starting` record that includes
|
||||||
|
`version`, `commit`, and `date`. Every state change emits a `backend-transition`
|
||||||
|
line at `INFO` level. Set `--log-level debug` to see individual probe attempts
|
||||||
|
and their outcomes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## maglevc
|
||||||
|
|
||||||
|
`maglevc` is the interactive control-plane client. It connects to a running
|
||||||
|
`maglevd` over gRPC and either executes a single command or drops into an
|
||||||
|
interactive shell.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
maglevc [--server host:port] [--color[=bool]] [command...]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `--server` | `localhost:9090` | Address of the `maglevd` gRPC server. |
|
||||||
|
| `--color` | `true` | Colorize static field labels in output (dark blue ANSI). Pass `--color=false` to disable, e.g. when piping. |
|
||||||
|
|
||||||
|
When `command` arguments are supplied the command is executed and `maglevc`
|
||||||
|
exits. When no arguments are given an interactive shell is started and the
|
||||||
|
build version is printed on entry.
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
show version Print build version, commit hash, and build date.
|
||||||
|
|
||||||
|
show frontends List all frontend names.
|
||||||
|
show frontend <name> Show address, protocol, port, description, and pools.
|
||||||
|
Each pool lists its backends with weights (if != 100)
|
||||||
|
and marks disabled backends with [disabled].
|
||||||
|
|
||||||
|
show backends List all backend names.
|
||||||
|
show backend <name> Show address, current state (with duration in that state),
|
||||||
|
enabled flag, health check, and recent state transitions
|
||||||
|
with timestamps and how long ago each occurred.
|
||||||
|
|
||||||
|
show healthchecks List all health-check names.
|
||||||
|
show healthcheck <name> Show full health-check configuration.
|
||||||
|
|
||||||
|
set backend <name> pause Suspend health checking for a backend, freezing its state.
|
||||||
|
set backend <name> resume Resume health checking; backend re-enters unknown state
|
||||||
|
and is probed immediately.
|
||||||
|
|
||||||
|
quit / exit Leave the interactive shell.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive shell
|
||||||
|
|
||||||
|
The shell prompt is `maglev> `. Two completion mechanisms are available:
|
||||||
|
|
||||||
|
**Tab completion** — pressing `<Tab>` at any point completes the current token.
|
||||||
|
Fixed keywords (commands and subcommands) are completed from the command tree.
|
||||||
|
Backend, frontend, and health-check names are fetched live from the server with
|
||||||
|
a 1-second timeout. If the partial token is unambiguous the word is completed
|
||||||
|
in place; if multiple candidates exist they are listed and the prompt is
|
||||||
|
restored.
|
||||||
|
|
||||||
|
**Inline help (`?`)** — typing `?` at any point prints the available
|
||||||
|
completions for the current position, with a short description next to each
|
||||||
|
keyword. The `?` character is not added to the input line.
|
||||||
|
|
||||||
|
Commands and keywords support **prefix matching**: typing `sh b` is equivalent
|
||||||
|
to `show backend` provided the prefix is unambiguous. Exact matches always take
|
||||||
|
priority over prefix matches, so `show backend` and `show backends` are
|
||||||
|
unambiguous even though one is a prefix of the other.
|
||||||
|
|
||||||
|
### Command tree and parser
|
||||||
|
|
||||||
|
Commands form a tree of `Node` values. Each node has a fixed `Word` (a keyword)
|
||||||
|
or is a *slot node* (marked by a `Dynamic` function that enumerates valid
|
||||||
|
values at completion time). The parser (`Walk`) descends the tree token by
|
||||||
|
token:
|
||||||
|
|
||||||
|
1. Try to match the current token against the fixed-keyword children of the
|
||||||
|
current node (exact match first, then unique prefix match).
|
||||||
|
2. If no fixed child matches, try a slot child — any token is accepted and
|
||||||
|
stored as an argument.
|
||||||
|
3. Stop when tokens are exhausted or no match is found.
|
||||||
|
|
||||||
|
The leaf node reached by `Walk` must have a `Run` function; otherwise the
|
||||||
|
available sub-commands at that position are printed as help. Arguments
|
||||||
|
collected from slot nodes are passed to `Run` as a slice.
|
||||||
|
|
||||||
|
Example walk for `set backend nginx0-ams pause`:
|
||||||
|
|
||||||
|
```
|
||||||
|
root → set → backend → <name>(nginx0-ams collected as arg) → pause
|
||||||
|
```
|
||||||
|
|
||||||
|
`pause.Run` is called with `args = ["nginx0-ams"]`.
|
||||||
@@ -208,11 +208,18 @@ func (c *Checker) ListFrontendBackends(frontendName string) []*health.Backend {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var out []*health.Backend
|
var out []*health.Backend
|
||||||
for _, name := range fe.Backends {
|
seen := map[string]struct{}{}
|
||||||
|
for _, pool := range fe.Pools {
|
||||||
|
for name := range pool.Backends {
|
||||||
|
if _, already := seen[name]; already {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[name] = struct{}{}
|
||||||
if w, ok := c.workers[name]; ok {
|
if w, ok := c.workers[name]; ok {
|
||||||
out = append(out, w.backend)
|
out = append(out, w.backend)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,18 +405,25 @@ func (c *Checker) runProbe(ctx context.Context, name string, pos, total int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// emitForBackend emits one Event per frontend that references backendName,
|
// emitForBackend emits one Event per frontend that references backendName
|
||||||
// using the provided frontends map. Must be called with c.mu held.
|
// (in any pool), using the provided frontends map. Must be called with c.mu held.
|
||||||
func (c *Checker) emitForBackend(backendName string, addr net.IP, t health.Transition, frontends map[string]config.Frontend) {
|
func (c *Checker) emitForBackend(backendName string, addr net.IP, t health.Transition, frontends map[string]config.Frontend) {
|
||||||
for feName, fe := range frontends {
|
for feName, fe := range frontends {
|
||||||
for _, name := range fe.Backends {
|
emitted := false
|
||||||
|
for _, pool := range fe.Pools {
|
||||||
|
if emitted {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for name := range pool.Backends {
|
||||||
if name == backendName {
|
if name == backendName {
|
||||||
c.emit(Event{FrontendName: feName, BackendName: backendName, Backend: addr, Transition: t})
|
c.emit(Event{FrontendName: feName, BackendName: backendName, Backend: addr, Transition: t})
|
||||||
|
emitted = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// emit sends an event to the internal fan-out channel (non-blocking).
|
// emit sends an event to the internal fan-out channel (non-blocking).
|
||||||
// Must be called with c.mu held.
|
// Must be called with c.mu held.
|
||||||
@@ -491,16 +505,18 @@ func tcpParamsEqual(a, b *config.TCPParams) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// activeBackendNames returns a sorted, deduplicated list of backend names that
|
// activeBackendNames returns a sorted, deduplicated list of backend names that
|
||||||
// are referenced by at least one frontend and have Enabled: true.
|
// are referenced by at least one frontend pool and have Enabled: true.
|
||||||
func activeBackendNames(cfg *config.Config) []string {
|
func activeBackendNames(cfg *config.Config) []string {
|
||||||
seen := map[string]struct{}{}
|
seen := map[string]struct{}{}
|
||||||
for _, fe := range cfg.Frontends {
|
for _, fe := range cfg.Frontends {
|
||||||
for _, name := range fe.Backends {
|
for _, pool := range fe.Pools {
|
||||||
|
for name := range pool.Backends {
|
||||||
if b, ok := cfg.Backends[name]; ok && b.Enabled {
|
if b, ok := cfg.Backends[name]; ok && b.Enabled {
|
||||||
seen[name] = struct{}{}
|
seen[name] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
names := make([]string, 0, len(seen))
|
names := make([]string, 0, len(seen))
|
||||||
for name := range seen {
|
for name := range seen {
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ func makeTestConfig(interval time.Duration, fall, rise int) *config.Config {
|
|||||||
Address: net.ParseIP("10.0.0.2"),
|
Address: net.ParseIP("10.0.0.2"),
|
||||||
HealthCheck: "icmp",
|
HealthCheck: "icmp",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Weight: 100,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Frontends: map[string]config.Frontend{
|
Frontends: map[string]config.Frontend{
|
||||||
@@ -37,7 +36,11 @@ func makeTestConfig(interval time.Duration, fall, rise int) *config.Config {
|
|||||||
Address: net.ParseIP("192.0.2.1"),
|
Address: net.ParseIP("192.0.2.1"),
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
Port: 80,
|
Port: 80,
|
||||||
Backends: []string{"be0"},
|
Pools: []config.Pool{
|
||||||
|
{Name: "primary", Backends: map[string]config.PoolBackend{
|
||||||
|
"be0": {Weight: 100},
|
||||||
|
}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -116,13 +119,16 @@ func TestReloadAddsBackend(t *testing.T) {
|
|||||||
Address: net.ParseIP("10.0.0.3"),
|
Address: net.ParseIP("10.0.0.3"),
|
||||||
HealthCheck: "icmp",
|
HealthCheck: "icmp",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Weight: 100,
|
|
||||||
}
|
}
|
||||||
newCfg.Frontends["web2"] = config.Frontend{
|
newCfg.Frontends["web2"] = config.Frontend{
|
||||||
Address: net.ParseIP("192.0.2.2"),
|
Address: net.ParseIP("192.0.2.2"),
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
Port: 443,
|
Port: 443,
|
||||||
Backends: []string{"be1"},
|
Pools: []config.Pool{
|
||||||
|
{Name: "primary", Backends: map[string]config.PoolBackend{
|
||||||
|
"be1": {Weight: 100},
|
||||||
|
}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -186,7 +192,11 @@ func TestSharedBackendProbedOnce(t *testing.T) {
|
|||||||
Address: net.ParseIP("192.0.2.3"),
|
Address: net.ParseIP("192.0.2.3"),
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
Port: 443,
|
Port: 443,
|
||||||
Backends: []string{"be0"},
|
Pools: []config.Pool{
|
||||||
|
{Name: "primary", Backends: map[string]config.PoolBackend{
|
||||||
|
"be0": {Weight: 100},
|
||||||
|
}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
c := New(cfg)
|
c := New(cfg)
|
||||||
|
|||||||
@@ -67,16 +67,26 @@ type Backend struct {
|
|||||||
Address net.IP
|
Address net.IP
|
||||||
HealthCheck string // name reference into Config.HealthChecks; "" = no probing, assume healthy
|
HealthCheck string // name reference into Config.HealthChecks; "" = no probing, assume healthy
|
||||||
Enabled bool // default true; false = exclude from serving entirely
|
Enabled bool // default true; false = exclude from serving entirely
|
||||||
|
}
|
||||||
|
|
||||||
|
// PoolBackend is a backend reference within a pool, with pool-local weight.
|
||||||
|
type PoolBackend struct {
|
||||||
Weight int // 0-100, default 100
|
Weight int // 0-100, default 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pool is an ordered tier of backends within a frontend.
|
||||||
|
type Pool struct {
|
||||||
|
Name string
|
||||||
|
Backends map[string]PoolBackend // keyed by backend name
|
||||||
|
}
|
||||||
|
|
||||||
// Frontend is a single virtual IP entry.
|
// Frontend is a single virtual IP entry.
|
||||||
type Frontend struct {
|
type Frontend struct {
|
||||||
Description string
|
Description string
|
||||||
Address net.IP
|
Address net.IP
|
||||||
Protocol string // "tcp", "udp", or "" (all traffic)
|
Protocol string // "tcp", "udp", or "" (all traffic)
|
||||||
Port uint16 // 0 means omitted (all ports)
|
Port uint16 // 0 means omitted (all ports)
|
||||||
Backends []string // backend names, each must exist in Config.Backends
|
Pools []Pool // ordered tiers; first pool with any up backend is active
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- raw YAML types --------------------------------------------------------
|
// ---- raw YAML types --------------------------------------------------------
|
||||||
@@ -127,15 +137,23 @@ type rawBackend struct {
|
|||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
||||||
HealthCheck string `yaml:"healthcheck"`
|
HealthCheck string `yaml:"healthcheck"`
|
||||||
Enabled *bool `yaml:"enabled"` // nil → default true
|
Enabled *bool `yaml:"enabled"` // nil → default true
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawPoolBackend struct {
|
||||||
Weight *int `yaml:"weight"` // nil → default 100
|
Weight *int `yaml:"weight"` // nil → default 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rawPool struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Backends map[string]rawPoolBackend `yaml:"backends"`
|
||||||
|
}
|
||||||
|
|
||||||
type rawFrontend struct {
|
type rawFrontend struct {
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
||||||
Protocol string `yaml:"protocol"`
|
Protocol string `yaml:"protocol"`
|
||||||
Port uint16 `yaml:"port"`
|
Port uint16 `yaml:"port"`
|
||||||
Backends []string `yaml:"backends"`
|
Pools []rawPool `yaml:"pools"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Load ------------------------------------------------------------------
|
// ---- Load ------------------------------------------------------------------
|
||||||
@@ -319,11 +337,6 @@ func convertBackend(name string, r *rawBackend, hcs map[string]HealthCheck) (Bac
|
|||||||
Address: ip,
|
Address: ip,
|
||||||
HealthCheck: r.HealthCheck,
|
HealthCheck: r.HealthCheck,
|
||||||
Enabled: boolDefault(r.Enabled, true),
|
Enabled: boolDefault(r.Enabled, true),
|
||||||
Weight: intDefault(r.Weight, 100),
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Weight < 0 || b.Weight > 100 {
|
|
||||||
return Backend{}, fmt.Errorf("weight %d is out of range [0, 100]", b.Weight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.HealthCheck != "" {
|
if b.HealthCheck != "" {
|
||||||
@@ -340,7 +353,6 @@ func convertFrontend(name string, r *rawFrontend, backends map[string]Backend) (
|
|||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
Protocol: r.Protocol,
|
Protocol: r.Protocol,
|
||||||
Port: r.Port,
|
Port: r.Port,
|
||||||
Backends: r.Backends,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(r.Address)
|
ip := net.ParseIP(r.Address)
|
||||||
@@ -361,21 +373,38 @@ func convertFrontend(name string, r *rawFrontend, backends map[string]Backend) (
|
|||||||
return Frontend{}, fmt.Errorf("protocol %q requires port to be set (1-65535)", r.Protocol)
|
return Frontend{}, fmt.Errorf("protocol %q requires port to be set (1-65535)", r.Protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(r.Backends) == 0 {
|
if len(r.Pools) == 0 {
|
||||||
return Frontend{}, fmt.Errorf("backends must not be empty")
|
return Frontend{}, fmt.Errorf("pools must not be empty")
|
||||||
}
|
}
|
||||||
var firstFamily int
|
var firstFamily int
|
||||||
for i, bName := range r.Backends {
|
firstBackend := true
|
||||||
|
for pi, rp := range r.Pools {
|
||||||
|
if rp.Name == "" {
|
||||||
|
return Frontend{}, fmt.Errorf("pools[%d].name must not be empty", pi)
|
||||||
|
}
|
||||||
|
if len(rp.Backends) == 0 {
|
||||||
|
return Frontend{}, fmt.Errorf("pool %q backends must not be empty", rp.Name)
|
||||||
|
}
|
||||||
|
pool := Pool{Name: rp.Name, Backends: make(map[string]PoolBackend, len(rp.Backends))}
|
||||||
|
for bName, rpb := range rp.Backends {
|
||||||
b, ok := backends[bName]
|
b, ok := backends[bName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return Frontend{}, fmt.Errorf("backends[%d] %q not defined", i, bName)
|
return Frontend{}, fmt.Errorf("pool %q backend %q not defined", rp.Name, bName)
|
||||||
}
|
}
|
||||||
fam := ipFamily(b.Address)
|
fam := ipFamily(b.Address)
|
||||||
if i == 0 {
|
if firstBackend {
|
||||||
firstFamily = fam
|
firstFamily = fam
|
||||||
|
firstBackend = false
|
||||||
} else if fam != firstFamily {
|
} else if fam != firstFamily {
|
||||||
return Frontend{}, fmt.Errorf("backends[%d] %q has different address family than backends[0]", i, bName)
|
return Frontend{}, fmt.Errorf("pool %q backend %q has different address family than first backend", rp.Name, bName)
|
||||||
}
|
}
|
||||||
|
w := intDefault(rpb.Weight, 100)
|
||||||
|
if w < 0 || w > 100 {
|
||||||
|
return Frontend{}, fmt.Errorf("pool %q backend %q weight %d out of range [0, 100]", rp.Name, bName, w)
|
||||||
|
}
|
||||||
|
pool.Backends[bName] = PoolBackend{Weight: w}
|
||||||
|
}
|
||||||
|
fe.Pools = append(fe.Pools, pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fe, nil
|
return fe, nil
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ maglev:
|
|||||||
be-v6b:
|
be-v6b:
|
||||||
address: 2001:db8:2::2
|
address: 2001:db8:2::2
|
||||||
healthcheck: icmp-check
|
healthcheck: icmp-check
|
||||||
weight: 50
|
|
||||||
enabled: true
|
enabled: true
|
||||||
frontends:
|
frontends:
|
||||||
web4:
|
web4:
|
||||||
@@ -49,13 +48,22 @@ maglev:
|
|||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
port: 80
|
port: 80
|
||||||
backends: [be-v4]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be-v4: {}
|
||||||
web6:
|
web6:
|
||||||
description: "IPv6 VIP"
|
description: "IPv6 VIP"
|
||||||
address: 2001:db8::1
|
address: 2001:db8::1
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
port: 443
|
port: 443
|
||||||
backends: [be-v6a, be-v6b]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be-v6a:
|
||||||
|
weight: 100
|
||||||
|
be-v6b:
|
||||||
|
weight: 50
|
||||||
`
|
`
|
||||||
|
|
||||||
func TestValidConfig(t *testing.T) {
|
func TestValidConfig(t *testing.T) {
|
||||||
@@ -106,7 +114,7 @@ func TestValidConfig(t *testing.T) {
|
|||||||
t.Errorf("icmp-check probe-ipv6-src: got %s, want 2001:db8:1::1", icmp.ProbeIPv6Src)
|
t.Errorf("icmp-check probe-ipv6-src: got %s, want 2001:db8:1::1", icmp.ProbeIPv6Src)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend defaults and explicit fields.
|
// Backend fields.
|
||||||
beV4 := cfg.Backends["be-v4"]
|
beV4 := cfg.Backends["be-v4"]
|
||||||
if beV4.Address.String() != "192.0.2.10" {
|
if beV4.Address.String() != "192.0.2.10" {
|
||||||
t.Errorf("be-v4 address: got %s", beV4.Address)
|
t.Errorf("be-v4 address: got %s", beV4.Address)
|
||||||
@@ -117,23 +125,25 @@ func TestValidConfig(t *testing.T) {
|
|||||||
if !beV4.Enabled {
|
if !beV4.Enabled {
|
||||||
t.Error("be-v4 enabled: want true (default)")
|
t.Error("be-v4 enabled: want true (default)")
|
||||||
}
|
}
|
||||||
if beV4.Weight != 100 {
|
|
||||||
t.Errorf("be-v4 weight: got %d, want 100 (default)", beV4.Weight)
|
|
||||||
}
|
|
||||||
|
|
||||||
beV6b := cfg.Backends["be-v6b"]
|
// Pool structure.
|
||||||
if beV6b.Weight != 50 {
|
|
||||||
t.Errorf("be-v6b weight: got %d, want 50", beV6b.Weight)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frontend references.
|
|
||||||
web4 := cfg.Frontends["web4"]
|
web4 := cfg.Frontends["web4"]
|
||||||
if len(web4.Backends) != 1 || web4.Backends[0] != "be-v4" {
|
if len(web4.Pools) != 1 || web4.Pools[0].Name != "primary" {
|
||||||
t.Errorf("web4 backends: got %v", web4.Backends)
|
t.Errorf("web4 pools: got %v", web4.Pools)
|
||||||
}
|
}
|
||||||
|
if _, ok := web4.Pools[0].Backends["be-v4"]; !ok {
|
||||||
|
t.Error("web4 primary pool missing be-v4")
|
||||||
|
}
|
||||||
|
if web4.Pools[0].Backends["be-v4"].Weight != 100 {
|
||||||
|
t.Errorf("web4 be-v4 weight: got %d, want 100 (default)", web4.Pools[0].Backends["be-v4"].Weight)
|
||||||
|
}
|
||||||
|
|
||||||
web6 := cfg.Frontends["web6"]
|
web6 := cfg.Frontends["web6"]
|
||||||
if len(web6.Backends) != 2 {
|
if len(web6.Pools) != 1 || len(web6.Pools[0].Backends) != 2 {
|
||||||
t.Errorf("web6 backends: got %d, want 2", len(web6.Backends))
|
t.Errorf("web6 pools[0] backends: got %d, want 2", len(web6.Pools[0].Backends))
|
||||||
|
}
|
||||||
|
if web6.Pools[0].Backends["be-v6b"].Weight != 50 {
|
||||||
|
t.Errorf("web6 be-v6b weight: got %d, want 50", web6.Pools[0].Backends["be-v6b"].Weight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +162,10 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
`
|
`
|
||||||
cfg, err := parse([]byte(raw))
|
cfg, err := parse([]byte(raw))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,8 +182,13 @@ maglev:
|
|||||||
t.Errorf("defaults rise/fall: got %d/%d, want 2/3", hc.Rise, hc.Fall)
|
t.Errorf("defaults rise/fall: got %d/%d, want 2/3", hc.Rise, hc.Fall)
|
||||||
}
|
}
|
||||||
be := cfg.Backends["be"]
|
be := cfg.Backends["be"]
|
||||||
if !be.Enabled || be.Weight != 100 {
|
if !be.Enabled {
|
||||||
t.Errorf("backend defaults: enabled=%v weight=%d", be.Enabled, be.Weight)
|
t.Errorf("backend default enabled: got false, want true")
|
||||||
|
}
|
||||||
|
// Pool backend weight defaults to 100.
|
||||||
|
v := cfg.Frontends["v"]
|
||||||
|
if v.Pools[0].Backends["be"].Weight != 100 {
|
||||||
|
t.Errorf("pool backend default weight: got %d, want 100", v.Pools[0].Backends["be"].Weight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +203,10 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
`
|
`
|
||||||
cfg, err := parse([]byte(raw))
|
cfg, err := parse([]byte(raw))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -213,7 +234,10 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
`
|
`
|
||||||
cfg, err := parse([]byte(raw))
|
cfg, err := parse([]byte(raw))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -249,7 +273,10 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
` + feExtra
|
` + feExtra
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +291,7 @@ maglev:
|
|||||||
errSub: "probe-ipv4-src",
|
errSub: "probe-ipv4-src",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mixed backend address families in frontend",
|
name: "mixed backend address families in pool",
|
||||||
yaml: `
|
yaml: `
|
||||||
maglev:
|
maglev:
|
||||||
healthchecks:
|
healthchecks:
|
||||||
@@ -278,7 +305,11 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [v4, v6]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
v4: {}
|
||||||
|
v6: {}
|
||||||
`,
|
`,
|
||||||
errSub: "address family",
|
errSub: "address family",
|
||||||
},
|
},
|
||||||
@@ -302,7 +333,10 @@ maglev:
|
|||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
`,
|
`,
|
||||||
errSub: "requires port",
|
errSub: "requires port",
|
||||||
},
|
},
|
||||||
@@ -320,7 +354,10 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
`,
|
`,
|
||||||
errSub: "type must be",
|
errSub: "type must be",
|
||||||
},
|
},
|
||||||
@@ -339,12 +376,15 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
`,
|
`,
|
||||||
errSub: "params.path",
|
errSub: "params.path",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "negative interval",
|
name: "no error case",
|
||||||
yaml: base("", "", ""),
|
yaml: base("", "", ""),
|
||||||
errSub: "",
|
errSub: "",
|
||||||
},
|
},
|
||||||
@@ -358,12 +398,15 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
`,
|
`,
|
||||||
errSub: "not defined",
|
errSub: "not defined",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "undefined backend reference in frontend",
|
name: "undefined backend reference in pool",
|
||||||
yaml: `
|
yaml: `
|
||||||
maglev:
|
maglev:
|
||||||
healthchecks:
|
healthchecks:
|
||||||
@@ -375,13 +418,33 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [missing]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
missing: {}
|
||||||
`,
|
`,
|
||||||
errSub: "not defined",
|
errSub: "not defined",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "weight out of range",
|
name: "pool weight out of range",
|
||||||
yaml: base("", " weight: 150\n", ""),
|
yaml: `
|
||||||
|
maglev:
|
||||||
|
healthchecks:
|
||||||
|
c:
|
||||||
|
type: icmp
|
||||||
|
interval: 1s
|
||||||
|
timeout: 2s
|
||||||
|
backends:
|
||||||
|
be: {address: 10.0.0.2, healthcheck: c}
|
||||||
|
frontends:
|
||||||
|
v:
|
||||||
|
address: 192.0.2.1
|
||||||
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be:
|
||||||
|
weight: 150
|
||||||
|
`,
|
||||||
errSub: "out of range",
|
errSub: "out of range",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -403,7 +466,10 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
`,
|
`,
|
||||||
errSub: "requires port",
|
errSub: "requires port",
|
||||||
},
|
},
|
||||||
@@ -423,10 +489,51 @@ maglev:
|
|||||||
frontends:
|
frontends:
|
||||||
v:
|
v:
|
||||||
address: 192.0.2.1
|
address: 192.0.2.1
|
||||||
backends: [be]
|
pools:
|
||||||
|
- name: primary
|
||||||
|
backends:
|
||||||
|
be: {}
|
||||||
`,
|
`,
|
||||||
errSub: "requires port",
|
errSub: "requires port",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "empty pools",
|
||||||
|
yaml: `
|
||||||
|
maglev:
|
||||||
|
healthchecks:
|
||||||
|
c:
|
||||||
|
type: icmp
|
||||||
|
interval: 1s
|
||||||
|
timeout: 2s
|
||||||
|
backends:
|
||||||
|
be: {address: 10.0.0.2, healthcheck: c}
|
||||||
|
frontends:
|
||||||
|
v:
|
||||||
|
address: 192.0.2.1
|
||||||
|
pools: []
|
||||||
|
`,
|
||||||
|
errSub: "pools must not be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pool missing name",
|
||||||
|
yaml: `
|
||||||
|
maglev:
|
||||||
|
healthchecks:
|
||||||
|
c:
|
||||||
|
type: icmp
|
||||||
|
interval: 1s
|
||||||
|
timeout: 2s
|
||||||
|
backends:
|
||||||
|
be: {address: 10.0.0.2, healthcheck: c}
|
||||||
|
frontends:
|
||||||
|
v:
|
||||||
|
address: 192.0.2.1
|
||||||
|
pools:
|
||||||
|
- backends:
|
||||||
|
be: {}
|
||||||
|
`,
|
||||||
|
errSub: "name must not be empty",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -385,13 +385,117 @@ func (x *ListFrontendsResponse) GetFrontendNames() []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PoolBackendInfo struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Weight int32 `protobuf:"varint,2,opt,name=weight,proto3" json:"weight,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PoolBackendInfo) Reset() {
|
||||||
|
*x = PoolBackendInfo{}
|
||||||
|
mi := &file_proto_maglev_proto_msgTypes[9]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PoolBackendInfo) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*PoolBackendInfo) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *PoolBackendInfo) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_maglev_proto_msgTypes[9]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use PoolBackendInfo.ProtoReflect.Descriptor instead.
|
||||||
|
func (*PoolBackendInfo) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_maglev_proto_rawDescGZIP(), []int{9}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PoolBackendInfo) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PoolBackendInfo) GetWeight() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Weight
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type PoolInfo struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Backends []*PoolBackendInfo `protobuf:"bytes,2,rep,name=backends,proto3" json:"backends,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PoolInfo) Reset() {
|
||||||
|
*x = PoolInfo{}
|
||||||
|
mi := &file_proto_maglev_proto_msgTypes[10]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PoolInfo) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*PoolInfo) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *PoolInfo) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_maglev_proto_msgTypes[10]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use PoolInfo.ProtoReflect.Descriptor instead.
|
||||||
|
func (*PoolInfo) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_maglev_proto_rawDescGZIP(), []int{10}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PoolInfo) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PoolInfo) GetBackends() []*PoolBackendInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Backends
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type FrontendInfo struct {
|
type FrontendInfo struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
|
Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
|
||||||
Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||||
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
|
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
|
||||||
BackendNames []string `protobuf:"bytes,5,rep,name=backend_names,json=backendNames,proto3" json:"backend_names,omitempty"`
|
Pools []*PoolInfo `protobuf:"bytes,5,rep,name=pools,proto3" json:"pools,omitempty"`
|
||||||
Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"`
|
Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -399,7 +503,7 @@ type FrontendInfo struct {
|
|||||||
|
|
||||||
func (x *FrontendInfo) Reset() {
|
func (x *FrontendInfo) Reset() {
|
||||||
*x = FrontendInfo{}
|
*x = FrontendInfo{}
|
||||||
mi := &file_proto_maglev_proto_msgTypes[9]
|
mi := &file_proto_maglev_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -411,7 +515,7 @@ func (x *FrontendInfo) String() string {
|
|||||||
func (*FrontendInfo) ProtoMessage() {}
|
func (*FrontendInfo) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *FrontendInfo) ProtoReflect() protoreflect.Message {
|
func (x *FrontendInfo) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_maglev_proto_msgTypes[9]
|
mi := &file_proto_maglev_proto_msgTypes[11]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -424,7 +528,7 @@ func (x *FrontendInfo) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use FrontendInfo.ProtoReflect.Descriptor instead.
|
// Deprecated: Use FrontendInfo.ProtoReflect.Descriptor instead.
|
||||||
func (*FrontendInfo) Descriptor() ([]byte, []int) {
|
func (*FrontendInfo) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_maglev_proto_rawDescGZIP(), []int{9}
|
return file_proto_maglev_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *FrontendInfo) GetName() string {
|
func (x *FrontendInfo) GetName() string {
|
||||||
@@ -455,9 +559,9 @@ func (x *FrontendInfo) GetPort() uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *FrontendInfo) GetBackendNames() []string {
|
func (x *FrontendInfo) GetPools() []*PoolInfo {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.BackendNames
|
return x.Pools
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -478,7 +582,7 @@ type ListBackendsResponse struct {
|
|||||||
|
|
||||||
func (x *ListBackendsResponse) Reset() {
|
func (x *ListBackendsResponse) Reset() {
|
||||||
*x = ListBackendsResponse{}
|
*x = ListBackendsResponse{}
|
||||||
mi := &file_proto_maglev_proto_msgTypes[10]
|
mi := &file_proto_maglev_proto_msgTypes[12]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -490,7 +594,7 @@ func (x *ListBackendsResponse) String() string {
|
|||||||
func (*ListBackendsResponse) ProtoMessage() {}
|
func (*ListBackendsResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListBackendsResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListBackendsResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_maglev_proto_msgTypes[10]
|
mi := &file_proto_maglev_proto_msgTypes[12]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -503,7 +607,7 @@ func (x *ListBackendsResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListBackendsResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListBackendsResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ListBackendsResponse) Descriptor() ([]byte, []int) {
|
func (*ListBackendsResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_maglev_proto_rawDescGZIP(), []int{10}
|
return file_proto_maglev_proto_rawDescGZIP(), []int{12}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListBackendsResponse) GetBackendNames() []string {
|
func (x *ListBackendsResponse) GetBackendNames() []string {
|
||||||
@@ -522,7 +626,7 @@ type ListHealthChecksResponse struct {
|
|||||||
|
|
||||||
func (x *ListHealthChecksResponse) Reset() {
|
func (x *ListHealthChecksResponse) Reset() {
|
||||||
*x = ListHealthChecksResponse{}
|
*x = ListHealthChecksResponse{}
|
||||||
mi := &file_proto_maglev_proto_msgTypes[11]
|
mi := &file_proto_maglev_proto_msgTypes[13]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -534,7 +638,7 @@ func (x *ListHealthChecksResponse) String() string {
|
|||||||
func (*ListHealthChecksResponse) ProtoMessage() {}
|
func (*ListHealthChecksResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ListHealthChecksResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListHealthChecksResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_maglev_proto_msgTypes[11]
|
mi := &file_proto_maglev_proto_msgTypes[13]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -547,7 +651,7 @@ func (x *ListHealthChecksResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ListHealthChecksResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ListHealthChecksResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ListHealthChecksResponse) Descriptor() ([]byte, []int) {
|
func (*ListHealthChecksResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_maglev_proto_rawDescGZIP(), []int{11}
|
return file_proto_maglev_proto_rawDescGZIP(), []int{13}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListHealthChecksResponse) GetNames() []string {
|
func (x *ListHealthChecksResponse) GetNames() []string {
|
||||||
@@ -572,7 +676,7 @@ type HTTPCheckParams struct {
|
|||||||
|
|
||||||
func (x *HTTPCheckParams) Reset() {
|
func (x *HTTPCheckParams) Reset() {
|
||||||
*x = HTTPCheckParams{}
|
*x = HTTPCheckParams{}
|
||||||
mi := &file_proto_maglev_proto_msgTypes[12]
|
mi := &file_proto_maglev_proto_msgTypes[14]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -584,7 +688,7 @@ func (x *HTTPCheckParams) String() string {
|
|||||||
func (*HTTPCheckParams) ProtoMessage() {}
|
func (*HTTPCheckParams) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *HTTPCheckParams) ProtoReflect() protoreflect.Message {
|
func (x *HTTPCheckParams) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_maglev_proto_msgTypes[12]
|
mi := &file_proto_maglev_proto_msgTypes[14]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -597,7 +701,7 @@ func (x *HTTPCheckParams) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use HTTPCheckParams.ProtoReflect.Descriptor instead.
|
// Deprecated: Use HTTPCheckParams.ProtoReflect.Descriptor instead.
|
||||||
func (*HTTPCheckParams) Descriptor() ([]byte, []int) {
|
func (*HTTPCheckParams) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_maglev_proto_rawDescGZIP(), []int{12}
|
return file_proto_maglev_proto_rawDescGZIP(), []int{14}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *HTTPCheckParams) GetPath() string {
|
func (x *HTTPCheckParams) GetPath() string {
|
||||||
@@ -660,7 +764,7 @@ type TCPCheckParams struct {
|
|||||||
|
|
||||||
func (x *TCPCheckParams) Reset() {
|
func (x *TCPCheckParams) Reset() {
|
||||||
*x = TCPCheckParams{}
|
*x = TCPCheckParams{}
|
||||||
mi := &file_proto_maglev_proto_msgTypes[13]
|
mi := &file_proto_maglev_proto_msgTypes[15]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -672,7 +776,7 @@ func (x *TCPCheckParams) String() string {
|
|||||||
func (*TCPCheckParams) ProtoMessage() {}
|
func (*TCPCheckParams) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *TCPCheckParams) ProtoReflect() protoreflect.Message {
|
func (x *TCPCheckParams) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_maglev_proto_msgTypes[13]
|
mi := &file_proto_maglev_proto_msgTypes[15]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -685,7 +789,7 @@ func (x *TCPCheckParams) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use TCPCheckParams.ProtoReflect.Descriptor instead.
|
// Deprecated: Use TCPCheckParams.ProtoReflect.Descriptor instead.
|
||||||
func (*TCPCheckParams) Descriptor() ([]byte, []int) {
|
func (*TCPCheckParams) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_maglev_proto_rawDescGZIP(), []int{13}
|
return file_proto_maglev_proto_rawDescGZIP(), []int{15}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *TCPCheckParams) GetSsl() bool {
|
func (x *TCPCheckParams) GetSsl() bool {
|
||||||
@@ -730,7 +834,7 @@ type HealthCheckInfo struct {
|
|||||||
|
|
||||||
func (x *HealthCheckInfo) Reset() {
|
func (x *HealthCheckInfo) Reset() {
|
||||||
*x = HealthCheckInfo{}
|
*x = HealthCheckInfo{}
|
||||||
mi := &file_proto_maglev_proto_msgTypes[14]
|
mi := &file_proto_maglev_proto_msgTypes[16]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -742,7 +846,7 @@ func (x *HealthCheckInfo) String() string {
|
|||||||
func (*HealthCheckInfo) ProtoMessage() {}
|
func (*HealthCheckInfo) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *HealthCheckInfo) ProtoReflect() protoreflect.Message {
|
func (x *HealthCheckInfo) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_maglev_proto_msgTypes[14]
|
mi := &file_proto_maglev_proto_msgTypes[16]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -755,7 +859,7 @@ func (x *HealthCheckInfo) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use HealthCheckInfo.ProtoReflect.Descriptor instead.
|
// Deprecated: Use HealthCheckInfo.ProtoReflect.Descriptor instead.
|
||||||
func (*HealthCheckInfo) Descriptor() ([]byte, []int) {
|
func (*HealthCheckInfo) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_maglev_proto_rawDescGZIP(), []int{14}
|
return file_proto_maglev_proto_rawDescGZIP(), []int{16}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *HealthCheckInfo) GetName() string {
|
func (x *HealthCheckInfo) GetName() string {
|
||||||
@@ -856,15 +960,14 @@ type BackendInfo struct {
|
|||||||
State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"`
|
State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"`
|
||||||
Transitions []*TransitionRecord `protobuf:"bytes,4,rep,name=transitions,proto3" json:"transitions,omitempty"`
|
Transitions []*TransitionRecord `protobuf:"bytes,4,rep,name=transitions,proto3" json:"transitions,omitempty"`
|
||||||
Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||||
Weight int32 `protobuf:"varint,6,opt,name=weight,proto3" json:"weight,omitempty"`
|
Healthcheck string `protobuf:"bytes,6,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"`
|
||||||
Healthcheck string `protobuf:"bytes,7,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *BackendInfo) Reset() {
|
func (x *BackendInfo) Reset() {
|
||||||
*x = BackendInfo{}
|
*x = BackendInfo{}
|
||||||
mi := &file_proto_maglev_proto_msgTypes[15]
|
mi := &file_proto_maglev_proto_msgTypes[17]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -876,7 +979,7 @@ func (x *BackendInfo) String() string {
|
|||||||
func (*BackendInfo) ProtoMessage() {}
|
func (*BackendInfo) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *BackendInfo) ProtoReflect() protoreflect.Message {
|
func (x *BackendInfo) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_maglev_proto_msgTypes[15]
|
mi := &file_proto_maglev_proto_msgTypes[17]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -889,7 +992,7 @@ func (x *BackendInfo) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use BackendInfo.ProtoReflect.Descriptor instead.
|
// Deprecated: Use BackendInfo.ProtoReflect.Descriptor instead.
|
||||||
func (*BackendInfo) Descriptor() ([]byte, []int) {
|
func (*BackendInfo) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_maglev_proto_rawDescGZIP(), []int{15}
|
return file_proto_maglev_proto_rawDescGZIP(), []int{17}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *BackendInfo) GetName() string {
|
func (x *BackendInfo) GetName() string {
|
||||||
@@ -927,13 +1030,6 @@ func (x *BackendInfo) GetEnabled() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *BackendInfo) GetWeight() int32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.Weight
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *BackendInfo) GetHealthcheck() string {
|
func (x *BackendInfo) GetHealthcheck() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Healthcheck
|
return x.Healthcheck
|
||||||
@@ -952,7 +1048,7 @@ type TransitionRecord struct {
|
|||||||
|
|
||||||
func (x *TransitionRecord) Reset() {
|
func (x *TransitionRecord) Reset() {
|
||||||
*x = TransitionRecord{}
|
*x = TransitionRecord{}
|
||||||
mi := &file_proto_maglev_proto_msgTypes[16]
|
mi := &file_proto_maglev_proto_msgTypes[18]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -964,7 +1060,7 @@ func (x *TransitionRecord) String() string {
|
|||||||
func (*TransitionRecord) ProtoMessage() {}
|
func (*TransitionRecord) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *TransitionRecord) ProtoReflect() protoreflect.Message {
|
func (x *TransitionRecord) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_maglev_proto_msgTypes[16]
|
mi := &file_proto_maglev_proto_msgTypes[18]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -977,7 +1073,7 @@ func (x *TransitionRecord) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use TransitionRecord.ProtoReflect.Descriptor instead.
|
// Deprecated: Use TransitionRecord.ProtoReflect.Descriptor instead.
|
||||||
func (*TransitionRecord) Descriptor() ([]byte, []int) {
|
func (*TransitionRecord) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_maglev_proto_rawDescGZIP(), []int{16}
|
return file_proto_maglev_proto_rawDescGZIP(), []int{18}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *TransitionRecord) GetFrom() string {
|
func (x *TransitionRecord) GetFrom() string {
|
||||||
@@ -1011,7 +1107,7 @@ type BackendEvent struct {
|
|||||||
|
|
||||||
func (x *BackendEvent) Reset() {
|
func (x *BackendEvent) Reset() {
|
||||||
*x = BackendEvent{}
|
*x = BackendEvent{}
|
||||||
mi := &file_proto_maglev_proto_msgTypes[17]
|
mi := &file_proto_maglev_proto_msgTypes[19]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -1023,7 +1119,7 @@ func (x *BackendEvent) String() string {
|
|||||||
func (*BackendEvent) ProtoMessage() {}
|
func (*BackendEvent) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *BackendEvent) ProtoReflect() protoreflect.Message {
|
func (x *BackendEvent) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_maglev_proto_msgTypes[17]
|
mi := &file_proto_maglev_proto_msgTypes[19]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -1036,7 +1132,7 @@ func (x *BackendEvent) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use BackendEvent.ProtoReflect.Descriptor instead.
|
// Deprecated: Use BackendEvent.ProtoReflect.Descriptor instead.
|
||||||
func (*BackendEvent) Descriptor() ([]byte, []int) {
|
func (*BackendEvent) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_maglev_proto_rawDescGZIP(), []int{17}
|
return file_proto_maglev_proto_rawDescGZIP(), []int{19}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *BackendEvent) GetBackendName() string {
|
func (x *BackendEvent) GetBackendName() string {
|
||||||
@@ -1071,13 +1167,19 @@ const file_proto_maglev_proto_rawDesc = "" +
|
|||||||
"\x04name\x18\x01 \x01(\tR\x04name\"\x0e\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\"\x0e\n" +
|
||||||
"\fWatchRequest\">\n" +
|
"\fWatchRequest\">\n" +
|
||||||
"\x15ListFrontendsResponse\x12%\n" +
|
"\x15ListFrontendsResponse\x12%\n" +
|
||||||
"\x0efrontend_names\x18\x01 \x03(\tR\rfrontendNames\"\xb3\x01\n" +
|
"\x0efrontend_names\x18\x01 \x03(\tR\rfrontendNames\"=\n" +
|
||||||
|
"\x0fPoolBackendInfo\x12\x12\n" +
|
||||||
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" +
|
||||||
|
"\x06weight\x18\x02 \x01(\x05R\x06weight\"S\n" +
|
||||||
|
"\bPoolInfo\x12\x12\n" +
|
||||||
|
"\x04name\x18\x01 \x01(\tR\x04name\x123\n" +
|
||||||
|
"\bbackends\x18\x02 \x03(\v2\x17.maglev.PoolBackendInfoR\bbackends\"\xb6\x01\n" +
|
||||||
"\fFrontendInfo\x12\x12\n" +
|
"\fFrontendInfo\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
|
||||||
"\aaddress\x18\x02 \x01(\tR\aaddress\x12\x1a\n" +
|
"\aaddress\x18\x02 \x01(\tR\aaddress\x12\x1a\n" +
|
||||||
"\bprotocol\x18\x03 \x01(\tR\bprotocol\x12\x12\n" +
|
"\bprotocol\x18\x03 \x01(\tR\bprotocol\x12\x12\n" +
|
||||||
"\x04port\x18\x04 \x01(\rR\x04port\x12#\n" +
|
"\x04port\x18\x04 \x01(\rR\x04port\x12&\n" +
|
||||||
"\rbackend_names\x18\x05 \x03(\tR\fbackendNames\x12 \n" +
|
"\x05pools\x18\x05 \x03(\v2\x10.maglev.PoolInfoR\x05pools\x12 \n" +
|
||||||
"\vdescription\x18\x06 \x01(\tR\vdescription\";\n" +
|
"\vdescription\x18\x06 \x01(\tR\vdescription\";\n" +
|
||||||
"\x14ListBackendsResponse\x12#\n" +
|
"\x14ListBackendsResponse\x12#\n" +
|
||||||
"\rbackend_names\x18\x01 \x03(\tR\fbackendNames\"0\n" +
|
"\rbackend_names\x18\x01 \x03(\tR\fbackendNames\"0\n" +
|
||||||
@@ -1113,15 +1215,14 @@ const file_proto_maglev_proto_rawDesc = "" +
|
|||||||
" \x01(\x05R\x04rise\x12\x12\n" +
|
" \x01(\x05R\x04rise\x12\x12\n" +
|
||||||
"\x04fall\x18\v \x01(\x05R\x04fall\x12+\n" +
|
"\x04fall\x18\v \x01(\x05R\x04fall\x12+\n" +
|
||||||
"\x04http\x18\f \x01(\v2\x17.maglev.HTTPCheckParamsR\x04http\x12(\n" +
|
"\x04http\x18\f \x01(\v2\x17.maglev.HTTPCheckParamsR\x04http\x12(\n" +
|
||||||
"\x03tcp\x18\r \x01(\v2\x16.maglev.TCPCheckParamsR\x03tcp\"\xe1\x01\n" +
|
"\x03tcp\x18\r \x01(\v2\x16.maglev.TCPCheckParamsR\x03tcp\"\xc9\x01\n" +
|
||||||
"\vBackendInfo\x12\x12\n" +
|
"\vBackendInfo\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
|
||||||
"\aaddress\x18\x02 \x01(\tR\aaddress\x12\x14\n" +
|
"\aaddress\x18\x02 \x01(\tR\aaddress\x12\x14\n" +
|
||||||
"\x05state\x18\x03 \x01(\tR\x05state\x12:\n" +
|
"\x05state\x18\x03 \x01(\tR\x05state\x12:\n" +
|
||||||
"\vtransitions\x18\x04 \x03(\v2\x18.maglev.TransitionRecordR\vtransitions\x12\x18\n" +
|
"\vtransitions\x18\x04 \x03(\v2\x18.maglev.TransitionRecordR\vtransitions\x12\x18\n" +
|
||||||
"\aenabled\x18\x05 \x01(\bR\aenabled\x12\x16\n" +
|
"\aenabled\x18\x05 \x01(\bR\aenabled\x12 \n" +
|
||||||
"\x06weight\x18\x06 \x01(\x05R\x06weight\x12 \n" +
|
"\vhealthcheck\x18\x06 \x01(\tR\vhealthcheck\"T\n" +
|
||||||
"\vhealthcheck\x18\a \x01(\tR\vhealthcheck\"T\n" +
|
|
||||||
"\x10TransitionRecord\x12\x12\n" +
|
"\x10TransitionRecord\x12\x12\n" +
|
||||||
"\x04from\x18\x01 \x01(\tR\x04from\x12\x0e\n" +
|
"\x04from\x18\x01 \x01(\tR\x04from\x12\x0e\n" +
|
||||||
"\x02to\x18\x02 \x01(\tR\x02to\x12\x1c\n" +
|
"\x02to\x18\x02 \x01(\tR\x02to\x12\x1c\n" +
|
||||||
@@ -1156,7 +1257,7 @@ func file_proto_maglev_proto_rawDescGZIP() []byte {
|
|||||||
return file_proto_maglev_proto_rawDescData
|
return file_proto_maglev_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_proto_maglev_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
|
var file_proto_maglev_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
|
||||||
var file_proto_maglev_proto_goTypes = []any{
|
var file_proto_maglev_proto_goTypes = []any{
|
||||||
(*ListFrontendsRequest)(nil), // 0: maglev.ListFrontendsRequest
|
(*ListFrontendsRequest)(nil), // 0: maglev.ListFrontendsRequest
|
||||||
(*GetFrontendRequest)(nil), // 1: maglev.GetFrontendRequest
|
(*GetFrontendRequest)(nil), // 1: maglev.GetFrontendRequest
|
||||||
@@ -1167,44 +1268,48 @@ var file_proto_maglev_proto_goTypes = []any{
|
|||||||
(*GetHealthCheckRequest)(nil), // 6: maglev.GetHealthCheckRequest
|
(*GetHealthCheckRequest)(nil), // 6: maglev.GetHealthCheckRequest
|
||||||
(*WatchRequest)(nil), // 7: maglev.WatchRequest
|
(*WatchRequest)(nil), // 7: maglev.WatchRequest
|
||||||
(*ListFrontendsResponse)(nil), // 8: maglev.ListFrontendsResponse
|
(*ListFrontendsResponse)(nil), // 8: maglev.ListFrontendsResponse
|
||||||
(*FrontendInfo)(nil), // 9: maglev.FrontendInfo
|
(*PoolBackendInfo)(nil), // 9: maglev.PoolBackendInfo
|
||||||
(*ListBackendsResponse)(nil), // 10: maglev.ListBackendsResponse
|
(*PoolInfo)(nil), // 10: maglev.PoolInfo
|
||||||
(*ListHealthChecksResponse)(nil), // 11: maglev.ListHealthChecksResponse
|
(*FrontendInfo)(nil), // 11: maglev.FrontendInfo
|
||||||
(*HTTPCheckParams)(nil), // 12: maglev.HTTPCheckParams
|
(*ListBackendsResponse)(nil), // 12: maglev.ListBackendsResponse
|
||||||
(*TCPCheckParams)(nil), // 13: maglev.TCPCheckParams
|
(*ListHealthChecksResponse)(nil), // 13: maglev.ListHealthChecksResponse
|
||||||
(*HealthCheckInfo)(nil), // 14: maglev.HealthCheckInfo
|
(*HTTPCheckParams)(nil), // 14: maglev.HTTPCheckParams
|
||||||
(*BackendInfo)(nil), // 15: maglev.BackendInfo
|
(*TCPCheckParams)(nil), // 15: maglev.TCPCheckParams
|
||||||
(*TransitionRecord)(nil), // 16: maglev.TransitionRecord
|
(*HealthCheckInfo)(nil), // 16: maglev.HealthCheckInfo
|
||||||
(*BackendEvent)(nil), // 17: maglev.BackendEvent
|
(*BackendInfo)(nil), // 17: maglev.BackendInfo
|
||||||
|
(*TransitionRecord)(nil), // 18: maglev.TransitionRecord
|
||||||
|
(*BackendEvent)(nil), // 19: maglev.BackendEvent
|
||||||
}
|
}
|
||||||
var file_proto_maglev_proto_depIdxs = []int32{
|
var file_proto_maglev_proto_depIdxs = []int32{
|
||||||
12, // 0: maglev.HealthCheckInfo.http:type_name -> maglev.HTTPCheckParams
|
9, // 0: maglev.PoolInfo.backends:type_name -> maglev.PoolBackendInfo
|
||||||
13, // 1: maglev.HealthCheckInfo.tcp:type_name -> maglev.TCPCheckParams
|
10, // 1: maglev.FrontendInfo.pools:type_name -> maglev.PoolInfo
|
||||||
16, // 2: maglev.BackendInfo.transitions:type_name -> maglev.TransitionRecord
|
14, // 2: maglev.HealthCheckInfo.http:type_name -> maglev.HTTPCheckParams
|
||||||
16, // 3: maglev.BackendEvent.transition:type_name -> maglev.TransitionRecord
|
15, // 3: maglev.HealthCheckInfo.tcp:type_name -> maglev.TCPCheckParams
|
||||||
0, // 4: maglev.Maglev.ListFrontends:input_type -> maglev.ListFrontendsRequest
|
18, // 4: maglev.BackendInfo.transitions:type_name -> maglev.TransitionRecord
|
||||||
1, // 5: maglev.Maglev.GetFrontend:input_type -> maglev.GetFrontendRequest
|
18, // 5: maglev.BackendEvent.transition:type_name -> maglev.TransitionRecord
|
||||||
2, // 6: maglev.Maglev.ListBackends:input_type -> maglev.ListBackendsRequest
|
0, // 6: maglev.Maglev.ListFrontends:input_type -> maglev.ListFrontendsRequest
|
||||||
3, // 7: maglev.Maglev.GetBackend:input_type -> maglev.GetBackendRequest
|
1, // 7: maglev.Maglev.GetFrontend:input_type -> maglev.GetFrontendRequest
|
||||||
4, // 8: maglev.Maglev.PauseBackend:input_type -> maglev.PauseResumeRequest
|
2, // 8: maglev.Maglev.ListBackends:input_type -> maglev.ListBackendsRequest
|
||||||
4, // 9: maglev.Maglev.ResumeBackend:input_type -> maglev.PauseResumeRequest
|
3, // 9: maglev.Maglev.GetBackend:input_type -> maglev.GetBackendRequest
|
||||||
5, // 10: maglev.Maglev.ListHealthChecks:input_type -> maglev.ListHealthChecksRequest
|
4, // 10: maglev.Maglev.PauseBackend:input_type -> maglev.PauseResumeRequest
|
||||||
6, // 11: maglev.Maglev.GetHealthCheck:input_type -> maglev.GetHealthCheckRequest
|
4, // 11: maglev.Maglev.ResumeBackend:input_type -> maglev.PauseResumeRequest
|
||||||
7, // 12: maglev.Maglev.WatchBackendEvents:input_type -> maglev.WatchRequest
|
5, // 12: maglev.Maglev.ListHealthChecks:input_type -> maglev.ListHealthChecksRequest
|
||||||
8, // 13: maglev.Maglev.ListFrontends:output_type -> maglev.ListFrontendsResponse
|
6, // 13: maglev.Maglev.GetHealthCheck:input_type -> maglev.GetHealthCheckRequest
|
||||||
9, // 14: maglev.Maglev.GetFrontend:output_type -> maglev.FrontendInfo
|
7, // 14: maglev.Maglev.WatchBackendEvents:input_type -> maglev.WatchRequest
|
||||||
10, // 15: maglev.Maglev.ListBackends:output_type -> maglev.ListBackendsResponse
|
8, // 15: maglev.Maglev.ListFrontends:output_type -> maglev.ListFrontendsResponse
|
||||||
15, // 16: maglev.Maglev.GetBackend:output_type -> maglev.BackendInfo
|
11, // 16: maglev.Maglev.GetFrontend:output_type -> maglev.FrontendInfo
|
||||||
15, // 17: maglev.Maglev.PauseBackend:output_type -> maglev.BackendInfo
|
12, // 17: maglev.Maglev.ListBackends:output_type -> maglev.ListBackendsResponse
|
||||||
15, // 18: maglev.Maglev.ResumeBackend:output_type -> maglev.BackendInfo
|
17, // 18: maglev.Maglev.GetBackend:output_type -> maglev.BackendInfo
|
||||||
11, // 19: maglev.Maglev.ListHealthChecks:output_type -> maglev.ListHealthChecksResponse
|
17, // 19: maglev.Maglev.PauseBackend:output_type -> maglev.BackendInfo
|
||||||
14, // 20: maglev.Maglev.GetHealthCheck:output_type -> maglev.HealthCheckInfo
|
17, // 20: maglev.Maglev.ResumeBackend:output_type -> maglev.BackendInfo
|
||||||
17, // 21: maglev.Maglev.WatchBackendEvents:output_type -> maglev.BackendEvent
|
13, // 21: maglev.Maglev.ListHealthChecks:output_type -> maglev.ListHealthChecksResponse
|
||||||
13, // [13:22] is the sub-list for method output_type
|
16, // 22: maglev.Maglev.GetHealthCheck:output_type -> maglev.HealthCheckInfo
|
||||||
4, // [4:13] is the sub-list for method input_type
|
19, // 23: maglev.Maglev.WatchBackendEvents:output_type -> maglev.BackendEvent
|
||||||
4, // [4:4] is the sub-list for extension type_name
|
15, // [15:24] is the sub-list for method output_type
|
||||||
4, // [4:4] is the sub-list for extension extendee
|
6, // [6:15] is the sub-list for method input_type
|
||||||
0, // [0:4] is the sub-list for field type_name
|
6, // [6:6] is the sub-list for extension type_name
|
||||||
|
6, // [6:6] is the sub-list for extension extendee
|
||||||
|
0, // [0:6] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_proto_maglev_proto_init() }
|
func init() { file_proto_maglev_proto_init() }
|
||||||
@@ -1218,7 +1323,7 @@ func file_proto_maglev_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_maglev_proto_rawDesc), len(file_proto_maglev_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_maglev_proto_rawDesc), len(file_proto_maglev_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 18,
|
NumMessages: 20,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -137,13 +137,24 @@ func (s *Server) WatchBackendEvents(_ *WatchRequest, stream Maglev_WatchBackendE
|
|||||||
// ---- conversion helpers ----------------------------------------------------
|
// ---- conversion helpers ----------------------------------------------------
|
||||||
|
|
||||||
func frontendToProto(name string, fe config.Frontend) *FrontendInfo {
|
func frontendToProto(name string, fe config.Frontend) *FrontendInfo {
|
||||||
|
pools := make([]*PoolInfo, 0, len(fe.Pools))
|
||||||
|
for _, p := range fe.Pools {
|
||||||
|
pi := &PoolInfo{Name: p.Name}
|
||||||
|
for bName, pb := range p.Backends {
|
||||||
|
pi.Backends = append(pi.Backends, &PoolBackendInfo{
|
||||||
|
Name: bName,
|
||||||
|
Weight: int32(pb.Weight),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pools = append(pools, pi)
|
||||||
|
}
|
||||||
return &FrontendInfo{
|
return &FrontendInfo{
|
||||||
Name: name,
|
Name: name,
|
||||||
Address: fe.Address.String(),
|
Address: fe.Address.String(),
|
||||||
Protocol: fe.Protocol,
|
Protocol: fe.Protocol,
|
||||||
Port: uint32(fe.Port),
|
Port: uint32(fe.Port),
|
||||||
Description: fe.Description,
|
Description: fe.Description,
|
||||||
BackendNames: fe.Backends,
|
Pools: pools,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +164,6 @@ func backendToProto(snap checker.BackendSnapshot) *BackendInfo {
|
|||||||
Address: snap.Health.Address.String(),
|
Address: snap.Health.Address.String(),
|
||||||
State: snap.Health.State.String(),
|
State: snap.Health.State.String(),
|
||||||
Enabled: snap.Config.Enabled,
|
Enabled: snap.Config.Enabled,
|
||||||
Weight: int32(snap.Config.Weight),
|
|
||||||
Healthcheck: snap.Config.HealthCheck,
|
Healthcheck: snap.Config.HealthCheck,
|
||||||
}
|
}
|
||||||
for _, t := range snap.Health.Transitions {
|
for _, t := range snap.Health.Transitions {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ func makeTestChecker(ctx context.Context) *checker.Checker {
|
|||||||
Address: net.ParseIP("10.0.0.2"),
|
Address: net.ParseIP("10.0.0.2"),
|
||||||
HealthCheck: "icmp",
|
HealthCheck: "icmp",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Weight: 100,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Frontends: map[string]config.Frontend{
|
Frontends: map[string]config.Frontend{
|
||||||
@@ -41,7 +40,11 @@ func makeTestChecker(ctx context.Context) *checker.Checker {
|
|||||||
Address: net.ParseIP("192.0.2.1"),
|
Address: net.ParseIP("192.0.2.1"),
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
Port: 80,
|
Port: 80,
|
||||||
Backends: []string{"be0"},
|
Pools: []config.Pool{
|
||||||
|
{Name: "primary", Backends: map[string]config.PoolBackend{
|
||||||
|
"be0": {Weight: 100},
|
||||||
|
}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -108,8 +111,14 @@ func TestGetFrontend(t *testing.T) {
|
|||||||
if info.Port != 80 {
|
if info.Port != 80 {
|
||||||
t.Errorf("GetFrontend port: got %d, want 80", info.Port)
|
t.Errorf("GetFrontend port: got %d, want 80", info.Port)
|
||||||
}
|
}
|
||||||
if len(info.BackendNames) != 1 || info.BackendNames[0] != "be0" {
|
if len(info.Pools) != 1 || info.Pools[0].Name != "primary" {
|
||||||
t.Errorf("GetFrontend backend_names: got %v, want [be0]", info.BackendNames)
|
t.Errorf("GetFrontend pools: got %v, want [{primary [be0]}]", info.Pools)
|
||||||
|
}
|
||||||
|
if len(info.Pools[0].Backends) != 1 || info.Pools[0].Backends[0].Name != "be0" {
|
||||||
|
t.Errorf("GetFrontend pools[0].backends: got %v, want [{be0 100}]", info.Pools[0].Backends)
|
||||||
|
}
|
||||||
|
if info.Pools[0].Backends[0].Weight != 100 {
|
||||||
|
t.Errorf("GetFrontend pools[0].backends[0].weight: got %d, want 100", info.Pools[0].Backends[0].Weight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,9 +171,6 @@ func TestGetBackend(t *testing.T) {
|
|||||||
if !info.Enabled {
|
if !info.Enabled {
|
||||||
t.Error("expected enabled=true")
|
t.Error("expected enabled=true")
|
||||||
}
|
}
|
||||||
if info.Weight != 100 {
|
|
||||||
t.Errorf("weight: got %d, want 100", info.Weight)
|
|
||||||
}
|
|
||||||
if info.Healthcheck != "icmp" {
|
if info.Healthcheck != "icmp" {
|
||||||
t.Errorf("healthcheck: got %q, want icmp", info.Healthcheck)
|
t.Errorf("healthcheck: got %q, want icmp", info.Healthcheck)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ func doHTTPProbe(ctx context.Context, cfg ProbeConfig, useTLS bool) health.Probe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scheme := "http"
|
// Always use "http" scheme: TLS (if any) is already applied to conn during
|
||||||
if useTLS {
|
// the netns dial phase. Using "https" here would cause http.Transport to
|
||||||
scheme = "https"
|
// wrap conn in a second TLS layer, producing "http: server gave HTTP
|
||||||
}
|
// response to HTTPS client".
|
||||||
target := fmt.Sprintf("%s://%s%s", scheme, net.JoinHostPort(cfg.Target.String(), strconv.Itoa(int(port))), p.Path)
|
target := fmt.Sprintf("http://%s%s", net.JoinHostPort(cfg.Target.String(), strconv.Itoa(int(port))), p.Path)
|
||||||
|
|
||||||
hostHeader := p.Host
|
hostHeader := p.Host
|
||||||
if hostHeader == "" {
|
if hostHeader == "" {
|
||||||
|
|||||||
@@ -170,6 +170,42 @@ func TestHTTPProbeRegexpNoMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHTTPSProbe(t *testing.T) {
|
||||||
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
host, portStr, _ := net.SplitHostPort(srv.Listener.Addr().String())
|
||||||
|
port := uint16(0)
|
||||||
|
fmt.Sscanf(portStr, "%d", &port)
|
||||||
|
|
||||||
|
cfg := ProbeConfig{
|
||||||
|
Target: net.ParseIP(host),
|
||||||
|
Port: port,
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
HTTP: &config.HTTPParams{
|
||||||
|
Path: "/",
|
||||||
|
ResponseCodeMin: 200,
|
||||||
|
ResponseCodeMax: 200,
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify HTTPSProbe succeeds (TLS conn reused, no double-wrap).
|
||||||
|
result := HTTPSProbe(context.Background(), cfg)
|
||||||
|
if !result.OK {
|
||||||
|
t.Errorf("HTTPSProbe failed: code=%s detail=%s", result.Code, result.Detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify HTTPProbe (plain) against the TLS server fails at the TLS layer,
|
||||||
|
// not with a double-TLS confusion error.
|
||||||
|
result = HTTPProbe(context.Background(), cfg)
|
||||||
|
if result.OK {
|
||||||
|
t.Error("plain HTTPProbe against TLS server should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHTTPProbeNoRedirect(t *testing.T) {
|
func TestHTTPProbeNoRedirect(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/other", http.StatusFound)
|
http.Redirect(w, r, "/other", http.StatusFound)
|
||||||
|
|||||||
@@ -49,12 +49,22 @@ message ListFrontendsResponse {
|
|||||||
repeated string frontend_names = 1;
|
repeated string frontend_names = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message PoolBackendInfo {
|
||||||
|
string name = 1;
|
||||||
|
int32 weight = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PoolInfo {
|
||||||
|
string name = 1;
|
||||||
|
repeated PoolBackendInfo backends = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message FrontendInfo {
|
message FrontendInfo {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
string address = 2;
|
string address = 2;
|
||||||
string protocol = 3;
|
string protocol = 3;
|
||||||
uint32 port = 4;
|
uint32 port = 4;
|
||||||
repeated string backend_names = 5;
|
repeated PoolInfo pools = 5;
|
||||||
string description = 6;
|
string description = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +114,7 @@ message BackendInfo {
|
|||||||
string state = 3;
|
string state = 3;
|
||||||
repeated TransitionRecord transitions = 4;
|
repeated TransitionRecord transitions = 4;
|
||||||
bool enabled = 5;
|
bool enabled = 5;
|
||||||
int32 weight = 6;
|
string healthcheck = 6;
|
||||||
string healthcheck = 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message TransitionRecord {
|
message TransitionRecord {
|
||||||
|
|||||||
Reference in New Issue
Block a user