Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c6416debe | |||
| 303b426207 | |||
| f847d1aad4 | |||
| 3b493e9047 | |||
| bec968b354 |
@@ -1,54 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [automc]
|
||||
tags: ["automc-v*"]
|
||||
pull_request:
|
||||
branches: [automc]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
- name: Test automc package
|
||||
run: go test ./internal/automc/...
|
||||
- name: Test full suite
|
||||
run: go test ./...
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: validate
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Compute tags
|
||||
id: meta
|
||||
run: |
|
||||
if [[ "$GITEA_REF" == refs/tags/automc-v* ]]; then
|
||||
echo "tag=${GITEA_REF#refs/tags/}" >> "$GITEA_OUTPUT"
|
||||
else
|
||||
echo "tag=automc" >> "$GITEA_OUTPUT"
|
||||
fi
|
||||
echo "sha=sha-$(echo "$GITEA_SHA" | cut -c1-7)" >> "$GITEA_OUTPUT"
|
||||
|
||||
- name: Login to registry
|
||||
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.timemachine.center -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
IMG=git.timemachine.center/timemachine/${{ gitea.event.repository.name }}
|
||||
docker build -t "$IMG:${{ steps.meta.outputs.tag }}" -t "$IMG:${{ steps.meta.outputs.sha }}" .
|
||||
docker push "$IMG:${{ steps.meta.outputs.tag }}"
|
||||
docker push "$IMG:${{ steps.meta.outputs.sha }}"
|
||||
@@ -1,51 +0,0 @@
|
||||
# Fork status — Timemachine/mc-router
|
||||
|
||||
This is a **soft fork** of [`itzg/mc-router`](https://github.com/itzg/mc-router) maintained for the [automc](https://git.timemachine.center/Timemachine/automc) Minecraft management platform.
|
||||
|
||||
## Branch model
|
||||
|
||||
| Branch | Tracks | Contents |
|
||||
|---|---|---|
|
||||
| `main` | `upstream/main` (github.com/itzg/mc-router) | Verbatim mirror. **Do not commit here.** |
|
||||
| `automc` | branched from `v1.42.1` | Soft fork; carries the automc-specific patch. |
|
||||
|
||||
## Patch surface
|
||||
|
||||
Minimal — designed for low-friction rebases.
|
||||
|
||||
| Path | Type | Why |
|
||||
|---|---|---|
|
||||
| `cmd/mc-router/main.go` | edit | 1 import line + 4-line `automc.Wire(ctx)` call. Only upstream file modified. |
|
||||
| `internal/automc/` | new dir | Self-contained extension package — no upstream conflicts possible. |
|
||||
| `docs/AUTOMC.md` | new | Operator doc for the extensions. See [`docs/AUTOMC.md`](docs/AUTOMC.md). |
|
||||
| `Makefile` | edit | Appended `sync-upstream` + `automc-build` targets. |
|
||||
| `go.mod` / `go.sum` | edit | Added `github.com/jackc/pgx/v5 v5.8.0` for LISTEN/NOTIFY. |
|
||||
| `FORK.md` | new | This file. |
|
||||
|
||||
Everything else stays untouched. No edits to `server/`, `mcproto/`, `cmd/mc-router/` beyond the wire-call.
|
||||
|
||||
## What the fork adds
|
||||
|
||||
1. **Postgres LISTEN/NOTIFY route source** — drop-in alternative to `--routes-config`, `--api-binding`, K8s, and Docker route sources. Pulled in via `AUTOMC_DSN` env var.
|
||||
2. **HTTP waker integration** — registers a `WakerFunc` per route that POSTs to a control-plane (`AUTOMC_WAKER_URL`) and polls until `state=running`.
|
||||
|
||||
Both are opt-in. With `AUTOMC_DSN` unset, the binary behaves exactly like upstream `itzg/mc-router`.
|
||||
|
||||
See [`docs/AUTOMC.md`](docs/AUTOMC.md) for env vars, schema, troubleshooting.
|
||||
|
||||
## Sync from upstream
|
||||
|
||||
```
|
||||
make sync-upstream
|
||||
```
|
||||
|
||||
Rebases `automc` onto `upstream/main`, runs `go build ./...` and `go test ./internal/automc/...`. If `server.Routes.CreateMapping` signature changes, only [`internal/automc/pgsync.go`](internal/automc/pgsync.go) needs adjustment.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
| Where it goes | What |
|
||||
|---|---|
|
||||
| [`Timemachine/mc-router`](https://git.timemachine.center/Timemachine/mc-router/issues) | automc-specific bugs (pg sync, waker, fork mechanics) |
|
||||
| [`itzg/mc-router`](https://github.com/itzg/mc-router/issues) | Upstream core bugs (routing, mcproto, REST API) |
|
||||
|
||||
If unsure, file upstream first — most reports turn out to be upstream-side.
|
||||
@@ -5,15 +5,3 @@ test:
|
||||
.PHONY: release
|
||||
release:
|
||||
curl -sL https://git.io/goreleaser | bash
|
||||
|
||||
.PHONY: sync-upstream
|
||||
sync-upstream:
|
||||
git fetch upstream
|
||||
git checkout automc
|
||||
git rebase upstream/main
|
||||
go build ./...
|
||||
go test ./internal/automc/...
|
||||
|
||||
.PHONY: automc-build
|
||||
automc-build:
|
||||
go build -o mc-router ./cmd/mc-router
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/itzg/go-flagsfiller"
|
||||
"github.com/itzg/mc-router/internal/automc"
|
||||
"github.com/itzg/mc-router/server"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -66,10 +65,6 @@ func main() {
|
||||
logrus.WithError(err).Fatal("Could not setup server")
|
||||
}
|
||||
|
||||
if err := automc.Wire(ctx); err != nil {
|
||||
logrus.WithError(err).Fatal("automc Wire failed")
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(s.Run)
|
||||
|
||||
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
# automc extensions
|
||||
|
||||
Soft fork of `itzg/mc-router` that adds Postgres-driven route management and an HTTP waker, without touching upstream behavior by default.
|
||||
|
||||
The `internal/automc` package is opt-in via env vars: with `AUTOMC_DSN` unset, the binary behaves exactly like upstream.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Var | Required | Purpose |
|
||||
|---|---|---|
|
||||
| `AUTOMC_DSN` | yes (to enable) | Postgres DSN, e.g. `postgres://user:pass@host:5432/automc?sslmode=disable`. When unset, automc is a no-op. |
|
||||
| `AUTOMC_WAKER_URL` | no | Base URL of the waker control plane (server-manager). When set, stopped backends are auto-started on login. |
|
||||
| `AUTOMC_WAKER_TOKEN` | no | Sent as `X-API-Key` header on every waker request. |
|
||||
|
||||
Recommended companion upstream flags:
|
||||
|
||||
- `--use-asleep-motd` / `--use-loading-motd` — supplies friendly MOTD to clients during the wake window. Already implemented upstream; automc does not duplicate this.
|
||||
|
||||
## Postgres schema
|
||||
|
||||
Apply this once to the database referenced by `AUTOMC_DSN`. mc-router only reads; the trigger is what tells it to re-read.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
name TEXT PRIMARY KEY,
|
||||
domain TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'stopped',
|
||||
UNIQUE(domain)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION automc_notify_routes_changed() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('automc_routes_changed', '');
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS automc_servers_route_notify ON servers;
|
||||
CREATE TRIGGER automc_servers_route_notify
|
||||
AFTER INSERT OR UPDATE OF domain, address, state, enabled OR DELETE ON servers
|
||||
FOR EACH ROW EXECUTE FUNCTION automc_notify_routes_changed();
|
||||
```
|
||||
|
||||
The trigger fires on every mutation to a route-relevant column. mc-router holds a persistent `LISTEN automc_routes_changed` and re-runs `SELECT name, domain, address FROM servers WHERE domain != '' AND address != ''`, diffing against its in-memory map. Adds/removes/changes call `server.Routes.CreateMapping` and `DeleteMapping` directly — no file I/O.
|
||||
|
||||
State column is not read by mc-router; it exists to drive the trigger and for the waker's own ready-check.
|
||||
|
||||
### Sync lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Wire as automc.Wire
|
||||
participant Sync as syncer
|
||||
participant PG as Postgres
|
||||
participant R as server.Routes
|
||||
|
||||
Wire->>Sync: newSyncer(dsn, waker)
|
||||
Wire-)Sync: go run(ctx)
|
||||
Note over Sync: backoff = 1s
|
||||
|
||||
loop reconnect (until ctx cancelled)
|
||||
Sync->>PG: pgx.Connect(dsn)
|
||||
alt connect ok
|
||||
Sync->>PG: LISTEN automc_routes_changed
|
||||
Sync->>PG: SELECT name, domain, address FROM servers
|
||||
PG-->>Sync: initial rows
|
||||
Sync->>Sync: diff vs current (empty on first run)
|
||||
loop for each add
|
||||
Sync->>R: CreateMapping(host, addr, waker)
|
||||
end
|
||||
loop for each del
|
||||
Sync->>R: DeleteMapping(host)
|
||||
end
|
||||
Note over Sync: backoff reset
|
||||
|
||||
loop while connection healthy
|
||||
Sync->>PG: WaitForNotification(ctx)
|
||||
PG-->>Sync: NOTIFY automc_routes_changed
|
||||
Sync->>PG: SELECT all servers
|
||||
PG-->>Sync: fresh rows
|
||||
Sync->>R: CreateMapping / DeleteMapping (diff only)
|
||||
end
|
||||
else connect or notify error
|
||||
Note over Sync: warn log<br/>sleep backoff<br/>backoff = min(backoff*2, 30s)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Waker contract
|
||||
|
||||
When `AUTOMC_WAKER_URL` is set, every route is registered with a `WakerFunc` that the upstream connector calls only when a client tries to LOGIN (not on status pings — those are answered locally via `--use-asleep-motd`).
|
||||
|
||||
The waker:
|
||||
|
||||
1. `POST {AUTOMC_WAKER_URL}/servers/{name}/start` — fire-and-forget start signal. `409 Conflict` is treated as success (already starting).
|
||||
2. Polls `GET {AUTOMC_WAKER_URL}/servers/{name}` every 2 s, expecting JSON `{"state":"running","address":"host:port"}`.
|
||||
3. Returns the polled `address` once `state == "running"`, or errors after 90 s.
|
||||
|
||||
The polled address overrides the route's static address for that connection only — useful when the backend's IP is allocated lazily.
|
||||
|
||||
### Waker dispatch on stopped backend
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Client as MC Client
|
||||
participant MR as mc-router
|
||||
participant SM as server-manager (waker URL)
|
||||
participant MC as MC backend
|
||||
|
||||
Client->>MR: TCP + Handshake (next_state=login)
|
||||
Note over MR: Routes.FindBackendForServerAddress<br/>returns backend + WakerFunc
|
||||
MR->>MC: dial backend address
|
||||
MC--xMR: connection refused
|
||||
Note over MR: WakerFunc != nil →<br/>invoke it before kicking client
|
||||
|
||||
MR->>SM: POST /servers/test1/start<br/>X-API-Key: ...
|
||||
alt 202 Accepted
|
||||
SM-->>MR: 202
|
||||
else 409 Conflict (already starting)
|
||||
SM-->>MR: 409 — treat as success
|
||||
else 5xx
|
||||
SM-->>MR: error → WakerFunc returns err
|
||||
MR->>Client: kick (connection refused)
|
||||
end
|
||||
|
||||
loop every 2s, up to 90s
|
||||
MR->>SM: GET /servers/test1
|
||||
alt running
|
||||
SM-->>MR: 200 state=running address=10.0.0.5:25565
|
||||
else still starting
|
||||
SM-->>MR: 200 state=starting
|
||||
Note over MR: continue polling
|
||||
end
|
||||
end
|
||||
|
||||
Note over MR: WakerFunc returns address
|
||||
MR->>MC: dial polled address
|
||||
MC-->>MR: ok
|
||||
MR->>Client: splice handshake-and-onward
|
||||
```
|
||||
|
||||
## Upstream sync
|
||||
|
||||
```
|
||||
make sync-upstream
|
||||
```
|
||||
|
||||
Fetches `upstream/main`, rebases the `automc` branch onto it, builds, and runs the automc tests. The patch surface is intentionally tiny so rebase conflicts are rare:
|
||||
|
||||
```
|
||||
cmd/mc-router/main.go — 1 import line + 4-line Wire call
|
||||
internal/automc/ — new directory; no upstream conflicts possible
|
||||
docs/AUTOMC.md — new doc; no upstream conflicts
|
||||
Makefile — appended targets only
|
||||
go.mod / go.sum — pgx dep added; mergeable
|
||||
```
|
||||
|
||||
If upstream renames `server.Routes.CreateMapping` or changes its signature, only `pgsync.go:apply` needs adjustment.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# 1. Start a postgres reachable from mc-router
|
||||
podman run -d --name automc-pg \
|
||||
-e POSTGRES_PASSWORD=test -e POSTGRES_DB=automc \
|
||||
-p 127.0.0.1:5432:5432 docker.io/postgres:16-alpine
|
||||
|
||||
# 2. Apply the schema + trigger (see "Postgres schema" above)
|
||||
podman exec -i automc-pg psql -U postgres -d automc < schema.sql
|
||||
|
||||
# 3. Run mc-router with automc enabled
|
||||
AUTOMC_DSN="postgres://postgres:test@127.0.0.1:5432/automc?sslmode=disable" \
|
||||
AUTOMC_WAKER_URL="http://server-manager:8080" \
|
||||
AUTOMC_WAKER_TOKEN="$SM_API_KEY" \
|
||||
mc-router --port 25565 --api-binding 127.0.0.1:25590 --use-asleep-motd
|
||||
|
||||
# 4. Add a route
|
||||
podman exec automc-pg psql -U postgres -d automc -c \
|
||||
"INSERT INTO servers (name, domain, address, state) \
|
||||
VALUES ('test1', 'test1.example.com', '127.0.0.1:25001', 'running');"
|
||||
|
||||
# Log should show: automc route +: test1.example.com → 127.0.0.1:25001 (test1)
|
||||
# REST API should show it: curl http://127.0.0.1:25590/routes
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Check |
|
||||
|---|---|---|
|
||||
| Binary starts but no `automc: pg route sync started` log | `AUTOMC_DSN` empty or unset | `env \| grep AUTOMC_DSN` |
|
||||
| `automc pgsync disconnected; reconnecting in 1s` repeating | pg unreachable / wrong DSN | Test with `psql "$AUTOMC_DSN" -c "SELECT 1"` |
|
||||
| INSERT into `servers` doesn't fire a route | Trigger missing or not on the right columns | `\d+ servers` in psql — confirm `automc_servers_route_notify` trigger exists |
|
||||
| Routes appear in REST `/routes` but client connect times out | Backend address wrong / unreachable from mc-router | `podman exec mc-router nc -zv <address>` |
|
||||
| WakerFunc never called for stopped backends | `AUTOMC_WAKER_URL` empty | Set it; without it stopped backends get connection refused on login |
|
||||
| `waker timeout for X after 1m30s` | Backend takes longer than 90 s to come up | Tune `wakerPollTimeout` (currently a const in `waker.go:18`); planned env var |
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
go build ./...
|
||||
go test ./internal/automc/...
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
End-to-end smoke recipe verified 2026-05-27 on local podman: INSERT/UPDATE/DELETE in `servers` table propagated to `/routes` REST API within ~1 s; postgres restart triggered exponential backoff reconnect (1 s → 30 s cap) and full route re-sync on reconnect.
|
||||
@@ -3,23 +3,23 @@ module github.com/itzg/mc-router
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-kit/kit v0.13.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
|
||||
github.com/itzg/go-flagsfiller v1.17.0
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/juju/ratelimit v1.0.2
|
||||
github.com/pires/go-proxyproto v0.11.0
|
||||
github.com/pires/go-proxyproto v0.12.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.ngrok.com/ngrok v1.12.1
|
||||
golang.org/x/text v0.36.0
|
||||
k8s.io/api v0.35.4
|
||||
k8s.io/apimachinery v0.35.4
|
||||
k8s.io/client-go v0.35.4
|
||||
golang.org/x/text v0.37.0
|
||||
k8s.io/api v0.36.1
|
||||
k8s.io/apimachinery v0.36.1
|
||||
k8s.io/client-go v0.36.1
|
||||
)
|
||||
|
||||
// go-kit pulls in old, ambiguous package
|
||||
@@ -31,15 +31,11 @@ exclude google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -64,23 +60,25 @@ require (
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.ngrok.com/muxado/v2 v2.0.1 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -92,7 +90,6 @@ require (
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
@@ -104,16 +101,16 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.3.0 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@@ -31,8 +29,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
@@ -58,8 +56,6 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -68,14 +64,12 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
@@ -88,14 +82,6 @@ github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSH
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/itzg/go-flagsfiller v1.17.0 h1:Zkg+qsbB24Msu78l+1aqzXAIEKEeLRzAiK7DN40Fdkk=
|
||||
github.com/itzg/go-flagsfiller v1.17.0/go.mod h1:ub1t7dNqIj57TWKUtEqfopXg0xKbBgd9JVuCLmelwNo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
@@ -136,16 +122,12 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -171,32 +153,31 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
@@ -212,19 +193,17 @@ golang.ngrok.com/ngrok v1.12.1/go.mod h1:BKOMdoZXfD4w6o3EtE7Cu9TVbaUWBqptrZRWnVc
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
@@ -235,34 +214,32 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
|
||||
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -272,29 +249,28 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
|
||||
gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
|
||||
k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988=
|
||||
k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU=
|
||||
k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds=
|
||||
k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc=
|
||||
k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8=
|
||||
k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY=
|
||||
k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo=
|
||||
k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA=
|
||||
k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8=
|
||||
k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0=
|
||||
k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
package automc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// LogEntry is the structured shape pushed to UI subscribers via SSE.
|
||||
type LogEntry struct {
|
||||
Time time.Time `json:"time"`
|
||||
Level string `json:"level"`
|
||||
Msg string `json:"msg"`
|
||||
Attrs string `json:"attrs,omitempty"`
|
||||
}
|
||||
|
||||
// LogBus is a fan-out buffer for logrus entries: a ring of the last N
|
||||
// entries (replayed on connect) + live broadcast to current subscribers.
|
||||
// Identical model to the one in svc-proxy/internal/httpsrv — kept local to
|
||||
// avoid a cross-repo dep on the fork.
|
||||
type LogBus struct {
|
||||
cap int
|
||||
|
||||
mu sync.RWMutex
|
||||
ring []LogEntry
|
||||
next int
|
||||
full bool
|
||||
listeners map[chan LogEntry]struct{}
|
||||
}
|
||||
|
||||
func NewLogBus(capacity int) *LogBus {
|
||||
if capacity <= 0 {
|
||||
capacity = 500
|
||||
}
|
||||
return &LogBus{
|
||||
cap: capacity,
|
||||
ring: make([]LogEntry, capacity),
|
||||
listeners: map[chan LogEntry]struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LogBus) Push(e LogEntry) {
|
||||
b.mu.Lock()
|
||||
b.ring[b.next] = e
|
||||
b.next = (b.next + 1) % b.cap
|
||||
if b.next == 0 {
|
||||
b.full = true
|
||||
}
|
||||
subs := make([]chan LogEntry, 0, len(b.listeners))
|
||||
for ch := range b.listeners {
|
||||
subs = append(subs, ch)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
for _, ch := range subs {
|
||||
select {
|
||||
case ch <- e:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LogBus) Backlog() []LogEntry {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
if !b.full {
|
||||
out := make([]LogEntry, b.next)
|
||||
copy(out, b.ring[:b.next])
|
||||
return out
|
||||
}
|
||||
out := make([]LogEntry, 0, b.cap)
|
||||
out = append(out, b.ring[b.next:]...)
|
||||
out = append(out, b.ring[:b.next]...)
|
||||
return out
|
||||
}
|
||||
|
||||
func (b *LogBus) Subscribe() chan LogEntry {
|
||||
ch := make(chan LogEntry, 32)
|
||||
b.mu.Lock()
|
||||
b.listeners[ch] = struct{}{}
|
||||
b.mu.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (b *LogBus) Unsubscribe(ch chan LogEntry) {
|
||||
b.mu.Lock()
|
||||
delete(b.listeners, ch)
|
||||
b.mu.Unlock()
|
||||
close(ch)
|
||||
}
|
||||
|
||||
// logrusBusHook adapts the LogBus to logrus's Hook interface. Registered
|
||||
// globally from Wire() so every upstream log emission is captured.
|
||||
type logrusBusHook struct{ bus *LogBus }
|
||||
|
||||
func (h *logrusBusHook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
func (h *logrusBusHook) Fire(e *logrus.Entry) error {
|
||||
var attrs string
|
||||
for k, v := range e.Data {
|
||||
if attrs != "" {
|
||||
attrs += " "
|
||||
}
|
||||
attrs += fmt.Sprintf("%s=%v", k, v)
|
||||
}
|
||||
h.bus.Push(LogEntry{
|
||||
Time: e.Time,
|
||||
Level: e.Level.String(),
|
||||
Msg: e.Message,
|
||||
Attrs: attrs,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package automc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/itzg/mc-router/server"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyChannel = "automc_routes_changed"
|
||||
reconnectMin = 1 * time.Second
|
||||
reconnectMax = 30 * time.Second
|
||||
)
|
||||
|
||||
type route struct {
|
||||
name string
|
||||
domain string
|
||||
address string
|
||||
}
|
||||
|
||||
type syncer struct {
|
||||
dsn string
|
||||
waker *wakerConfig
|
||||
current map[string]route
|
||||
}
|
||||
|
||||
func newSyncer(dsn string, w *wakerConfig) *syncer {
|
||||
return &syncer{dsn: dsn, waker: w, current: map[string]route{}}
|
||||
}
|
||||
|
||||
func (s *syncer) run(ctx context.Context) {
|
||||
backoff := reconnectMin
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
err := s.connectAndLoop(ctx)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
logrus.WithError(err).Warnf("automc pgsync disconnected; reconnecting in %s", backoff)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(backoff):
|
||||
}
|
||||
backoff *= 2
|
||||
if backoff > reconnectMax {
|
||||
backoff = reconnectMax
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *syncer) connectAndLoop(ctx context.Context) error {
|
||||
conn, err := pgx.Connect(ctx, s.dsn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pgx connect: %w", err)
|
||||
}
|
||||
defer conn.Close(context.Background())
|
||||
|
||||
if _, err := conn.Exec(ctx, "LISTEN "+notifyChannel); err != nil {
|
||||
return fmt.Errorf("LISTEN: %w", err)
|
||||
}
|
||||
logrus.Infof("automc pgsync connected; LISTEN %s", notifyChannel)
|
||||
|
||||
if err := s.refresh(ctx, conn); err != nil {
|
||||
return fmt.Errorf("initial refresh: %w", err)
|
||||
}
|
||||
|
||||
for {
|
||||
if _, err := conn.WaitForNotification(ctx); err != nil {
|
||||
return fmt.Errorf("wait notification: %w", err)
|
||||
}
|
||||
if err := s.refresh(ctx, conn); err != nil {
|
||||
return fmt.Errorf("refresh: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *syncer) refresh(ctx context.Context, conn *pgx.Conn) error {
|
||||
rows, err := conn.Query(ctx, `SELECT name, domain, address FROM servers WHERE domain IS NOT NULL AND domain != '' AND address != '' AND enabled IS NOT FALSE`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
desired := map[string]route{}
|
||||
for rows.Next() {
|
||||
var r route
|
||||
if err := rows.Scan(&r.name, &r.domain, &r.address); err != nil {
|
||||
return err
|
||||
}
|
||||
desired[r.domain] = r
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
add, del := diff(s.current, desired)
|
||||
s.apply(add, del)
|
||||
s.current = desired
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *syncer) apply(add []route, del []string) {
|
||||
for _, host := range del {
|
||||
if server.Routes.DeleteMapping(host) {
|
||||
logrus.Infof("automc route -: %s", host)
|
||||
}
|
||||
}
|
||||
for _, r := range add {
|
||||
server.Routes.CreateMapping(r.domain, r.address, "", s.waker.wakerFor(r.name), nil, "", "")
|
||||
logrus.Infof("automc route +: %s → %s (%s)", r.domain, r.address, r.name)
|
||||
}
|
||||
}
|
||||
|
||||
func diff(prev, next map[string]route) (add []route, del []string) {
|
||||
for host, r := range next {
|
||||
if p, ok := prev[host]; !ok || p.address != r.address || p.name != r.name {
|
||||
add = append(add, r)
|
||||
}
|
||||
}
|
||||
for host := range prev {
|
||||
if _, ok := next[host]; !ok {
|
||||
del = append(del, host)
|
||||
}
|
||||
}
|
||||
return add, del
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package automc
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
prev, next map[string]route
|
||||
wantAddHost []string
|
||||
wantDel []string
|
||||
}{
|
||||
{
|
||||
name: "empty to empty",
|
||||
prev: map[string]route{},
|
||||
next: map[string]route{},
|
||||
wantAddHost: nil,
|
||||
wantDel: nil,
|
||||
},
|
||||
{
|
||||
name: "add one",
|
||||
prev: map[string]route{},
|
||||
next: map[string]route{"a.example.com": {name: "a", domain: "a.example.com", address: "10.0.0.1:25565"}},
|
||||
wantAddHost: []string{"a.example.com"},
|
||||
wantDel: nil,
|
||||
},
|
||||
{
|
||||
name: "delete one",
|
||||
prev: map[string]route{"a.example.com": {name: "a", domain: "a.example.com", address: "10.0.0.1:25565"}},
|
||||
next: map[string]route{},
|
||||
wantAddHost: nil,
|
||||
wantDel: []string{"a.example.com"},
|
||||
},
|
||||
{
|
||||
name: "address change",
|
||||
prev: map[string]route{"a.example.com": {name: "a", domain: "a.example.com", address: "10.0.0.1:25565"}},
|
||||
next: map[string]route{"a.example.com": {name: "a", domain: "a.example.com", address: "10.0.0.2:25565"}},
|
||||
wantAddHost: []string{"a.example.com"},
|
||||
wantDel: nil,
|
||||
},
|
||||
{
|
||||
name: "name change with same address triggers re-register (waker rebind)",
|
||||
prev: map[string]route{"a.example.com": {name: "a", domain: "a.example.com", address: "10.0.0.1:25565"}},
|
||||
next: map[string]route{"a.example.com": {name: "b", domain: "a.example.com", address: "10.0.0.1:25565"}},
|
||||
wantAddHost: []string{"a.example.com"},
|
||||
wantDel: nil,
|
||||
},
|
||||
{
|
||||
name: "no change",
|
||||
prev: map[string]route{"a.example.com": {name: "a", domain: "a.example.com", address: "10.0.0.1:25565"}},
|
||||
next: map[string]route{"a.example.com": {name: "a", domain: "a.example.com", address: "10.0.0.1:25565"}},
|
||||
wantAddHost: nil,
|
||||
wantDel: nil,
|
||||
},
|
||||
{
|
||||
name: "mixed add + delete",
|
||||
prev: map[string]route{
|
||||
"a.example.com": {name: "a", domain: "a.example.com", address: "10.0.0.1:25565"},
|
||||
"b.example.com": {name: "b", domain: "b.example.com", address: "10.0.0.2:25565"},
|
||||
},
|
||||
next: map[string]route{
|
||||
"a.example.com": {name: "a", domain: "a.example.com", address: "10.0.0.1:25565"},
|
||||
"c.example.com": {name: "c", domain: "c.example.com", address: "10.0.0.3:25565"},
|
||||
},
|
||||
wantAddHost: []string{"c.example.com"},
|
||||
wantDel: []string{"b.example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
add, del := diff(tc.prev, tc.next)
|
||||
gotAdd := make([]string, 0, len(add))
|
||||
for _, r := range add {
|
||||
gotAdd = append(gotAdd, r.domain)
|
||||
}
|
||||
sort.Strings(gotAdd)
|
||||
sort.Strings(del)
|
||||
sort.Strings(tc.wantAddHost)
|
||||
sort.Strings(tc.wantDel)
|
||||
if !equalSlice(gotAdd, tc.wantAddHost) {
|
||||
t.Errorf("add: got %v want %v", gotAdd, tc.wantAddHost)
|
||||
}
|
||||
if !equalSlice(del, tc.wantDel) {
|
||||
t.Errorf("del: got %v want %v", del, tc.wantDel)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func equalSlice(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>mc-router</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #0e0e0e; color: #eee;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 13px; line-height: 1.4; }
|
||||
body { display: flex; flex-direction: column; }
|
||||
header { padding: 8px 12px; background: #1a1a1a; border-bottom: 1px solid #333;
|
||||
display: flex; justify-content: space-between; align-items: center; flex: 0 0 auto; }
|
||||
header h1 { margin: 0; font-size: 14px; font-weight: 600; }
|
||||
header h1 .meta { color: #888; font-weight: normal; margin-left: 10px; }
|
||||
#status { font-size: 12px; color: #6f6; }
|
||||
#status.disconnected { color: #f66; }
|
||||
#status.connecting { color: #fc6; }
|
||||
|
||||
main { flex: 1; display: grid; grid-template-rows: 1fr 1fr; min-height: 0; }
|
||||
section { padding: 12px 16px; overflow: auto; min-height: 0; }
|
||||
section + section { border-top: 1px solid #333; }
|
||||
|
||||
h2 { margin: 0 0 8px; font-size: 12px; font-weight: 600; color: #888;
|
||||
text-transform: uppercase; letter-spacing: 0.6px;
|
||||
display: flex; justify-content: space-between; align-items: center; }
|
||||
h2 .count { color: #888; font-weight: normal; margin-left: 6px; }
|
||||
h2 button { background: transparent; color: #888; border: 1px solid #333;
|
||||
padding: 2px 8px; cursor: pointer; font: inherit; font-size: 11px; border-radius: 3px; }
|
||||
h2 button:hover { color: #eee; border-color: #6cf; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
||||
th, td { text-align: left; padding: 4px 10px; border-bottom: 1px solid #1f1f1f; }
|
||||
th { color: #888; font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
td.num { text-align: right; }
|
||||
.empty { color: #888; padding: 16px 0; }
|
||||
|
||||
pre.logbox { margin: 0; white-space: pre-wrap; word-break: break-word; }
|
||||
.log-line { padding: 0; }
|
||||
.log-line .ts { color: #888; margin-right: 8px; }
|
||||
.log-line .lvl { margin-right: 6px; font-weight: 600; }
|
||||
.log-line.lvl-debug .lvl { color: #888; }
|
||||
.log-line.lvl-info .lvl { color: #6cf; }
|
||||
.log-line.lvl-warning .lvl, .log-line.lvl-warn .lvl { color: #fc6; }
|
||||
.log-line.lvl-error .lvl { color: #f66; }
|
||||
.log-line .attrs { color: #888; margin-left: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>mc-router <span class="meta" id="meta">— connecting…</span></h1>
|
||||
<span id="status" class="connecting">log stream: connecting…</span>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<h2><span>Routes <span class="count" id="route-count"></span></span></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server address (host)</th>
|
||||
<th>Backend</th>
|
||||
<th class="num">Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="route-rows"></tbody>
|
||||
</table>
|
||||
<div class="empty" id="route-empty">no routes registered</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2><span>Logs</span><button onclick="document.getElementById('logbox').innerHTML=''">clear</button></h2>
|
||||
<pre class="logbox" id="logbox"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
async function refreshRoutes() {
|
||||
try {
|
||||
const r = await fetch('./api/routes');
|
||||
const j = await r.json();
|
||||
const rows = document.getElementById('route-rows');
|
||||
const empty = document.getElementById('route-empty');
|
||||
const count = document.getElementById('route-count');
|
||||
rows.innerHTML = '';
|
||||
if (!j.routes || j.routes.length === 0) {
|
||||
empty.style.display = '';
|
||||
count.textContent = '';
|
||||
} else {
|
||||
empty.style.display = 'none';
|
||||
count.textContent = '(' + j.routes.length + ')';
|
||||
for (const r of j.routes) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td>' + r.server_address + '</td>' +
|
||||
'<td>' + r.backend + '</td>' +
|
||||
'<td class="num">' + r.active_connections + '</td>';
|
||||
rows.appendChild(tr);
|
||||
}
|
||||
}
|
||||
document.getElementById('meta').textContent =
|
||||
'— ' + j.routes.length + ' routes · ' + (j.total_connections || 0) + ' active conn';
|
||||
} catch (e) {
|
||||
document.getElementById('meta').textContent = '— api error';
|
||||
}
|
||||
}
|
||||
setInterval(refreshRoutes, 2000);
|
||||
refreshRoutes();
|
||||
|
||||
function startLogStream() {
|
||||
const status = document.getElementById('status');
|
||||
const box = document.getElementById('logbox');
|
||||
const es = new EventSource('./api/logs');
|
||||
es.onopen = () => { status.textContent = 'log stream: live'; status.className = ''; };
|
||||
es.onerror = () => { status.textContent = 'log stream: reconnecting…'; status.className = 'disconnected'; };
|
||||
es.onmessage = ev => {
|
||||
let e;
|
||||
try { e = JSON.parse(ev.data); } catch { return; }
|
||||
const ts = e.time ? e.time.split('T')[1].split('.')[0] : '';
|
||||
const div = document.createElement('div');
|
||||
const lvl = (e.level || 'info').toLowerCase();
|
||||
div.className = 'log-line lvl-' + lvl;
|
||||
div.innerHTML =
|
||||
'<span class="ts">' + ts + '</span>' +
|
||||
'<span class="lvl">' + (e.level || 'info') + '</span>' +
|
||||
(e.msg || '') +
|
||||
(e.attrs ? '<span class="attrs">' + e.attrs + '</span>' : '');
|
||||
box.appendChild(div);
|
||||
const parent = box.parentElement;
|
||||
if (parent.scrollHeight - parent.scrollTop - parent.clientHeight < 60) {
|
||||
parent.scrollTop = parent.scrollHeight;
|
||||
}
|
||||
while (box.children.length > 1000) box.removeChild(box.firstChild);
|
||||
};
|
||||
}
|
||||
startLogStream();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,174 +0,0 @@
|
||||
package automc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/itzg/mc-router/server"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticFS embed.FS
|
||||
|
||||
// startUI starts a separate HTTP server on uiBinding serving the operator
|
||||
// dashboard (embedded index.html), an SSE log feed, and a JSON snapshot of
|
||||
// the current route table. The upstream JSON API on API_BINDING is left
|
||||
// untouched so existing tooling keeps working.
|
||||
func startUI(ctx context.Context, uiBinding string, bus *LogBus) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
sub, err := fs.Sub(staticFS, "static")
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("automc ui: embed misconfigured")
|
||||
return
|
||||
}
|
||||
mux.Handle("GET /", http.FileServer(http.FS(sub)))
|
||||
mux.HandleFunc("GET /api/routes", routesSnapshotHandler)
|
||||
mux.HandleFunc("GET /api/logs", sseLogsHandler(bus))
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: uiBinding,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(shutCtx)
|
||||
}()
|
||||
go func() {
|
||||
logrus.WithField("binding", uiBinding).Info("automc: ui server listening")
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
logrus.WithError(err).Error("automc ui server failed")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// RouteSnapshot is one row of the routes table the UI renders. Same shape as
|
||||
// the upstream /routes JSON but flatter — the UI doesn't need both backend
|
||||
// and scalingTarget shown separately. ActiveConnections is the live gauge
|
||||
// value scraped from the Prometheus registry (-1 if the metric isn't
|
||||
// registered, which can happen briefly at startup).
|
||||
type RouteSnapshot struct {
|
||||
ServerAddress string `json:"server_address"`
|
||||
Backend string `json:"backend"`
|
||||
ActiveConnections int `json:"active_connections"`
|
||||
}
|
||||
|
||||
func routesSnapshotHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
mappings := server.Routes.GetMappings()
|
||||
perRoute, total := scrapeActiveConnections()
|
||||
|
||||
out := make([]RouteSnapshot, 0, len(mappings))
|
||||
for addr, backend := range mappings {
|
||||
conns, ok := perRoute[addr]
|
||||
if !ok {
|
||||
conns = 0
|
||||
}
|
||||
out = append(out, RouteSnapshot{ServerAddress: addr, Backend: backend, ActiveConnections: conns})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ServerAddress < out[j].ServerAddress })
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"routes": out,
|
||||
"total_connections": total,
|
||||
"at": time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// scrapeActiveConnections walks the Prometheus default registry to extract
|
||||
// two gauges that mc-router exposes:
|
||||
//
|
||||
// mc_router_active_connections (no labels)
|
||||
// mc_router_server_active_connections{server_address=…} (per route)
|
||||
//
|
||||
// Returns (perServerAddress, total). On any gathering error returns zero
|
||||
// values silently — the UI shows 0 rather than blocking on a metrics issue.
|
||||
func scrapeActiveConnections() (map[string]int, int) {
|
||||
per := map[string]int{}
|
||||
total := 0
|
||||
families, err := prometheus.DefaultGatherer.Gather()
|
||||
if err != nil {
|
||||
return per, 0
|
||||
}
|
||||
for _, mf := range families {
|
||||
switch mf.GetName() {
|
||||
case "mc_router_active_connections":
|
||||
for _, m := range mf.GetMetric() {
|
||||
if g := m.GetGauge(); g != nil {
|
||||
total = int(g.GetValue())
|
||||
}
|
||||
}
|
||||
case "mc_router_server_active_connections":
|
||||
for _, m := range mf.GetMetric() {
|
||||
var addr string
|
||||
for _, lp := range m.GetLabel() {
|
||||
if lp.GetName() == "server_address" {
|
||||
addr = lp.GetValue()
|
||||
break
|
||||
}
|
||||
}
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
if g := m.GetGauge(); g != nil {
|
||||
per[addr] = int(g.GetValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return per, total
|
||||
}
|
||||
|
||||
func sseLogsHandler(bus *LogBus) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
for _, e := range bus.Backlog() {
|
||||
writeSSEEvent(w, e)
|
||||
}
|
||||
flusher.Flush()
|
||||
|
||||
ch := bus.Subscribe()
|
||||
defer bus.Unsubscribe(ch)
|
||||
|
||||
hb := time.NewTicker(30 * time.Second)
|
||||
defer hb.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case e := <-ch:
|
||||
writeSSEEvent(w, e)
|
||||
flusher.Flush()
|
||||
case <-hb.C:
|
||||
_, _ = io.WriteString(w, ":hb\n\n")
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeSSEEvent(w io.Writer, e LogEntry) {
|
||||
fmt.Fprintf(w, "data: {\"time\":%q,\"level\":%q,\"msg\":%q,\"attrs\":%q}\n\n",
|
||||
e.Time.Format(time.RFC3339Nano), e.Level, e.Msg, e.Attrs)
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package automc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/itzg/mc-router/server"
|
||||
)
|
||||
|
||||
const (
|
||||
wakerPollInterval = 2 * time.Second
|
||||
wakerPollTimeout = 90 * time.Second
|
||||
)
|
||||
|
||||
var wakerPollIntervalForTest = wakerPollInterval
|
||||
|
||||
type wakerConfig struct {
|
||||
baseURL string
|
||||
token string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newWakerConfig(baseURL, token string) *wakerConfig {
|
||||
if baseURL == "" {
|
||||
return nil
|
||||
}
|
||||
return &wakerConfig{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
token: token,
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wakerConfig) wakerFor(serverName string) server.WakerFunc {
|
||||
if w == nil {
|
||||
return nil
|
||||
}
|
||||
return func(ctx context.Context) (string, error) {
|
||||
if err := w.start(ctx, serverName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return w.pollUntilRunning(ctx, serverName)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wakerConfig) start(ctx context.Context, name string) error {
|
||||
u := fmt.Sprintf("%s/servers/%s/start", w.baseURL, url.PathEscape(name))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.setAuth(req)
|
||||
resp, err := w.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("waker start %s: %w", name, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusConflict {
|
||||
// 409 = already starting/running, treat as success
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return fmt.Errorf("waker start %s: %s — %s", name, resp.Status, strings.TrimSpace(string(body)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *wakerConfig) pollUntilRunning(ctx context.Context, name string) (string, error) {
|
||||
deadline := time.Now().Add(wakerPollTimeout)
|
||||
ticker := time.NewTicker(wakerPollIntervalForTest)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
state, addr, err := w.queryState(ctx, name)
|
||||
if err == nil && state == "running" && addr != "" {
|
||||
return addr, nil
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return "", fmt.Errorf("waker timeout for %s after %s (last state=%q err=%v)", name, wakerPollTimeout, state, err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wakerConfig) queryState(ctx context.Context, name string) (string, string, error) {
|
||||
u := fmt.Sprintf("%s/servers/%s", w.baseURL, url.PathEscape(name))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
w.setAuth(req)
|
||||
resp, err := w.client.Do(req)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", "", fmt.Errorf("query state %s: %s", name, resp.Status)
|
||||
}
|
||||
var body struct {
|
||||
State string `json:"state"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return body.State, body.Address, nil
|
||||
}
|
||||
|
||||
func (w *wakerConfig) setAuth(req *http.Request) {
|
||||
if w.token != "" {
|
||||
req.Header.Set("X-API-Key", w.token)
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package automc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWakerNilWhenURLEmpty(t *testing.T) {
|
||||
w := newWakerConfig("", "")
|
||||
if w != nil {
|
||||
t.Fatalf("expected nil waker config when URL empty, got %+v", w)
|
||||
}
|
||||
if w.wakerFor("foo") != nil {
|
||||
t.Fatalf("expected nil WakerFunc from nil config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWakerStartThenPoll(t *testing.T) {
|
||||
var startCalls int32
|
||||
var pollCalls int32
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("X-API-Key") != "secret" {
|
||||
http.Error(w, "no auth", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/start"):
|
||||
atomic.AddInt32(&startCalls, 1)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
case r.Method == http.MethodGet:
|
||||
n := atomic.AddInt32(&pollCalls, 1)
|
||||
state := "starting"
|
||||
addr := ""
|
||||
if n >= 2 {
|
||||
state = "running"
|
||||
addr = "10.0.0.5:25565"
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"state": state,
|
||||
"address": addr,
|
||||
})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
wc := newWakerConfig(srv.URL, "secret")
|
||||
wc.client.Timeout = 2 * time.Second
|
||||
|
||||
// Tighten poll interval for the test only.
|
||||
saved := wakerPollIntervalForTest
|
||||
wakerPollIntervalForTest = 10 * time.Millisecond
|
||||
t.Cleanup(func() { wakerPollIntervalForTest = saved })
|
||||
|
||||
fn := wc.wakerFor("test1")
|
||||
if fn == nil {
|
||||
t.Fatal("expected non-nil WakerFunc")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
addr, err := fn(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("wake failed: %v", err)
|
||||
}
|
||||
if addr != "10.0.0.5:25565" {
|
||||
t.Errorf("addr: got %q want 10.0.0.5:25565", addr)
|
||||
}
|
||||
if atomic.LoadInt32(&startCalls) != 1 {
|
||||
t.Errorf("expected 1 start call, got %d", startCalls)
|
||||
}
|
||||
if got := atomic.LoadInt32(&pollCalls); got < 2 {
|
||||
t.Errorf("expected >=2 polls, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWakerStartHandles409(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/start") {
|
||||
http.Error(w, "already running", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"state": "running",
|
||||
"address": "10.0.0.6:25565",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
wc := newWakerConfig(srv.URL, "")
|
||||
wakerPollIntervalForTest = 10 * time.Millisecond
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
addr, err := wc.wakerFor("x")(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("expected 409 to be treated as success, got err: %v", err)
|
||||
}
|
||||
if addr != "10.0.0.6:25565" {
|
||||
t.Errorf("addr: got %q", addr)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Package automc wires automc-specific extensions onto upstream mc-router.
|
||||
//
|
||||
// All behavior is opt-in via env vars; when AUTOMC_DSN is unset, Wire is a no-op
|
||||
// and the binary behaves exactly like upstream itzg/mc-router.
|
||||
package automc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Wire(ctx context.Context) error {
|
||||
dsn := os.Getenv("AUTOMC_DSN")
|
||||
if dsn == "" {
|
||||
return nil
|
||||
}
|
||||
waker := newWakerConfig(os.Getenv("AUTOMC_WAKER_URL"), os.Getenv("AUTOMC_WAKER_TOKEN"))
|
||||
s := newSyncer(dsn, waker)
|
||||
go s.run(ctx)
|
||||
logrus.Info("automc: pg route sync started")
|
||||
|
||||
// Operator UI on a separate port — upstream's API_BINDING stays
|
||||
// JSON-only and untouched. Enable by setting AUTOMC_UI_BINDING (e.g.
|
||||
// ":8082"); leave unset to skip and behave exactly like upstream.
|
||||
if uiBinding := os.Getenv("AUTOMC_UI_BINDING"); uiBinding != "" {
|
||||
bus := NewLogBus(500)
|
||||
logrus.AddHook(&logrusBusHook{bus: bus})
|
||||
startUI(ctx, uiBinding, bus)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+3
-3
@@ -85,7 +85,7 @@ func ReadPacket(reader *bufio.Reader, addr net.Addr, state State) (*Packet, erro
|
||||
logrus.
|
||||
WithField("client", addr).
|
||||
WithField("packet", packet).
|
||||
Debug("Read packet")
|
||||
Trace("Read packet")
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ func ReadFrame(reader io.Reader, addr net.Addr) (*Frame, error) {
|
||||
logrus.
|
||||
WithField("client", addr).
|
||||
WithField("length", frame.Length).
|
||||
Debug("Read frame length")
|
||||
Trace("Read frame length")
|
||||
|
||||
frame.Payload = make([]byte, frame.Length)
|
||||
total := 0
|
||||
@@ -223,7 +223,7 @@ func ReadFrame(reader io.Reader, addr net.Addr) (*Frame, error) {
|
||||
WithField("client", addr).
|
||||
WithField("total", total).
|
||||
WithField("length", frame.Length).
|
||||
Debug("Reading frame content")
|
||||
Trace("Reading frame content")
|
||||
|
||||
if n == 0 {
|
||||
logrus.
|
||||
|
||||
+3
-8
@@ -526,6 +526,7 @@ func (c *Connector) cleanupBackendConnection(clientAddr net.Addr, serverAddress
|
||||
logrus.
|
||||
WithField("client", clientAddr).
|
||||
WithField("backendHostPort", backendHostPort).
|
||||
WithField("player", playerInfo).
|
||||
WithField("connectionCount", c.activeConnections.GetCount(backendHostPort)).
|
||||
Info("Closed connection to backend")
|
||||
if checkScaleDown && c.scaleActiveConnections.GetCount(scalingTarget) <= 0 {
|
||||
@@ -659,18 +660,12 @@ func (c *Connector) findAndConnectBackend(frontendConn net.Conn,
|
||||
}
|
||||
}
|
||||
|
||||
if nextState == mcproto.StateStatus {
|
||||
// Previously gated on `waker != nil` so only auto-scale routes
|
||||
// got a predefined response. For static fleets (no waker) clients
|
||||
// just saw a closed connection on backend-down. Now any known
|
||||
// route whose backend dial fails returns the configured asleep/
|
||||
// loading MOTD — falls back to the global AUTO_SCALE_ASLEEP_MOTD
|
||||
// when no per-route override is set.
|
||||
if waker != nil && nextState == mcproto.StateStatus {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"client": clientAddr,
|
||||
"server": serverAddress,
|
||||
"isLegacy": isLegacy,
|
||||
}).Debug("Backend unreachable: serving predefined status response")
|
||||
}).Debug("Scalable backend unreachable: serving predefined status response")
|
||||
|
||||
br := bufio.NewReader(frontendConn)
|
||||
if isLegacy {
|
||||
|
||||
@@ -482,6 +482,9 @@ func (w *dockerWatcherImpl) listContainers(ctx context.Context) ([]*routableCont
|
||||
if !data.notRunning {
|
||||
endpoint = fmt.Sprintf("%s:%d", data.ip, data.port)
|
||||
}
|
||||
logrus.WithField("backendEndpoint", endpoint).
|
||||
WithField("containerID", container.ID).
|
||||
Debug("Found routable Docker container")
|
||||
|
||||
for _, host := range data.hosts {
|
||||
result = append(result, &routableContainer{
|
||||
|
||||
Reference in New Issue
Block a user