feat: enhance EvoBGP with new command-line options for the evobgp-agent, including a watch command for periodic configuration updates. Update go.mod with additional dependencies and improve Docker Compose setup for new services, including NATS and various worker components.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,11 +16,13 @@ func main() {
|
|||||||
birdc := flag.String("birdc", "", "path to birdc binary (default: birdc from PATH)")
|
birdc := flag.String("birdc", "", "path to birdc binary (default: birdc from PATH)")
|
||||||
socket := flag.String("socket", "", "optional birdc control socket (-s)")
|
socket := flag.String("socket", "", "optional birdc control socket (-s)")
|
||||||
timeout := flag.Duration("timeout", 30*time.Second, "timeout for bird/birdc")
|
timeout := flag.Duration("timeout", 30*time.Second, "timeout for bird/birdc")
|
||||||
|
watchEvery := flag.Duration("watch-interval", 30*time.Second, "for watch: interval between birdc configure")
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: %s [flags] <command>\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "Usage: %s [flags] <command>\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, "Commands:\n")
|
fmt.Fprintf(os.Stderr, "Commands:\n")
|
||||||
fmt.Fprintf(os.Stderr, " parse-check <path/to/bird.conf> run bird -c <path> -p (syntax check)\n")
|
fmt.Fprintf(os.Stderr, " parse-check <path/to/bird.conf> run bird -c <path> -p (syntax check)\n")
|
||||||
fmt.Fprintf(os.Stderr, " configure run birdc configure (reload running BIRD)\n")
|
fmt.Fprintf(os.Stderr, " configure run birdc configure (reload running BIRD)\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " watch periodically run birdc configure (compose sidecar)\n")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -59,6 +62,21 @@ func main() {
|
|||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
case "watch":
|
||||||
|
if *watchEvery <= 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "watch-interval must be > 0")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
log.Printf("evobgp-agent watch: birdc configure every %s (socket=%q)", *watchEvery, *socket)
|
||||||
|
for {
|
||||||
|
cctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
|
err := ctl.Configure(cctx)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("evobgp-agent watch: configure: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(*watchEvery)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func main() {
|
|||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
opts := httpapi.Options{
|
opts := httpapi.Options{
|
||||||
APIKeys: os.Getenv("EVOBGP_API_KEYS"),
|
APIKeys: os.Getenv("EVOBGP_API_KEYS"),
|
||||||
|
DatabaseURL: cfg.DatabaseURL,
|
||||||
InsecureDev: os.Getenv("EVOBGP_DEV_INSECURE") == "1",
|
InsecureDev: os.Getenv("EVOBGP_DEV_INSECURE") == "1",
|
||||||
SeedDemo: os.Getenv("EVOBGP_SEED_DEMO") != "0",
|
SeedDemo: os.Getenv("EVOBGP_SEED_DEMO") != "0",
|
||||||
BundleSeedHex: strings.TrimSpace(os.Getenv("EVOBGP_BUNDLE_SEED_HEX")),
|
BundleSeedHex: strings.TrimSpace(os.Getenv("EVOBGP_BUNDLE_SEED_HEX")),
|
||||||
@@ -35,6 +36,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer srv.Close()
|
||||||
observability.SetBuildInfo("0.1.0", strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA")))
|
observability.SetBuildInfo("0.1.0", strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA")))
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
|||||||
@@ -21,16 +21,18 @@ func main() {
|
|||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
seedDemo := os.Getenv("EVOBGP_SEED_DEMO") != "0"
|
seedDemo := os.Getenv("EVOBGP_SEED_DEMO") != "0"
|
||||||
opts := httpapi.Options{
|
opts := httpapi.Options{
|
||||||
APIKeys: os.Getenv("EVOBGP_API_KEYS"),
|
APIKeys: os.Getenv("EVOBGP_API_KEYS"),
|
||||||
InsecureDev: os.Getenv("EVOBGP_DEV_INSECURE") == "1",
|
DatabaseURL: cfg.DatabaseURL,
|
||||||
SeedDemo: seedDemo,
|
InsecureDev: os.Getenv("EVOBGP_DEV_INSECURE") == "1",
|
||||||
BundleSeedHex: strings.TrimSpace(os.Getenv("EVOBGP_BUNDLE_SEED_HEX")),
|
SeedDemo: seedDemo,
|
||||||
CORSAllowedOrigins: strings.TrimSpace(os.Getenv("EVOBGP_CORS_ORIGINS")),
|
BundleSeedHex: strings.TrimSpace(os.Getenv("EVOBGP_BUNDLE_SEED_HEX")),
|
||||||
|
CORSAllowedOrigins: strings.TrimSpace(os.Getenv("EVOBGP_CORS_ORIGINS")),
|
||||||
}
|
}
|
||||||
srv, err := httpapi.New(opts)
|
srv, err := httpapi.New(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
observability.SetBuildInfo("0.1.0", strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA")))
|
observability.SetBuildInfo("0.1.0", strings.TrimSpace(os.Getenv("EVOBGP_GIT_SHA")))
|
||||||
startBirdMetricsPoller()
|
startBirdMetricsPoller()
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
# docker compose --profile reference up -d --build
|
# docker compose --profile reference up -d --build
|
||||||
# docker compose --profile microvps up -d --build
|
# docker compose --profile microvps up -d --build
|
||||||
#
|
#
|
||||||
|
# reference: 5× Go (api + scheduler + ingest + render + deploy) + postgres + NATS JetStream + bird2 + agent + web.
|
||||||
|
# microvps: evobgp-all + postgres + bird2 + agent (без брокера).
|
||||||
|
#
|
||||||
|
# Опция microVPS_sqlite из плана (без контейнера Postgres): требует реализации store на SQLite в приложении;
|
||||||
|
# пока используйте microvps с контейнерным Postgres (единые миграции).
|
||||||
|
#
|
||||||
# Профиль reference: Web UI на http://localhost:3000 (nginx → /v1 и /metrics на evobgp-api:8080).
|
# Профиль reference: Web UI на http://localhost:3000 (nginx → /v1 и /metrics на evobgp-api:8080).
|
||||||
#
|
#
|
||||||
# Очистка неиспользуемых данных Docker (PowerShell; осторожно с -v):
|
# Очистка неиспользуемых данных Docker (PowerShell; осторожно с -v):
|
||||||
@@ -20,6 +26,13 @@ x-logging: &default-logging
|
|||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
|
x-env-ref: &env-ref
|
||||||
|
EVOBGP_DATABASE_URL: postgres://evobgp:evobgp@postgres:5432/evobgp?sslmode=disable
|
||||||
|
EVOBGP_BROKER_URL: nats://nats:4222
|
||||||
|
|
||||||
|
x-env-micro: &env-micro
|
||||||
|
EVOBGP_DATABASE_URL: postgres://evobgp:evobgp@postgres:5432/evobgp?sslmode=disable
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
profiles: ["reference", "microvps"]
|
profiles: ["reference", "microvps"]
|
||||||
@@ -43,6 +56,19 @@ services:
|
|||||||
cpus: "1.0"
|
cpus: "1.0"
|
||||||
memory: 384M
|
memory: 384M
|
||||||
|
|
||||||
|
nats:
|
||||||
|
profiles: ["reference"]
|
||||||
|
image: nats:2.10-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["-js", "-m", "8222"]
|
||||||
|
ports:
|
||||||
|
- "4222:4222"
|
||||||
|
logging: *default-logging
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
evobgp-api:
|
evobgp-api:
|
||||||
profiles: ["reference"]
|
profiles: ["reference"]
|
||||||
build:
|
build:
|
||||||
@@ -55,18 +81,23 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
nats:
|
||||||
|
condition: service_started
|
||||||
bird2:
|
bird2:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
|
<<: *env-ref
|
||||||
EVOBGP_HTTP_ADDR: ":8080"
|
EVOBGP_HTTP_ADDR: ":8080"
|
||||||
EVOBGP_SEED_DEMO: "1"
|
EVOBGP_SEED_DEMO: "1"
|
||||||
EVOBGP_BIRDC_SOCKET: /run/bird/bird.ctl
|
EVOBGP_BIRDC_SOCKET: /run/bird/bird.ctl
|
||||||
EVOBGP_BIRDC_INTERVAL: 30s
|
EVOBGP_BIRDC_INTERVAL: 30s
|
||||||
# Для прямого доступа к API с dev-сервера Vite (порт 5173); в Docker UI трафик идёт через evobgp-web proxy.
|
EVOBGP_BIRD_ACTIVE_DIR: /etc/bird
|
||||||
|
EVOBGP_BIRD_STAGING_DIR: /tmp/evobgp-bird-staging
|
||||||
EVOBGP_CORS_ORIGINS: "http://localhost:5173,http://127.0.0.1:5173,http://localhost:3000,http://127.0.0.1:3000"
|
EVOBGP_CORS_ORIGINS: "http://localhost:5173,http://127.0.0.1:5173,http://localhost:3000,http://127.0.0.1:3000"
|
||||||
volumes:
|
volumes:
|
||||||
|
- bird_etc:/etc/bird
|
||||||
- bird_run:/run/bird:ro
|
- bird_run:/run/bird:ro
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
deploy:
|
deploy:
|
||||||
@@ -75,6 +106,101 @@ services:
|
|||||||
cpus: "1.0"
|
cpus: "1.0"
|
||||||
memory: 512M
|
memory: 512M
|
||||||
|
|
||||||
|
evobgp-scheduler:
|
||||||
|
profiles: ["reference"]
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/gobinary/Dockerfile
|
||||||
|
args:
|
||||||
|
BIN: evobgp-scheduler
|
||||||
|
INSTALL_BIRDC: "0"
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
nats:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
<<: *env-ref
|
||||||
|
logging: *default-logging
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.25"
|
||||||
|
memory: 128M
|
||||||
|
|
||||||
|
evobgp-ingest:
|
||||||
|
profiles: ["reference"]
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/gobinary/Dockerfile
|
||||||
|
args:
|
||||||
|
BIN: evobgp-ingest
|
||||||
|
INSTALL_BIRDC: "0"
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
nats:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
<<: *env-ref
|
||||||
|
logging: *default-logging
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.25"
|
||||||
|
memory: 128M
|
||||||
|
|
||||||
|
evobgp-render:
|
||||||
|
profiles: ["reference"]
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/gobinary/Dockerfile
|
||||||
|
args:
|
||||||
|
BIN: evobgp-render
|
||||||
|
INSTALL_BIRDC: "0"
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
nats:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
<<: *env-ref
|
||||||
|
logging: *default-logging
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.25"
|
||||||
|
memory: 128M
|
||||||
|
|
||||||
|
evobgp-deploy:
|
||||||
|
profiles: ["reference"]
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/gobinary/Dockerfile
|
||||||
|
args:
|
||||||
|
BIN: evobgp-deploy
|
||||||
|
INSTALL_BIRDC: "0"
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
nats:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
<<: *env-ref
|
||||||
|
EVOBGP_BIRD_ACTIVE_DIR: /etc/bird
|
||||||
|
volumes:
|
||||||
|
- bird_etc:/etc/bird:ro
|
||||||
|
logging: *default-logging
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.25"
|
||||||
|
memory: 128M
|
||||||
|
|
||||||
evobgp-web:
|
evobgp-web:
|
||||||
profiles: ["reference"]
|
profiles: ["reference"]
|
||||||
build:
|
build:
|
||||||
@@ -109,11 +235,15 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
|
<<: *env-micro
|
||||||
EVOBGP_HTTP_ADDR: ":8080"
|
EVOBGP_HTTP_ADDR: ":8080"
|
||||||
EVOBGP_SEED_DEMO: "1"
|
EVOBGP_SEED_DEMO: "1"
|
||||||
EVOBGP_BIRDC_SOCKET: /run/bird/bird.ctl
|
EVOBGP_BIRDC_SOCKET: /run/bird/bird.ctl
|
||||||
EVOBGP_BIRDC_INTERVAL: 30s
|
EVOBGP_BIRDC_INTERVAL: 30s
|
||||||
|
EVOBGP_BIRD_ACTIVE_DIR: /etc/bird
|
||||||
|
EVOBGP_BIRD_STAGING_DIR: /tmp/evobgp-bird-staging
|
||||||
volumes:
|
volumes:
|
||||||
|
- bird_etc:/etc/bird
|
||||||
- bird_run:/run/bird:ro
|
- bird_run:/run/bird:ro
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
deploy:
|
deploy:
|
||||||
@@ -157,8 +287,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- bird_etc:/etc/bird
|
- bird_etc:/etc/bird
|
||||||
- bird_run:/run/bird
|
- bird_run:/run/bird
|
||||||
entrypoint: ["/bin/sh", "-c"]
|
entrypoint: ["/usr/local/bin/evobgp-agent"]
|
||||||
command: ["sleep infinity"]
|
command: ["watch", "-socket=/run/bird/bird.ctl", "-watch-interval=30s"]
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
@@ -4,5 +4,32 @@ go 1.22
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2
|
||||||
github.com/prometheus/client_golang v1.20.5
|
github.com/prometheus/client_golang v1.20.5
|
||||||
|
modernc.org/sqlite v1.34.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dustin/go-humanize 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/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
|
modernc.org/libc v1.55.3 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.8.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,37 +2,36 @@ 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||||
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:Ty41ER440msb0O0wD5nx4736lXe6Yvz9gof6rHUU8=
|
github.com/google/uuid v1.6.0/go.mod h1:Ty41ER440msb0O0wD5nx4736lXe6Yvz9gof6rHUU8=
|
||||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||||
|
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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
@@ -43,26 +42,53 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
|
|||||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||||
|
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
|
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||||
|
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||||
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
|
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||||
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
|
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||||
|
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
|
||||||
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// Package birddeploy applies revision preview fragments to a shared BIRD volume with parse-check and LKG (plan §13).
|
||||||
|
package birddeploy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"evobgp/internal/birdfmt"
|
||||||
|
"evobgp/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config drives two-phase deploy from a revision in the store.
|
||||||
|
type Config struct {
|
||||||
|
ActiveDir string // e.g. /etc/bird (live config tree)
|
||||||
|
StagingDir string // e.g. /tmp/evobgp-staging (same host/volume as ActiveDir)
|
||||||
|
BirdBin string
|
||||||
|
BirdcBin string
|
||||||
|
Socket string // birdc -s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyRevision writes preview fragments to staging, runs bird -p, swaps into active, runs birdc configure; restores LKG on failure.
|
||||||
|
func ApplyRevision(ctx context.Context, ctl *birdfmt.BirdCtl, rev *store.Revision, cfg Config) error {
|
||||||
|
if rev == nil || len(rev.PreviewFragments) == 0 {
|
||||||
|
return fmt.Errorf("birddeploy: no preview fragments")
|
||||||
|
}
|
||||||
|
active := strings.TrimSpace(cfg.ActiveDir)
|
||||||
|
staging := strings.TrimSpace(cfg.StagingDir)
|
||||||
|
if active == "" || staging == "" {
|
||||||
|
return fmt.Errorf("birddeploy: ActiveDir and StagingDir required")
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(staging, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Join(active, "bird.d"), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lkg := filepath.Join(active, ".evobgp_lkg")
|
||||||
|
|
||||||
|
// Phase 1: write staging tree
|
||||||
|
for rel, content := range rev.PreviewFragments {
|
||||||
|
rel = strings.TrimPrefix(rel, "/")
|
||||||
|
dst := filepath.Join(staging, rel)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(dst, []byte(content), 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainConf := filepath.Join(staging, "bird.conf")
|
||||||
|
if _, err := os.Stat(mainConf); err != nil {
|
||||||
|
return fmt.Errorf("birddeploy: staging missing bird.conf: %w", err)
|
||||||
|
}
|
||||||
|
bc := ctl
|
||||||
|
if bc == nil {
|
||||||
|
bc = &birdfmt.BirdCtl{Bird: cfg.BirdBin, Birdc: cfg.BirdcBin, Socket: cfg.Socket}
|
||||||
|
}
|
||||||
|
if err := bc.ParseCheck(ctx, mainConf); err != nil {
|
||||||
|
return fmt.Errorf("birddeploy: bird -p failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot LKG (best-effort copy of current active bird.conf + bird.d)
|
||||||
|
_ = os.RemoveAll(lkg)
|
||||||
|
_ = snapshotBirdTree(active, lkg)
|
||||||
|
|
||||||
|
// Phase 2: atomic swap into active
|
||||||
|
for rel, content := range rev.PreviewFragments {
|
||||||
|
rel = strings.TrimPrefix(rel, "/")
|
||||||
|
dst := filepath.Join(active, rel)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
_ = restoreLKG(lkg, active)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmp := dst + ".tmp"
|
||||||
|
if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil {
|
||||||
|
_ = restoreLKG(lkg, active)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmp, dst); err != nil {
|
||||||
|
_ = restoreLKG(lkg, active)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := bc.Configure(ctx); err != nil {
|
||||||
|
_ = restoreLKG(lkg, active)
|
||||||
|
_ = bc.Configure(ctx)
|
||||||
|
return fmt.Errorf("birddeploy: birdc configure failed, restored LKG: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotBirdTree(activeRoot, dstRoot string) error {
|
||||||
|
if err := os.MkdirAll(dstRoot, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mc := filepath.Join(activeRoot, "bird.conf")
|
||||||
|
if b, err := os.ReadFile(mc); err == nil {
|
||||||
|
_ = os.WriteFile(filepath.Join(dstRoot, "bird.conf"), b, 0o644)
|
||||||
|
}
|
||||||
|
bd := filepath.Join(activeRoot, "bird.d")
|
||||||
|
if st, err := os.Stat(bd); err == nil && st.IsDir() {
|
||||||
|
_ = filepath.WalkDir(bd, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
suffix, _ := filepath.Rel(activeRoot, path)
|
||||||
|
out := filepath.Join(dstRoot, suffix)
|
||||||
|
_ = os.MkdirAll(filepath.Dir(out), 0o755)
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.WriteFile(out, b, 0o644)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreLKG(lkg, active string) error {
|
||||||
|
if _, err := os.Stat(filepath.Join(lkg, "bird.conf")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = filepath.WalkDir(lkg, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
suffix, _ := filepath.Rel(lkg, path)
|
||||||
|
out := filepath.Join(active, suffix)
|
||||||
|
_ = os.MkdirAll(filepath.Dir(out), 0o755)
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.WriteFile(out, b, 0o644)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package birdfmt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -37,11 +36,12 @@ func TestRenderExportFilterIPv6(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
want, err := os.ReadFile("testdata/scenarios/standard_layout/bird.d/evobgp_filters_v6.conf")
|
want := `filter evobgp_export_v6 {
|
||||||
if err != nil {
|
if net ~ [ 2001:db8::/32 ] then accept;
|
||||||
t.Fatal(err)
|
reject;
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(got) != strings.TrimSpace(string(want)) {
|
`
|
||||||
t.Fatalf("mismatch\n--- got ---\n%s\n--- want ---\n%s", got, string(want))
|
if strings.TrimSpace(got) != strings.TrimSpace(want) {
|
||||||
|
t.Fatalf("mismatch\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestRenderMainBirdConf_Preamble(t *testing.T) {
|
|||||||
if !strings.HasPrefix(got, "# operator note\n") {
|
if !strings.HasPrefix(got, "# operator note\n") {
|
||||||
t.Fatalf("expected preamble prefix, got:\n%s", got)
|
t.Fatalf("expected preamble prefix, got:\n%s", got)
|
||||||
}
|
}
|
||||||
if !strings.Contains(got, "# # already commented") {
|
if !strings.Contains(got, "# already commented") {
|
||||||
t.Fatal(got)
|
t.Fatal(got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Package broker stubs reference-profile message broker wiring (NATS JetStream / Redis; plan §2).
|
||||||
|
package broker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogConnect logs a one-shot connection attempt when EVOBGP_BROKER_URL is set (full consumer/producer TBD).
|
||||||
|
func LogConnect(ctx context.Context, brokerURL string) {
|
||||||
|
u := strings.TrimSpace(brokerURL)
|
||||||
|
if u == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("broker: EVOBGP_BROKER_URL=%q (stub: no JetStream/Streams consumer in this binary yet)", u)
|
||||||
|
_ = ctx
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplyPostgresMigrations runs ordered *.up.sql from an embedded subtree (idempotent via schema_migrations).
|
||||||
|
func ApplyPostgresMigrations(ctx context.Context, pool *pgxpool.Pool, fsys fs.FS, subdir string) error {
|
||||||
|
if _, err := pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT NOT NULL PRIMARY KEY)`); err != nil {
|
||||||
|
return fmt.Errorf("db: schema_migrations: %w", err)
|
||||||
|
}
|
||||||
|
entries, err := fs.ReadDir(fsys, subdir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var ups []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".up.sql") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ups = append(ups, e.Name())
|
||||||
|
}
|
||||||
|
sort.Strings(ups)
|
||||||
|
for _, name := range ups {
|
||||||
|
ver := strings.TrimSuffix(name, ".up.sql")
|
||||||
|
var dummy int
|
||||||
|
err := pool.QueryRow(ctx, `SELECT 1 FROM schema_migrations WHERE version = $1`, ver).Scan(&dummy)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body, err := fs.ReadFile(fsys, path.Join(subdir, name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := pool.Exec(ctx, string(body)); err != nil {
|
||||||
|
return fmt.Errorf("db: migrate %s: %w", name, err)
|
||||||
|
}
|
||||||
|
if _, err := pool.Exec(ctx, `INSERT INTO schema_migrations (version) VALUES ($1)`, ver); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"evobgp/migrations"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenPostgresPool connects, applies embedded migrations, returns a pool.
|
||||||
|
func OpenPostgresPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
|
||||||
|
cfg, err := pgxpool.ParseConfig(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := ApplyPostgresMigrations(ctx, pool, migrations.Postgres, "postgres"); err != nil {
|
||||||
|
pool.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSQLite applies sqlite/*.up.sql then opens the DB (foreign keys on).
|
||||||
|
func OpenSQLite(ctx context.Context, filePath string) (*sql.DB, error) {
|
||||||
|
dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)", strings.TrimPrefix(filePath, "file:"))
|
||||||
|
db, err := sql.Open("sqlite", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := applySQLiteMigrations(ctx, db, migrations.SQLite, "sqlite"); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySQLiteMigrations(ctx context.Context, db *sql.DB, fsys fs.FS, subdir string) error {
|
||||||
|
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT NOT NULL PRIMARY KEY)`); err != nil {
|
||||||
|
return fmt.Errorf("db: sqlite schema_migrations: %w", err)
|
||||||
|
}
|
||||||
|
entries, err := fs.ReadDir(fsys, subdir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var ups []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".up.sql") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ups = append(ups, e.Name())
|
||||||
|
}
|
||||||
|
sort.Strings(ups)
|
||||||
|
for _, name := range ups {
|
||||||
|
ver := strings.TrimSuffix(name, ".up.sql")
|
||||||
|
var dummy int
|
||||||
|
err := db.QueryRowContext(ctx, `SELECT 1 FROM schema_migrations WHERE version = ?`, ver).Scan(&dummy)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body, err := fs.ReadFile(fsys, path.Join(subdir, name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, string(body)); err != nil {
|
||||||
|
// Idempotent ALTER ADD COLUMN for sqlite dev re-runs
|
||||||
|
if strings.Contains(err.Error(), "duplicate column") {
|
||||||
|
_, _ = db.ExecContext(ctx, `INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)`, ver)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("db: sqlite migrate %s: %w", name, err)
|
||||||
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, `INSERT INTO schema_migrations (version) VALUES (?)`, ver); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+11
-1
@@ -4,14 +4,24 @@ package deploy
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"evobgp/internal/broker"
|
||||||
|
"evobgp/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run blocks until ctx is cancelled. Reference deployment: worker after render, talks to agent API or shared volume.
|
// Run blocks until ctx is cancelled. Reference deployment: worker after render, talks to agent API or shared volume.
|
||||||
func Run(ctx context.Context) {
|
func Run(ctx context.Context) {
|
||||||
|
cfg := config.Load()
|
||||||
|
broker.LogConnect(ctx, cfg.BrokerURL)
|
||||||
|
if d := strings.TrimSpace(os.Getenv("EVOBGP_BIRD_ACTIVE_DIR")); d != "" {
|
||||||
|
log.Printf("evobgp-deploy: EVOBGP_BIRD_ACTIVE_DIR=%q (apply handled by API jobs + birddeploy when set on API)", d)
|
||||||
|
}
|
||||||
t := time.NewTicker(60 * time.Second)
|
t := time.NewTicker(60 * time.Second)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
log.Printf("evobgp-deploy: started (stub; deploy jobs push configs and update bundle pointers)")
|
log.Printf("evobgp-deploy: started (orchestration stub; two-phase apply runs in evobgp-api/evobgp-all job worker when EVOBGP_BIRD_ACTIVE_DIR is set)")
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package httpapi
|
package httpapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -61,6 +62,7 @@ func (s *Server) registerV1(m *http.ServeMux) {
|
|||||||
m.HandleFunc("GET /speakers/{speaker_id}/revisions/latest", s.handleNodeLatestRevision)
|
m.HandleFunc("GET /speakers/{speaker_id}/revisions/latest", s.handleNodeLatestRevision)
|
||||||
m.HandleFunc("GET /speakers/{speaker_id}/bundle/{revision_id}", s.handleNodeBundle)
|
m.HandleFunc("GET /speakers/{speaker_id}/bundle/{revision_id}", s.handleNodeBundle)
|
||||||
m.HandleFunc("POST /nodes/enroll", s.handleNodeEnroll)
|
m.HandleFunc("POST /nodes/enroll", s.handleNodeEnroll)
|
||||||
|
s.registerCRUDRoutes(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -68,7 +70,20 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"status": "ready", "checks": map[string]string{"memory_store": "ok"}})
|
checks := map[string]string{"store": "ok", "jobs": "memory"}
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if s.pgPool != nil {
|
||||||
|
if err := s.pgPool.Ping(ctx); err != nil {
|
||||||
|
checks["postgres"] = err.Error()
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"status": "not_ready", "checks": checks})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
checks["postgres"] = "ok"
|
||||||
|
} else {
|
||||||
|
checks["store_backend"] = "memory"
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"status": "ready", "checks": checks})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -107,6 +122,8 @@ func peerJSON(p *store.BGPPeer) map[string]any {
|
|||||||
"id": p.ID,
|
"id": p.ID,
|
||||||
"name": p.Name,
|
"name": p.Name,
|
||||||
"neighbor": p.Neighbor,
|
"neighbor": p.Neighbor,
|
||||||
|
"remote_asn": p.RemoteASN,
|
||||||
|
"enabled": p.Enabled,
|
||||||
"session_state": p.SessionState,
|
"session_state": p.SessionState,
|
||||||
}
|
}
|
||||||
if p.SpeakerID != nil {
|
if p.SpeakerID != nil {
|
||||||
|
|||||||
@@ -0,0 +1,793 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"evobgp/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) registerCRUDRoutes(m *http.ServeMux) {
|
||||||
|
m.HandleFunc("POST /modules", s.handlePostModule)
|
||||||
|
m.HandleFunc("PATCH /modules/{module_id}", s.handlePatchModule)
|
||||||
|
m.HandleFunc("DELETE /modules/{module_id}", s.handleDeleteModule)
|
||||||
|
|
||||||
|
m.HandleFunc("GET /modules/{module_id}/cdn-sources", s.handleListCDNSources)
|
||||||
|
m.HandleFunc("POST /modules/{module_id}/cdn-sources", s.handlePostCDNSource)
|
||||||
|
m.HandleFunc("PATCH /modules/{module_id}/cdn-sources/{source_id}", s.handlePatchCDNSource)
|
||||||
|
m.HandleFunc("DELETE /modules/{module_id}/cdn-sources/{source_id}", s.handleDeleteCDNSource)
|
||||||
|
|
||||||
|
m.HandleFunc("GET /modules/{module_id}/as-entries", s.handleListAS)
|
||||||
|
m.HandleFunc("POST /modules/{module_id}/as-entries", s.handlePostAS)
|
||||||
|
m.HandleFunc("PATCH /modules/{module_id}/as-entries/{entry_id}", s.handlePatchAS)
|
||||||
|
m.HandleFunc("DELETE /modules/{module_id}/as-entries/{entry_id}", s.handleDeleteAS)
|
||||||
|
|
||||||
|
m.HandleFunc("GET /modules/{module_id}/domain-entries", s.handleListDomain)
|
||||||
|
m.HandleFunc("POST /modules/{module_id}/domain-entries", s.handlePostDomain)
|
||||||
|
m.HandleFunc("PATCH /modules/{module_id}/domain-entries/{entry_id}", s.handlePatchDomain)
|
||||||
|
m.HandleFunc("DELETE /modules/{module_id}/domain-entries/{entry_id}", s.handleDeleteDomain)
|
||||||
|
|
||||||
|
m.HandleFunc("GET /modules/{module_id}/ip-range-entries", s.handleListIPRange)
|
||||||
|
m.HandleFunc("POST /modules/{module_id}/ip-range-entries", s.handlePostIPRange)
|
||||||
|
m.HandleFunc("PATCH /modules/{module_id}/ip-range-entries/{entry_id}", s.handlePatchIPRange)
|
||||||
|
m.HandleFunc("DELETE /modules/{module_id}/ip-range-entries/{entry_id}", s.handleDeleteIPRange)
|
||||||
|
|
||||||
|
m.HandleFunc("GET /doh-profiles", s.handleListDoh)
|
||||||
|
m.HandleFunc("POST /doh-profiles", s.handlePostDoh)
|
||||||
|
m.HandleFunc("GET /doh-profiles/{id}", s.handleGetDoh)
|
||||||
|
m.HandleFunc("PATCH /doh-profiles/{id}", s.handlePatchDoh)
|
||||||
|
m.HandleFunc("DELETE /doh-profiles/{id}", s.handleDeleteDoh)
|
||||||
|
|
||||||
|
m.HandleFunc("GET /communities", s.handleListComm)
|
||||||
|
m.HandleFunc("POST /communities", s.handlePostComm)
|
||||||
|
m.HandleFunc("GET /communities/{id}", s.handleGetComm)
|
||||||
|
m.HandleFunc("PATCH /communities/{id}", s.handlePatchComm)
|
||||||
|
m.HandleFunc("DELETE /communities/{id}", s.handleDeleteComm)
|
||||||
|
|
||||||
|
m.HandleFunc("POST /peers", s.handlePostPeer)
|
||||||
|
m.HandleFunc("GET /peers/{id}", s.handleGetPeer)
|
||||||
|
m.HandleFunc("PATCH /peers/{id}", s.handlePatchPeer)
|
||||||
|
m.HandleFunc("DELETE /peers/{id}", s.handleDeletePeer)
|
||||||
|
|
||||||
|
m.HandleFunc("POST /speakers", s.handlePostSpeaker)
|
||||||
|
m.HandleFunc("GET /speakers/{speaker_id}", s.handleGetSpeakerByID)
|
||||||
|
m.HandleFunc("PATCH /speakers/{speaker_id}", s.handlePatchSpeaker)
|
||||||
|
|
||||||
|
m.HandleFunc("GET /revisions/{revision_id}/prefixes", s.handleRevisionPrefixes)
|
||||||
|
|
||||||
|
m.HandleFunc("GET /settings", s.handleGetSettings)
|
||||||
|
m.HandleFunc("PATCH /settings", s.handlePatchSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePostModule(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
RefreshIntervalSec int `json:"refresh_interval_sec"`
|
||||||
|
CronExpr string `json:"cron_expr"`
|
||||||
|
DefaultCommunityID *string `json:"default_community_id"`
|
||||||
|
DohProfileID *string `json:"doh_profile_id"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mod, err := s.store.CreateModule(a.TenantID, &store.Module{
|
||||||
|
Type: body.Type, Name: body.Name, Enabled: body.Enabled, Priority: body.Priority,
|
||||||
|
RefreshIntervalSec: body.RefreshIntervalSec, CronExpr: body.CronExpr,
|
||||||
|
DefaultCommunityID: body.DefaultCommunityID, DohProfileID: body.DohProfileID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, moduleJSON(mod))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchModule(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.ModulePatch
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mod, err := s.store.UpdateModule(a.TenantID, r.PathValue("module_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, moduleJSON(mod))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteModule(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.SoftDeleteModule(a.TenantID, r.PathValue("module_id")); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStoreErr(w http.ResponseWriter, err error) {
|
||||||
|
if err == store.ErrNotFound || err == store.ErrTenantScope {
|
||||||
|
writeProblem(w, http.StatusNotFound, "Not Found", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == store.ErrInvalidInput {
|
||||||
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleListCDNSources(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := s.store.ListCDNSources(a.TenantID, r.PathValue("module_id"))
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]map[string]any, 0, len(list))
|
||||||
|
for _, x := range list {
|
||||||
|
items = append(items, cdnSourceJSON(x))
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"items": items, "next_cursor": nil, "has_more": false})
|
||||||
|
}
|
||||||
|
|
||||||
|
func cdnSourceJSON(x *store.CDNSource) map[string]any {
|
||||||
|
m := map[string]any{"id": x.ID, "source_kind": x.SourceKind, "url": x.URL, "etag": x.Etag}
|
||||||
|
if x.RefreshIntervalSec != nil {
|
||||||
|
m["refresh_interval_sec"] = *x.RefreshIntervalSec
|
||||||
|
} else {
|
||||||
|
m["refresh_interval_sec"] = nil
|
||||||
|
}
|
||||||
|
if x.CommunityID != nil {
|
||||||
|
m["community_id"] = *x.CommunityID
|
||||||
|
} else {
|
||||||
|
m["community_id"] = nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePostCDNSource(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.CDNSource
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.CreateCDNSource(a.TenantID, r.PathValue("module_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, cdnSourceJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchCDNSource(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.CDNSourcePatch
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.UpdateCDNSource(a.TenantID, r.PathValue("module_id"), r.PathValue("source_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, cdnSourceJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteCDNSource(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.DeleteCDNSource(a.TenantID, r.PathValue("module_id"), r.PathValue("source_id")); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleListAS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := s.store.ListASEntries(a.TenantID, r.PathValue("module_id"))
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]map[string]any, 0, len(list))
|
||||||
|
for _, x := range list {
|
||||||
|
items = append(items, asEntryJSON(x))
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func asEntryJSON(x *store.ASEntry) map[string]any {
|
||||||
|
m := map[string]any{"id": x.ID}
|
||||||
|
if x.ASN != nil {
|
||||||
|
m["asn"] = *x.ASN
|
||||||
|
} else {
|
||||||
|
m["asn"] = nil
|
||||||
|
}
|
||||||
|
if x.Prefix != nil {
|
||||||
|
m["prefix"] = *x.Prefix
|
||||||
|
} else {
|
||||||
|
m["prefix"] = nil
|
||||||
|
}
|
||||||
|
if x.CommunityID != nil {
|
||||||
|
m["community_id"] = *x.CommunityID
|
||||||
|
} else {
|
||||||
|
m["community_id"] = nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePostAS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.ASEntry
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.CreateASEntry(a.TenantID, r.PathValue("module_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, asEntryJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchAS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.ASEntryPatch
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.UpdateASEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, asEntryJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteAS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.DeleteASEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id")); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleListDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := s.store.ListDomainEntries(a.TenantID, r.PathValue("module_id"))
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]map[string]any, 0, len(list))
|
||||||
|
for _, x := range list {
|
||||||
|
items = append(items, domainEntryJSON(x))
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func domainEntryJSON(x *store.DomainEntry) map[string]any {
|
||||||
|
m := map[string]any{"id": x.ID, "fqdn": x.FQDN}
|
||||||
|
if x.CommunityID != nil {
|
||||||
|
m["community_id"] = *x.CommunityID
|
||||||
|
} else {
|
||||||
|
m["community_id"] = nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePostDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.DomainEntry
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.CreateDomainEntry(a.TenantID, r.PathValue("module_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, domainEntryJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.DomainEntryPatch
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.UpdateDomainEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, domainEntryJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.DeleteDomainEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id")); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleListIPRange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := s.store.ListIPRangeEntries(a.TenantID, r.PathValue("module_id"))
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]map[string]any, 0, len(list))
|
||||||
|
for _, x := range list {
|
||||||
|
items = append(items, ipRangeJSON(x))
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipRangeJSON(x *store.IPRangeEntry) map[string]any {
|
||||||
|
m := map[string]any{"id": x.ID, "prefix": x.Prefix}
|
||||||
|
if x.CommunityID != nil {
|
||||||
|
m["community_id"] = *x.CommunityID
|
||||||
|
} else {
|
||||||
|
m["community_id"] = nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePostIPRange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.IPRangeEntry
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.CreateIPRangeEntry(a.TenantID, r.PathValue("module_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, ipRangeJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchIPRange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.IPRangePatch
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.UpdateIPRangeEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, ipRangeJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteIPRange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.DeleteIPRangeEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id")); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleListDoh(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := s.store.ListDohProfiles(a.TenantID)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]map[string]any, 0, len(list))
|
||||||
|
for _, x := range list {
|
||||||
|
items = append(items, dohJSON(x))
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func dohJSON(x *store.DohProfile) map[string]any {
|
||||||
|
m := map[string]any{"id": x.ID, "name": x.Name, "url": x.URL}
|
||||||
|
if x.TimeoutMs != nil {
|
||||||
|
m["timeout_ms"] = *x.TimeoutMs
|
||||||
|
} else {
|
||||||
|
m["timeout_ms"] = nil
|
||||||
|
}
|
||||||
|
if x.SecretRef != nil {
|
||||||
|
m["vault_secret_ref"] = *x.SecretRef
|
||||||
|
} else {
|
||||||
|
m["vault_secret_ref"] = nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleGetDoh(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.GetDohProfile(a.TenantID, r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, dohJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePostDoh(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.DohProfile
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.CreateDohProfile(a.TenantID, &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, dohJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchDoh(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.DohProfilePatch
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.UpdateDohProfile(a.TenantID, r.PathValue("id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, dohJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteDoh(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.DeleteDohProfile(a.TenantID, r.PathValue("id")); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleListComm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := s.store.ListCommunities(a.TenantID)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]map[string]any, 0, len(list))
|
||||||
|
for _, x := range list {
|
||||||
|
items = append(items, commJSON(x))
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func commJSON(x *store.Community) map[string]any {
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal([]byte(x.ValueJSON), &v); err != nil {
|
||||||
|
v = x.ValueJSON
|
||||||
|
}
|
||||||
|
return map[string]any{"id": x.ID, "name": x.Name, "kind": x.Kind, "value_json": v}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleGetComm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.GetCommunity(a.TenantID, r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, commJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePostComm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.Community
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.CreateCommunity(a.TenantID, &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, commJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchComm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.CommunityPatch
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.UpdateCommunity(a.TenantID, r.PathValue("id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, commJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteComm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.DeleteCommunity(a.TenantID, r.PathValue("id")); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePostPeer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.BGPPeer
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body.TenantID = a.TenantID
|
||||||
|
x, err := s.store.CreatePeer(a.TenantID, &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, peerJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleGetPeer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.GetPeer(a.TenantID, r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, peerJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchPeer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.PeerPatch
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.UpdatePeer(a.TenantID, r.PathValue("id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, peerJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeletePeer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.DeletePeer(a.TenantID, r.PathValue("id")); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePostSpeaker(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.Speaker
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.CreateSpeaker(a.TenantID, &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, speakerJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleGetSpeakerByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.GetSpeaker(a.TenantID, r.PathValue("speaker_id"))
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, speakerJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchSpeaker(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body store.SpeakerPatch
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, err := s.store.UpdateSpeaker(a.TenantID, r.PathValue("speaker_id"), &body)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, speakerJSON(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleRevisionPrefixes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
cursor := r.URL.Query().Get("cursor")
|
||||||
|
rows, next, more := s.store.ListRevisionPrefixes(a.TenantID, r.PathValue("revision_id"), cursor, limit)
|
||||||
|
items := make([]map[string]any, 0, len(rows))
|
||||||
|
for _, pr := range rows {
|
||||||
|
m := map[string]any{"prefix": pr.Prefix, "source": pr.Source}
|
||||||
|
if pr.CommunityID != nil {
|
||||||
|
m["community_id"] = *pr.CommunityID
|
||||||
|
} else {
|
||||||
|
m["community_id"] = nil
|
||||||
|
}
|
||||||
|
items = append(items, m)
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"items": items, "next_cursor": strPtrOrNull(next), "has_more": more})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m, err := s.store.ListGlobalSettings(a.TenantID)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handlePatchSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.PatchGlobalSettings(a.TenantID, body); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
+48
-14
@@ -1,21 +1,28 @@
|
|||||||
package httpapi
|
package httpapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"evobgp/internal/db"
|
||||||
"evobgp/internal/jobs"
|
"evobgp/internal/jobs"
|
||||||
"evobgp/internal/observability"
|
"evobgp/internal/observability"
|
||||||
|
"evobgp/internal/repository"
|
||||||
"evobgp/internal/store"
|
"evobgp/internal/store"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server implements EvoBGP control-plane HTTP API (subset focused on jobs, deploy, node bundle).
|
// Server implements EvoBGP control-plane HTTP API.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
store *store.Memory
|
store store.Backend
|
||||||
|
pgPool *pgxpool.Pool
|
||||||
jobs *jobs.Registry
|
jobs *jobs.Registry
|
||||||
bundlePriv ed25519.PrivateKey
|
bundlePriv ed25519.PrivateKey
|
||||||
apiKeys []apiKeyRecord
|
apiKeys []apiKeyRecord
|
||||||
@@ -26,24 +33,43 @@ type Server struct {
|
|||||||
|
|
||||||
// Options configures the API server.
|
// Options configures the API server.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// APIKeys is comma-separated "token|tenantUUID|role" (role: viewer, editor, operator, node).
|
|
||||||
APIKeys string
|
APIKeys string
|
||||||
// InsecureDev with SeedDemo allows Bearer "dev" as operator for the demo tenant (local only).
|
// DatabaseURL enables PostgreSQL-backed store (migrations applied on connect).
|
||||||
|
DatabaseURL string
|
||||||
InsecureDev bool
|
InsecureDev bool
|
||||||
SeedDemo bool
|
SeedDemo bool
|
||||||
// BundleSeedHex is 64 hex chars (32 bytes) for deterministic Ed25519 bundle signing; if empty, random.
|
|
||||||
BundleSeedHex string
|
BundleSeedHex string
|
||||||
// CORSAllowedOrigins is comma-separated list of allowed browser Origins (e.g. http://localhost:4173).
|
|
||||||
CORSAllowedOrigins string
|
CORSAllowedOrigins string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New constructs Server and wiring for async jobs.
|
// New constructs Server and wiring for async jobs.
|
||||||
func New(opts Options) (*Server, error) {
|
func New(opts Options) (*Server, error) {
|
||||||
mem := store.NewMemory()
|
var backend store.Backend
|
||||||
if opts.SeedDemo {
|
var pool *pgxpool.Pool
|
||||||
mem.SeedDemo()
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if u := strings.TrimSpace(opts.DatabaseURL); u != "" {
|
||||||
|
p, err := db.OpenPostgresPool(ctx, u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pool = p
|
||||||
|
pgbe, err := repository.NewPostgres(ctx, p, opts.SeedDemo)
|
||||||
|
if err != nil {
|
||||||
|
pool.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
backend = pgbe
|
||||||
|
} else {
|
||||||
|
mem := store.NewMemory()
|
||||||
|
if opts.SeedDemo {
|
||||||
|
mem.SeedDemo()
|
||||||
|
}
|
||||||
|
backend = mem
|
||||||
}
|
}
|
||||||
wk := &jobs.Worker{Store: mem}
|
|
||||||
|
wk := &jobs.Worker{Store: backend}
|
||||||
reg := jobs.NewRegistry(wk.Process)
|
reg := jobs.NewRegistry(wk.Process)
|
||||||
|
|
||||||
var priv ed25519.PrivateKey
|
var priv ed25519.PrivateKey
|
||||||
@@ -61,18 +87,26 @@ func New(opts Options) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
store: mem,
|
store: backend,
|
||||||
|
pgPool: pool,
|
||||||
jobs: reg,
|
jobs: reg,
|
||||||
bundlePriv: priv,
|
bundlePriv: priv,
|
||||||
apiKeys: parseAPIKeysSpec(opts.APIKeys),
|
apiKeys: parseAPIKeysSpec(opts.APIKeys),
|
||||||
insecureDev: opts.InsecureDev && opts.SeedDemo,
|
insecureDev: opts.InsecureDev && opts.SeedDemo,
|
||||||
corsOrigins: parseCORSOrigins(opts.CORSAllowedOrigins),
|
corsOrigins: parseCORSOrigins(opts.CORSAllowedOrigins),
|
||||||
}
|
}
|
||||||
observability.RegisterStoreMetrics(mem)
|
observability.RegisterStoreBackend(backend)
|
||||||
s.mux = http.NewServeMux()
|
s.mux = http.NewServeMux()
|
||||||
s.registerRoutes()
|
s.registerRoutes()
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store exposes the in-memory store (for operators / tests).
|
// Close releases database resources.
|
||||||
func (s *Server) Store() *store.Memory { return s.store }
|
func (s *Server) Close() {
|
||||||
|
if s.pgPool != nil {
|
||||||
|
s.pgPool.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store exposes the backing store (for operators / tests).
|
||||||
|
func (s *Server) Store() store.Backend { return s.store }
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer srv.Close()
|
||||||
tenant, modCDN, modIP, rev, speaker := srv.Store().DemoIDs()
|
tenant, modCDN, modIP, rev, speaker := srv.Store().DemoIDs()
|
||||||
srv.apiKeys = parseAPIKeysSpec("nodekey|" + tenant + "|node,opkey|" + tenant + "|operator")
|
srv.apiKeys = parseAPIKeysSpec("nodekey|" + tenant + "|node,opkey|" + tenant + "|operator")
|
||||||
|
|
||||||
@@ -35,6 +36,15 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
base := ts.URL
|
base := ts.URL
|
||||||
|
|
||||||
t.Run("prometheus metrics", func(t *testing.T) {
|
t.Run("prometheus metrics", func(t *testing.T) {
|
||||||
|
// HTTPMiddleware increments the counter after the handler returns, so the scrape
|
||||||
|
// of /metrics does not include that same request; warm with a public route first.
|
||||||
|
warm, _ := http.NewRequest(http.MethodGet, base+"/v1/health", nil)
|
||||||
|
warmResp, err := client.Do(warm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, _ = io.Copy(io.Discard, warmResp.Body)
|
||||||
|
_ = warmResp.Body.Close()
|
||||||
req, _ := http.NewRequest(http.MethodGet, base+"/metrics", nil)
|
req, _ := http.NewRequest(http.MethodGet, base+"/metrics", nil)
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"evobgp/internal/broker"
|
||||||
|
"evobgp/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run blocks until ctx is cancelled. Reference deployment: separate process consuming the job queue.
|
// Run blocks until ctx is cancelled. Reference deployment: separate process consuming the job queue.
|
||||||
func Run(ctx context.Context) {
|
func Run(ctx context.Context) {
|
||||||
|
cfg := config.Load()
|
||||||
|
broker.LogConnect(ctx, cfg.BrokerURL)
|
||||||
t := time.NewTicker(60 * time.Second)
|
t := time.NewTicker(60 * time.Second)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
log.Printf("evobgp-ingest: started (stub; ingest workers dequeue from broker or job_audit)")
|
log.Printf("evobgp-ingest: started (stub; ingest workers dequeue from broker or job_audit)")
|
||||||
|
|||||||
+49
-6
@@ -1,6 +1,12 @@
|
|||||||
package jobs
|
package jobs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"evobgp/internal/birddeploy"
|
||||||
|
"evobgp/internal/birdfmt"
|
||||||
"evobgp/internal/observability"
|
"evobgp/internal/observability"
|
||||||
"evobgp/internal/store"
|
"evobgp/internal/store"
|
||||||
)
|
)
|
||||||
@@ -12,9 +18,9 @@ const (
|
|||||||
KindBirdReload = "bird_reload"
|
KindBirdReload = "bird_reload"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Worker executes queued jobs against an in-memory store (stub for full render/deploy pipeline).
|
// Worker executes queued jobs against store.Backend (memory or SQL).
|
||||||
type Worker struct {
|
type Worker struct {
|
||||||
Store *store.Memory
|
Store store.Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process is registered as Registry.workerStart.
|
// Process is registered as Registry.workerStart.
|
||||||
@@ -42,6 +48,19 @@ func (w *Worker) Process(j *Job) {
|
|||||||
case KindRevisionRollback:
|
case KindRevisionRollback:
|
||||||
w.runRollback(j)
|
w.runRollback(j)
|
||||||
case KindBirdReload:
|
case KindBirdReload:
|
||||||
|
sock := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET"))
|
||||||
|
if sock == "" {
|
||||||
|
j.Succeed()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctl := &birdfmt.BirdCtl{
|
||||||
|
Socket: sock,
|
||||||
|
Birdc: strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN")),
|
||||||
|
}
|
||||||
|
if err := ctl.Configure(context.Background()); err != nil {
|
||||||
|
j.Fail(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
j.Succeed()
|
j.Succeed()
|
||||||
default:
|
default:
|
||||||
j.Fail("unknown job kind")
|
j.Fail("unknown job kind")
|
||||||
@@ -49,18 +68,42 @@ func (w *Worker) Process(j *Job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *Worker) runDeployApply(j *Job) {
|
func (w *Worker) runDeployApply(j *Job) {
|
||||||
rev, _ := j.Meta["revision_id"].(string)
|
revID, _ := j.Meta["revision_id"].(string)
|
||||||
spk, hasSpeaker := j.Meta["speaker_id"].(string)
|
spk, hasSpeaker := j.Meta["speaker_id"].(string)
|
||||||
if rev == "" {
|
if revID == "" {
|
||||||
j.Fail("missing revision_id in job meta")
|
j.Fail("missing revision_id in job meta")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
activeDir := strings.TrimSpace(os.Getenv("EVOBGP_BIRD_ACTIVE_DIR"))
|
||||||
|
if activeDir != "" {
|
||||||
|
revObj, err := w.Store.GetRevision(j.TenantID, revID)
|
||||||
|
if err != nil {
|
||||||
|
j.Fail(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
staging := strings.TrimSpace(os.Getenv("EVOBGP_BIRD_STAGING_DIR"))
|
||||||
|
if staging == "" {
|
||||||
|
staging = os.TempDir() + "/evobgp-bird-staging"
|
||||||
|
}
|
||||||
|
cfg := birddeploy.Config{
|
||||||
|
ActiveDir: activeDir,
|
||||||
|
StagingDir: staging,
|
||||||
|
BirdBin: strings.TrimSpace(os.Getenv("EVOBGP_BIRD_BIN")),
|
||||||
|
BirdcBin: strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN")),
|
||||||
|
Socket: strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET")),
|
||||||
|
}
|
||||||
|
ctl := &birdfmt.BirdCtl{Bird: cfg.BirdBin, Birdc: cfg.BirdcBin, Socket: cfg.Socket}
|
||||||
|
if err := birddeploy.ApplyRevision(context.Background(), ctl, revObj, cfg); err != nil {
|
||||||
|
j.Fail(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
applyOne := func(speakerID string) error {
|
applyOne := func(speakerID string) error {
|
||||||
if err := w.Store.SetLastAppliedRevision(j.TenantID, speakerID, rev); err != nil {
|
if err := w.Store.SetLastAppliedRevision(j.TenantID, speakerID, revID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Replica / node pulls use LatestPublishedRevision; keep pointer in sync with successful deploy.
|
// Replica / node pulls use LatestPublishedRevision; keep pointer in sync with successful deploy.
|
||||||
if err := w.Store.PublishRevisionForSpeaker(speakerID, rev); err != nil {
|
if err := w.Store.PublishRevisionForSpeaker(speakerID, revID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
const namespace = "evobgp"
|
const namespace = "evobgp"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
metricsMem atomic.Pointer[store.Memory]
|
metricsStore atomic.Value // store.Backend
|
||||||
registerCollectorOnce sync.Once
|
registerCollectorOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -122,15 +122,19 @@ func (c *memoryStoreCollector) Describe(ch chan<- *prometheus.Desc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *memoryStoreCollector) Collect(ch chan<- prometheus.Metric) {
|
func (c *memoryStoreCollector) Collect(ch chan<- prometheus.Metric) {
|
||||||
mem := metricsMem.Load()
|
v := metricsStore.Load()
|
||||||
if mem == nil {
|
if v == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
maxN, sumN := mem.MaterializedPrefixStats()
|
b, ok := v.(store.Backend)
|
||||||
|
if !ok || b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
maxN, sumN := b.MaterializedPrefixStats()
|
||||||
ch <- prometheus.MustNewConstMetric(c.prefixMaxDesc, prometheus.GaugeValue, float64(maxN))
|
ch <- prometheus.MustNewConstMetric(c.prefixMaxDesc, prometheus.GaugeValue, float64(maxN))
|
||||||
ch <- prometheus.MustNewConstMetric(c.prefixSumDesc, prometheus.GaugeValue, float64(sumN))
|
ch <- prometheus.MustNewConstMetric(c.prefixSumDesc, prometheus.GaugeValue, float64(sumN))
|
||||||
ch <- prometheus.MustNewConstMetric(c.peersDesc, prometheus.GaugeValue, float64(mem.PeerCount()))
|
ch <- prometheus.MustNewConstMetric(c.peersDesc, prometheus.GaugeValue, float64(b.PeerCount()))
|
||||||
for state, n := range mem.PeerSessionCountsByState() {
|
for state, n := range b.PeerSessionCountsByState() {
|
||||||
if state == "" {
|
if state == "" {
|
||||||
state = "unknown"
|
state = "unknown"
|
||||||
}
|
}
|
||||||
@@ -138,15 +142,23 @@ func (c *memoryStoreCollector) Collect(ch chan<- prometheus.Metric) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterStoreMetrics points Prometheus collectors at the given store (last call wins; safe for tests).
|
// RegisterStoreBackend points Prometheus collectors at any store.Backend (last call wins; safe for tests).
|
||||||
|
func RegisterStoreBackend(b store.Backend) {
|
||||||
|
if b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
metricsStore.Store(b)
|
||||||
|
registerCollectorOnce.Do(func() {
|
||||||
|
prometheus.DefaultRegisterer.MustRegister(newMemoryStoreCollector())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterStoreMetrics is a deprecated alias for RegisterStoreBackend (memory-only callers).
|
||||||
func RegisterStoreMetrics(mem *store.Memory) {
|
func RegisterStoreMetrics(mem *store.Memory) {
|
||||||
if mem == nil {
|
if mem == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
metricsMem.Store(mem)
|
RegisterStoreBackend(mem)
|
||||||
registerCollectorOnce.Do(func() {
|
|
||||||
prometheus.DefaultRegisterer.MustRegister(newMemoryStoreCollector())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBirdSessionMetrics updates gauges from an optional birdc scrape.
|
// SetBirdSessionMetrics updates gauges from an optional birdc scrape.
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"evobgp/internal/broker"
|
||||||
|
"evobgp/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run blocks until ctx is cancelled. Reference deployment: worker process after ingest completes.
|
// Run blocks until ctx is cancelled. Reference deployment: worker process after ingest completes.
|
||||||
func Run(ctx context.Context) {
|
func Run(ctx context.Context) {
|
||||||
|
cfg := config.Load()
|
||||||
|
broker.LogConnect(ctx, cfg.BrokerURL)
|
||||||
t := time.NewTicker(60 * time.Second)
|
t := time.NewTicker(60 * time.Second)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
log.Printf("evobgp-render: started (stub; render jobs produce revisions and artifacts)")
|
log.Printf("evobgp-render: started (stub; render jobs produce revisions and artifacts)")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,560 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"evobgp/internal/store"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Postgres) ListCDNSources(tenantID, moduleID string) ([]*store.CDNSource, error) {
|
||||||
|
mod, err := p.GetModule(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "CDN_CIDRS" {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
rows, err := p.pool.Query(ctx, `
|
||||||
|
SELECT id::text, source_kind, url, COALESCE(etag,''), refresh_interval_sec, community_id::text
|
||||||
|
FROM module_cdn_source WHERE module_id=$1 ORDER BY url`, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []*store.CDNSource
|
||||||
|
for rows.Next() {
|
||||||
|
var s store.CDNSource
|
||||||
|
s.ModuleID = moduleID
|
||||||
|
var ri *int32
|
||||||
|
var comm *string
|
||||||
|
if err := rows.Scan(&s.ID, &s.SourceKind, &s.URL, &s.Etag, &ri, &comm); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ri != nil {
|
||||||
|
v := int(*ri)
|
||||||
|
s.RefreshIntervalSec = &v
|
||||||
|
}
|
||||||
|
s.CommunityID = strOrNil(comm)
|
||||||
|
out = append(out, &s)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) CreateCDNSource(tenantID, moduleID string, in *store.CDNSource) (*store.CDNSource, error) {
|
||||||
|
mod, err := p.GetModule(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "CDN_CIDRS" {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
if in == nil || strings.TrimSpace(in.URL) == "" {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
id := uuid.NewString()
|
||||||
|
_, err = p.pool.Exec(ctx, `
|
||||||
|
INSERT INTO module_cdn_source (id, module_id, source_kind, url, etag, refresh_interval_sec, community_id)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6, NULLIF($7::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
|
||||||
|
id, moduleID, in.SourceKind, strings.TrimSpace(in.URL), in.Etag, nullInt32Ptr(in.RefreshIntervalSec), uuidOrNilPtr(in.CommunityID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.getCDNSource(ctx, moduleID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) getCDNSource(ctx context.Context, moduleID, id string) (*store.CDNSource, error) {
|
||||||
|
var s store.CDNSource
|
||||||
|
s.ModuleID = moduleID
|
||||||
|
var ri *int32
|
||||||
|
var comm *string
|
||||||
|
err := p.pool.QueryRow(ctx, `
|
||||||
|
SELECT id::text, source_kind, url, COALESCE(etag,''), refresh_interval_sec, community_id::text
|
||||||
|
FROM module_cdn_source WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&s.ID, &s.SourceKind, &s.URL, &s.Etag, &ri, &comm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ri != nil {
|
||||||
|
v := int(*ri)
|
||||||
|
s.RefreshIntervalSec = &v
|
||||||
|
}
|
||||||
|
s.CommunityID = strOrNil(comm)
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uuidOrNilPtr(s *string) any {
|
||||||
|
if s == nil || strings.TrimSpace(*s) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(*s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *store.CDNSourcePatch) (*store.CDNSource, error) {
|
||||||
|
if _, err := p.GetModule(tenantID, moduleID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cur, err := p.getCDNSource(context.Background(), moduleID, sourceID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if patch.SourceKind != nil {
|
||||||
|
cur.SourceKind = *patch.SourceKind
|
||||||
|
}
|
||||||
|
if patch.URL != nil {
|
||||||
|
cur.URL = strings.TrimSpace(*patch.URL)
|
||||||
|
}
|
||||||
|
if patch.Etag != nil {
|
||||||
|
cur.Etag = *patch.Etag
|
||||||
|
}
|
||||||
|
if patch.RefreshIntervalSec != nil {
|
||||||
|
cur.RefreshIntervalSec = patch.RefreshIntervalSec
|
||||||
|
}
|
||||||
|
if patch.CommunityID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.CommunityID)
|
||||||
|
if v == "" {
|
||||||
|
cur.CommunityID = nil
|
||||||
|
} else {
|
||||||
|
cur.CommunityID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err = p.pool.Exec(ctx, `
|
||||||
|
UPDATE module_cdn_source SET source_kind=$3, url=$4, etag=$5, refresh_interval_sec=$6,
|
||||||
|
community_id=NULLIF($7::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
|
||||||
|
WHERE id=$1 AND module_id=$2`,
|
||||||
|
sourceID, moduleID, cur.SourceKind, cur.URL, cur.Etag, nullInt32Ptr(cur.RefreshIntervalSec), uuidOrNilPtr(cur.CommunityID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.getCDNSource(ctx, moduleID, sourceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) DeleteCDNSource(tenantID, moduleID, sourceID string) error {
|
||||||
|
if _, err := p.GetModule(tenantID, moduleID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
tag, err := p.pool.Exec(ctx, `DELETE FROM module_cdn_source WHERE id=$1 AND module_id=$2`, sourceID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) ListASEntries(tenantID, moduleID string) ([]*store.ASEntry, error) {
|
||||||
|
mod, err := p.GetModule(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "AS_PREFIXES" {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
rows, err := p.pool.Query(ctx, `
|
||||||
|
SELECT id::text, asn, prefix::text, community_id::text FROM module_as_entry WHERE module_id=$1`, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []*store.ASEntry
|
||||||
|
for rows.Next() {
|
||||||
|
var e store.ASEntry
|
||||||
|
e.ModuleID = moduleID
|
||||||
|
var asn *int64
|
||||||
|
var pref, comm *string
|
||||||
|
if err := rows.Scan(&e.ID, &asn, &pref, &comm); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.ASN = asn
|
||||||
|
e.Prefix = pref
|
||||||
|
e.CommunityID = strOrNil(comm)
|
||||||
|
out = append(out, &e)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) CreateASEntry(tenantID, moduleID string, in *store.ASEntry) (*store.ASEntry, error) {
|
||||||
|
mod, err := p.GetModule(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "AS_PREFIXES" {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
if in == nil || (in.ASN == nil && (in.Prefix == nil || strings.TrimSpace(*in.Prefix) == "")) {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
id := uuid.NewString()
|
||||||
|
var pref any
|
||||||
|
if in.Prefix != nil && strings.TrimSpace(*in.Prefix) != "" {
|
||||||
|
pref = strings.TrimSpace(*in.Prefix)
|
||||||
|
}
|
||||||
|
_, err = p.pool.Exec(ctx, `
|
||||||
|
INSERT INTO module_as_entry (id, module_id, asn, prefix, community_id)
|
||||||
|
VALUES ($1,$2,$3,$4::cidr, NULLIF($5::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
|
||||||
|
id, moduleID, in.ASN, pref, uuidOrNilPtr(in.CommunityID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.getASEntry(ctx, moduleID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) getASEntry(ctx context.Context, moduleID, id string) (*store.ASEntry, error) {
|
||||||
|
var e store.ASEntry
|
||||||
|
e.ModuleID = moduleID
|
||||||
|
var asn *int64
|
||||||
|
var pref, comm *string
|
||||||
|
err := p.pool.QueryRow(ctx, `
|
||||||
|
SELECT id::text, asn, prefix::text, community_id::text FROM module_as_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(
|
||||||
|
&e.ID, &asn, &pref, &comm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.ASN = asn
|
||||||
|
e.Prefix = pref
|
||||||
|
e.CommunityID = strOrNil(comm)
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) UpdateASEntry(tenantID, moduleID, entryID string, patch *store.ASEntryPatch) (*store.ASEntry, error) {
|
||||||
|
if _, err := p.GetModule(tenantID, moduleID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cur, err := p.getASEntry(context.Background(), moduleID, entryID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if patch.ASN != nil {
|
||||||
|
cur.ASN = patch.ASN
|
||||||
|
}
|
||||||
|
if patch.Prefix != nil {
|
||||||
|
p := strings.TrimSpace(*patch.Prefix)
|
||||||
|
if p == "" {
|
||||||
|
cur.Prefix = nil
|
||||||
|
} else {
|
||||||
|
cur.Prefix = &p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if patch.CommunityID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.CommunityID)
|
||||||
|
if v == "" {
|
||||||
|
cur.CommunityID = nil
|
||||||
|
} else {
|
||||||
|
cur.CommunityID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
var pref any
|
||||||
|
if cur.Prefix != nil {
|
||||||
|
pref = *cur.Prefix
|
||||||
|
}
|
||||||
|
_, err = p.pool.Exec(ctx, `
|
||||||
|
UPDATE module_as_entry SET asn=$3, prefix=$4::cidr, community_id=NULLIF($5::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
|
||||||
|
WHERE id=$1 AND module_id=$2`,
|
||||||
|
entryID, moduleID, cur.ASN, pref, uuidOrNilPtr(cur.CommunityID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.getASEntry(ctx, moduleID, entryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) DeleteASEntry(tenantID, moduleID, entryID string) error {
|
||||||
|
if _, err := p.GetModule(tenantID, moduleID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
tag, err := p.pool.Exec(ctx, `DELETE FROM module_as_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) ListDomainEntries(tenantID, moduleID string) ([]*store.DomainEntry, error) {
|
||||||
|
mod, err := p.GetModule(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "DOMAINS" {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
rows, err := p.pool.Query(ctx, `SELECT id::text, fqdn, community_id::text FROM module_domain_entry WHERE module_id=$1`, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []*store.DomainEntry
|
||||||
|
for rows.Next() {
|
||||||
|
var e store.DomainEntry
|
||||||
|
e.ModuleID = moduleID
|
||||||
|
var comm *string
|
||||||
|
if err := rows.Scan(&e.ID, &e.FQDN, &comm); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.CommunityID = strOrNil(comm)
|
||||||
|
out = append(out, &e)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) CreateDomainEntry(tenantID, moduleID string, in *store.DomainEntry) (*store.DomainEntry, error) {
|
||||||
|
mod, err := p.GetModule(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "DOMAINS" || in == nil || strings.TrimSpace(in.FQDN) == "" {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
id := uuid.NewString()
|
||||||
|
_, err = p.pool.Exec(ctx, `
|
||||||
|
INSERT INTO module_domain_entry (id, module_id, fqdn, community_id)
|
||||||
|
VALUES ($1,$2,$3, NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
|
||||||
|
id, moduleID, strings.TrimSpace(in.FQDN), uuidOrNilPtr(in.CommunityID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.getDomainEntry(ctx, moduleID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) getDomainEntry(ctx context.Context, moduleID, id string) (*store.DomainEntry, error) {
|
||||||
|
var e store.DomainEntry
|
||||||
|
e.ModuleID = moduleID
|
||||||
|
var comm *string
|
||||||
|
err := p.pool.QueryRow(ctx, `SELECT id::text, fqdn, community_id::text FROM module_domain_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&e.ID, &e.FQDN, &comm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.CommunityID = strOrNil(comm)
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *store.DomainEntryPatch) (*store.DomainEntry, error) {
|
||||||
|
if _, err := p.GetModule(tenantID, moduleID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cur, err := p.getDomainEntry(context.Background(), moduleID, entryID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if patch.FQDN != nil {
|
||||||
|
cur.FQDN = strings.TrimSpace(*patch.FQDN)
|
||||||
|
}
|
||||||
|
if patch.CommunityID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.CommunityID)
|
||||||
|
if v == "" {
|
||||||
|
cur.CommunityID = nil
|
||||||
|
} else {
|
||||||
|
cur.CommunityID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err = p.pool.Exec(ctx, `
|
||||||
|
UPDATE module_domain_entry SET fqdn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
|
||||||
|
WHERE id=$1 AND module_id=$2`,
|
||||||
|
entryID, moduleID, cur.FQDN, uuidOrNilPtr(cur.CommunityID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.getDomainEntry(ctx, moduleID, entryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) DeleteDomainEntry(tenantID, moduleID, entryID string) error {
|
||||||
|
if _, err := p.GetModule(tenantID, moduleID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
tag, err := p.pool.Exec(ctx, `DELETE FROM module_domain_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) ListIPRangeEntries(tenantID, moduleID string) ([]*store.IPRangeEntry, error) {
|
||||||
|
mod, err := p.GetModule(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "IP_RANGES" {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
rows, err := p.pool.Query(ctx, `SELECT id::text, prefix::text, community_id::text FROM module_ip_range_entry WHERE module_id=$1`, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []*store.IPRangeEntry
|
||||||
|
for rows.Next() {
|
||||||
|
var e store.IPRangeEntry
|
||||||
|
e.ModuleID = moduleID
|
||||||
|
var comm *string
|
||||||
|
if err := rows.Scan(&e.ID, &e.Prefix, &comm); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.CommunityID = strOrNil(comm)
|
||||||
|
out = append(out, &e)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) CreateIPRangeEntry(tenantID, moduleID string, in *store.IPRangeEntry) (*store.IPRangeEntry, error) {
|
||||||
|
mod, err := p.GetModule(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "IP_RANGES" || in == nil || strings.TrimSpace(in.Prefix) == "" {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
id := uuid.NewString()
|
||||||
|
_, err = p.pool.Exec(ctx, `
|
||||||
|
INSERT INTO module_ip_range_entry (id, module_id, prefix, community_id)
|
||||||
|
VALUES ($1,$2,$3::cidr, NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
|
||||||
|
id, moduleID, strings.TrimSpace(in.Prefix), uuidOrNilPtr(in.CommunityID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.getIPRangeEntry(ctx, moduleID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) getIPRangeEntry(ctx context.Context, moduleID, id string) (*store.IPRangeEntry, error) {
|
||||||
|
var e store.IPRangeEntry
|
||||||
|
e.ModuleID = moduleID
|
||||||
|
var comm *string
|
||||||
|
err := p.pool.QueryRow(ctx, `SELECT id::text, prefix::text, community_id::text FROM module_ip_range_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&e.ID, &e.Prefix, &comm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.CommunityID = strOrNil(comm)
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *store.IPRangePatch) (*store.IPRangeEntry, error) {
|
||||||
|
if _, err := p.GetModule(tenantID, moduleID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cur, err := p.getIPRangeEntry(context.Background(), moduleID, entryID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if patch.Prefix != nil {
|
||||||
|
cur.Prefix = strings.TrimSpace(*patch.Prefix)
|
||||||
|
}
|
||||||
|
if patch.CommunityID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.CommunityID)
|
||||||
|
if v == "" {
|
||||||
|
cur.CommunityID = nil
|
||||||
|
} else {
|
||||||
|
cur.CommunityID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err = p.pool.Exec(ctx, `
|
||||||
|
UPDATE module_ip_range_entry SET prefix=$3::cidr, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
|
||||||
|
WHERE id=$1 AND module_id=$2`,
|
||||||
|
entryID, moduleID, cur.Prefix, uuidOrNilPtr(cur.CommunityID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.getIPRangeEntry(ctx, moduleID, entryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error {
|
||||||
|
if _, err := p.GetModule(tenantID, moduleID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
tag, err := p.pool.Exec(ctx, `DELETE FROM module_ip_range_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) ListGlobalSettings(tenantID string) (map[string]any, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
rows, err := p.pool.Query(ctx, `SELECT key, value_json FROM global_settings WHERE tenant_id=$1`, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
out := make(map[string]any)
|
||||||
|
for rows.Next() {
|
||||||
|
var k string
|
||||||
|
var vj []byte
|
||||||
|
if err := rows.Scan(&k, &vj); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
_ = json.Unmarshal(vj, &v)
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) PatchGlobalSettings(tenantID string, patch map[string]any) error {
|
||||||
|
if patch == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
for k, v := range patch {
|
||||||
|
if strings.TrimSpace(k) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v == nil {
|
||||||
|
_, _ = p.pool.Exec(ctx, `DELETE FROM global_settings WHERE tenant_id=$1 AND key=$2`, tenantID, k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = p.pool.Exec(ctx, `
|
||||||
|
INSERT INTO global_settings (tenant_id, key, value_json) VALUES ($1,$2,$3::jsonb)
|
||||||
|
ON CONFLICT (tenant_id, key) DO UPDATE SET value_json = EXCLUDED.value_json, updated_at = now()`,
|
||||||
|
tenantID, k, string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ store.Backend = (*Postgres)(nil)
|
||||||
|
|
||||||
|
var _ store.Backend = (*Postgres)(nil)
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Postgres) seedDemo(ctx context.Context) error {
|
||||||
|
var n int
|
||||||
|
if err := p.pool.QueryRow(ctx, `SELECT COUNT(*)::int FROM tenant`).Scan(&n); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
return p.attachDemoIDs(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
tid := uuid.NewString()
|
||||||
|
mCDN := uuid.NewString()
|
||||||
|
mIP := uuid.NewString()
|
||||||
|
parent := uuid.NewString()
|
||||||
|
rid := uuid.NewString()
|
||||||
|
sid := uuid.NewString()
|
||||||
|
cid := uuid.NewString()
|
||||||
|
p1 := uuid.NewString()
|
||||||
|
p2 := uuid.NewString()
|
||||||
|
|
||||||
|
preview := map[string]any{
|
||||||
|
"preview_fragments": map[string]string{
|
||||||
|
"bird.conf": `# EvoBGP demo bundle
|
||||||
|
router id 192.0.2.1;
|
||||||
|
|
||||||
|
protocol device {
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol direct {
|
||||||
|
ipv4;
|
||||||
|
ipv6;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"bird.d/evobgp_demo.conf": "# static demo fragment\n",
|
||||||
|
},
|
||||||
|
"materialized_prefix_count": 128,
|
||||||
|
}
|
||||||
|
previewB, _ := json.Marshal(preview)
|
||||||
|
parentMeta, _ := json.Marshal(map[string]any{
|
||||||
|
"preview_fragments": map[string]string{"bird.conf": "# parent revision\n"},
|
||||||
|
"materialized_prefix_count": 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
tx, err := p.pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback(ctx) }()
|
||||||
|
|
||||||
|
if _, err := tx.Exec(ctx, `INSERT INTO tenant (id, name, slug) VALUES ($1,'Demo','demo')`, tid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `INSERT INTO bgp_community (id, tenant_id, name, kind, value_json) VALUES ($1,$2,'demo-comm','large','{}')`, cid, tid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
INSERT INTO module (id, tenant_id, type, name, enabled, priority, refresh_interval_sec)
|
||||||
|
VALUES ($1,$2,'CDN_CIDRS','demo-cdn',true,10,3600)`, mCDN, tid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
INSERT INTO module (id, tenant_id, type, name, enabled, priority)
|
||||||
|
VALUES ($1,$2,'IP_RANGES','demo-static',true,20)`, mIP, tid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
INSERT INTO config_revision (id, tenant_id, content_hash, parent_revision_id, meta_json)
|
||||||
|
VALUES ($1,$2,'sha256:parent',NULL,$3::jsonb)`, parent, tid, string(parentMeta)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
INSERT INTO config_revision (id, tenant_id, content_hash, parent_revision_id, meta_json)
|
||||||
|
VALUES ($1,$2,'sha256:demo-rev-1',$3::uuid,$4::jsonb)`, rid, tid, parent, string(previewB)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
INSERT INTO revision_materialized_prefix (revision_id, prefix, community_id, source)
|
||||||
|
VALUES ($1::uuid,'203.0.113.0/24',$2::uuid,'demo'), ($1::uuid,'2001:db8::/32',$2::uuid,'demo')`, rid, cid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
INSERT INTO bgp_speaker (id, tenant_id, role, endpoint, published_revision_id, published_at)
|
||||||
|
VALUES ($1,$2,'replica','10.0.0.2:179',$3::uuid, now())`, sid, tid, rid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
meta4 := `{"name":"demo-upstream-4","session_state":"Established"}`
|
||||||
|
meta6 := `{"name":"demo-upstream-6","session_state":"Idle"}`
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
INSERT INTO bgp_peer (id, tenant_id, bgp_speaker_id, neighbor, remote_asn, enabled, policies_json, meta_json)
|
||||||
|
VALUES ($1,$2,$3::uuid,'198.51.100.2'::inet,65001,true,'{}',$4::jsonb)`, p1, tid, sid, meta4); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
INSERT INTO bgp_peer (id, tenant_id, bgp_speaker_id, neighbor, remote_asn, enabled, policies_json, meta_json)
|
||||||
|
VALUES ($1,$2,$3::uuid,'2001:db8::2'::inet,65002,true,'{}',$4::jsonb)`, p2, tid, sid, meta6); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.demoTenant, p.demoCDN, p.demoIP, p.demoRev, p.demoSpk = tid, mCDN, mIP, rid, sid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// attachDemoIDs fills demo pointer fields when DB already has data (e.g. compose restart).
|
||||||
|
func (p *Postgres) attachDemoIDs(ctx context.Context) error {
|
||||||
|
if err := p.pool.QueryRow(ctx, `SELECT id::text FROM tenant WHERE slug = 'demo' ORDER BY created_at LIMIT 1`).Scan(&p.demoTenant); err != nil {
|
||||||
|
return nil // non-fatal: no demo tenant
|
||||||
|
}
|
||||||
|
_ = p.pool.QueryRow(ctx, `SELECT id::text FROM module WHERE tenant_id = $1::uuid AND name = 'demo-cdn' LIMIT 1`, p.demoTenant).Scan(&p.demoCDN)
|
||||||
|
_ = p.pool.QueryRow(ctx, `SELECT id::text FROM module WHERE tenant_id = $1::uuid AND name = 'demo-static' LIMIT 1`, p.demoTenant).Scan(&p.demoIP)
|
||||||
|
_ = p.pool.QueryRow(ctx, `
|
||||||
|
SELECT id::text FROM config_revision WHERE tenant_id = $1::uuid AND content_hash LIKE 'sha256:demo%' ORDER BY created_at DESC LIMIT 1`, p.demoTenant).Scan(&p.demoRev)
|
||||||
|
_ = p.pool.QueryRow(ctx, `SELECT id::text FROM bgp_speaker WHERE tenant_id = $1::uuid LIMIT 1`, p.demoTenant).Scan(&p.demoSpk)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,10 +6,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"evobgp/internal/broker"
|
||||||
|
"evobgp/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run blocks until ctx is cancelled. In production it connects to DB + broker and schedules refresh events.
|
// Run blocks until ctx is cancelled. In production it connects to DB + broker and schedules refresh events.
|
||||||
func Run(ctx context.Context) {
|
func Run(ctx context.Context) {
|
||||||
|
cfg := config.Load()
|
||||||
|
broker.LogConnect(ctx, cfg.BrokerURL)
|
||||||
t := time.NewTicker(60 * time.Second)
|
t := time.NewTicker(60 * time.Second)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
log.Printf("evobgp-scheduler: started (stub; connect EVOBGP_DATABASE_URL / EVOBGP_BROKER_URL for full stack)")
|
log.Printf("evobgp-scheduler: started (stub; connect EVOBGP_DATABASE_URL / EVOBGP_BROKER_URL for full stack)")
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Backend is the persistence abstraction for the control plane (memory, PostgreSQL, SQLite).
|
||||||
|
type Backend interface {
|
||||||
|
MaterializedPrefixStats() (max int, sum int)
|
||||||
|
PeerCount() int
|
||||||
|
PeerSessionCountsByState() map[string]int
|
||||||
|
// DemoIDs is non-empty only after SeedDemo (memory or seeded SQL).
|
||||||
|
DemoIDs() (tenant, moduleCDN, moduleIP, revision, speaker string)
|
||||||
|
|
||||||
|
ListModules(tenantID string) []*Module
|
||||||
|
GetModule(tenantID, moduleID string) (*Module, error)
|
||||||
|
CreateModule(tenantID string, in *Module) (*Module, error)
|
||||||
|
UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*Module, error)
|
||||||
|
SoftDeleteModule(tenantID, moduleID string) error
|
||||||
|
|
||||||
|
ListCDNSources(tenantID, moduleID string) ([]*CDNSource, error)
|
||||||
|
CreateCDNSource(tenantID, moduleID string, in *CDNSource) (*CDNSource, error)
|
||||||
|
UpdateCDNSource(tenantID, moduleID, sourceID string, patch *CDNSourcePatch) (*CDNSource, error)
|
||||||
|
DeleteCDNSource(tenantID, moduleID, sourceID string) error
|
||||||
|
|
||||||
|
ListASEntries(tenantID, moduleID string) ([]*ASEntry, error)
|
||||||
|
CreateASEntry(tenantID, moduleID string, in *ASEntry) (*ASEntry, error)
|
||||||
|
UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntryPatch) (*ASEntry, error)
|
||||||
|
DeleteASEntry(tenantID, moduleID, entryID string) error
|
||||||
|
|
||||||
|
ListDomainEntries(tenantID, moduleID string) ([]*DomainEntry, error)
|
||||||
|
CreateDomainEntry(tenantID, moduleID string, in *DomainEntry) (*DomainEntry, error)
|
||||||
|
UpdateDomainEntry(tenantID, moduleID, entryID string, patch *DomainEntryPatch) (*DomainEntry, error)
|
||||||
|
DeleteDomainEntry(tenantID, moduleID, entryID string) error
|
||||||
|
|
||||||
|
ListIPRangeEntries(tenantID, moduleID string) ([]*IPRangeEntry, error)
|
||||||
|
CreateIPRangeEntry(tenantID, moduleID string, in *IPRangeEntry) (*IPRangeEntry, error)
|
||||||
|
UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *IPRangePatch) (*IPRangeEntry, error)
|
||||||
|
DeleteIPRangeEntry(tenantID, moduleID, entryID string) error
|
||||||
|
|
||||||
|
ListDohProfiles(tenantID string) ([]*DohProfile, error)
|
||||||
|
GetDohProfile(tenantID, id string) (*DohProfile, error)
|
||||||
|
CreateDohProfile(tenantID string, in *DohProfile) (*DohProfile, error)
|
||||||
|
UpdateDohProfile(tenantID, id string, patch *DohProfilePatch) (*DohProfile, error)
|
||||||
|
DeleteDohProfile(tenantID, id string) error
|
||||||
|
|
||||||
|
ListCommunities(tenantID string) ([]*Community, error)
|
||||||
|
GetCommunity(tenantID, id string) (*Community, error)
|
||||||
|
CreateCommunity(tenantID string, in *Community) (*Community, error)
|
||||||
|
UpdateCommunity(tenantID, id string, patch *CommunityPatch) (*Community, error)
|
||||||
|
DeleteCommunity(tenantID, id string) error
|
||||||
|
|
||||||
|
ListPeers(tenantID string) []*BGPPeer
|
||||||
|
GetPeer(tenantID, id string) (*BGPPeer, error)
|
||||||
|
CreatePeer(tenantID string, in *BGPPeer) (*BGPPeer, error)
|
||||||
|
UpdatePeer(tenantID, id string, patch *PeerPatch) (*BGPPeer, error)
|
||||||
|
DeletePeer(tenantID, id string) error
|
||||||
|
|
||||||
|
ListSpeakersForTenant(tenantID string) []*Speaker
|
||||||
|
GetSpeaker(tenantID, speakerID string) (*Speaker, error)
|
||||||
|
GetSpeakerAnyTenant(speakerID string) (*Speaker, error)
|
||||||
|
CreateSpeaker(tenantID string, in *Speaker) (*Speaker, error)
|
||||||
|
UpdateSpeaker(tenantID, id string, patch *SpeakerPatch) (*Speaker, error)
|
||||||
|
|
||||||
|
GetRevision(tenantID, revisionID string) (*Revision, error)
|
||||||
|
ListRevisions(tenantID, moduleID string, cursor string, limit int) (items []*Revision, nextCursor string, hasMore bool)
|
||||||
|
ListRevisionPrefixes(tenantID, revisionID string, cursor string, limit int) (prefixes []PrefixRow, next string, more bool)
|
||||||
|
CreateRollbackRevision(tenantID, sourceRevisionID string) (newID string, err error)
|
||||||
|
RevisionDiff(tenantID, aID, bID string) (map[string]any, error)
|
||||||
|
SetLastAppliedRevision(tenantID, speakerID, revisionID string) error
|
||||||
|
PublishRevisionForSpeaker(speakerID, revisionID string) error
|
||||||
|
LatestPublishedRevision(speakerID string) (revisionID string, publishedAt time.Time, err error)
|
||||||
|
|
||||||
|
ListGlobalSettings(tenantID string) (map[string]any, error)
|
||||||
|
PatchGlobalSettings(tenantID string, patch map[string]any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModulePatch is a partial update for module.
|
||||||
|
type ModulePatch struct {
|
||||||
|
Name *string
|
||||||
|
Enabled *bool
|
||||||
|
Priority *int
|
||||||
|
RefreshIntervalSec *int
|
||||||
|
CronExpr *string
|
||||||
|
DefaultCommunityID *string
|
||||||
|
DohProfileID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CDNSource is a row under a CDN module.
|
||||||
|
type CDNSource struct {
|
||||||
|
ID string
|
||||||
|
ModuleID string
|
||||||
|
SourceKind string
|
||||||
|
URL string
|
||||||
|
Etag string
|
||||||
|
RefreshIntervalSec *int
|
||||||
|
CommunityID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CDNSourcePatch struct {
|
||||||
|
SourceKind *string
|
||||||
|
URL *string
|
||||||
|
Etag *string
|
||||||
|
RefreshIntervalSec *int
|
||||||
|
CommunityID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ASEntry struct {
|
||||||
|
ID string
|
||||||
|
ModuleID string
|
||||||
|
ASN *int64
|
||||||
|
Prefix *string
|
||||||
|
CommunityID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ASEntryPatch struct {
|
||||||
|
ASN *int64
|
||||||
|
Prefix *string
|
||||||
|
CommunityID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainEntry struct {
|
||||||
|
ID string
|
||||||
|
ModuleID string
|
||||||
|
FQDN string
|
||||||
|
CommunityID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainEntryPatch struct {
|
||||||
|
FQDN *string
|
||||||
|
CommunityID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPRangeEntry struct {
|
||||||
|
ID string
|
||||||
|
ModuleID string
|
||||||
|
Prefix string
|
||||||
|
CommunityID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPRangePatch struct {
|
||||||
|
Prefix *string
|
||||||
|
CommunityID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DohProfile struct {
|
||||||
|
ID string
|
||||||
|
TenantID string
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
TimeoutMs *int
|
||||||
|
SecretRef *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DohProfilePatch struct {
|
||||||
|
Name *string
|
||||||
|
URL *string
|
||||||
|
TimeoutMs *int
|
||||||
|
SecretRef *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Community struct {
|
||||||
|
ID string
|
||||||
|
TenantID string
|
||||||
|
Name string
|
||||||
|
Kind string
|
||||||
|
ValueJSON string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommunityPatch struct {
|
||||||
|
Name *string
|
||||||
|
Kind *string
|
||||||
|
ValueJSON *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerPatch struct {
|
||||||
|
Neighbor *string
|
||||||
|
RemoteASN *int64
|
||||||
|
SpeakerID *string
|
||||||
|
Enabled *bool
|
||||||
|
Name *string
|
||||||
|
SessionState *string
|
||||||
|
PoliciesJSON *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpeakerPatch struct {
|
||||||
|
Role *string
|
||||||
|
Endpoint *string
|
||||||
|
MetaJSON *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrefixRow is one materialized prefix for GET /revisions/.../prefixes.
|
||||||
|
type PrefixRow struct {
|
||||||
|
Prefix string
|
||||||
|
CommunityID *string
|
||||||
|
Source string
|
||||||
|
}
|
||||||
+81
-16
@@ -32,6 +32,15 @@ type Memory struct {
|
|||||||
|
|
||||||
peers map[string]*BGPPeer
|
peers map[string]*BGPPeer
|
||||||
|
|
||||||
|
dohProfiles map[string]*DohProfile
|
||||||
|
communities map[string]*Community
|
||||||
|
cdnSources map[string]*CDNSource
|
||||||
|
asEntries map[string]*ASEntry
|
||||||
|
domainEnt map[string]*DomainEntry
|
||||||
|
ipRanges map[string]*IPRangeEntry
|
||||||
|
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
||||||
|
revPrefixes map[string][]PrefixRow
|
||||||
|
|
||||||
// DemoIDs valid after SeedDemo()
|
// DemoIDs valid after SeedDemo()
|
||||||
demoTenantID string
|
demoTenantID string
|
||||||
demoModuleCDN string
|
demoModuleCDN string
|
||||||
@@ -59,9 +68,10 @@ type Module struct {
|
|||||||
Enabled bool
|
Enabled bool
|
||||||
RefreshIntervalSec int // 0 = unset
|
RefreshIntervalSec int // 0 = unset
|
||||||
CronExpr string // optional cron for scheduler (display / future use)
|
CronExpr string // optional cron for scheduler (display / future use)
|
||||||
Priority int
|
Priority int
|
||||||
DefaultCommunityID *string
|
DefaultCommunityID *string
|
||||||
DohProfileID *string
|
DohProfileID *string
|
||||||
|
DeletedAt *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type Revision struct {
|
type Revision struct {
|
||||||
@@ -77,14 +87,17 @@ type Revision struct {
|
|||||||
PreviewFragments map[string]string
|
PreviewFragments map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// BGPPeer is a minimal stand-in until SQL-backed bgp_peer is wired into the API.
|
// BGPPeer maps to bgp_peer (+ display fields in meta).
|
||||||
type BGPPeer struct {
|
type BGPPeer struct {
|
||||||
ID string
|
ID string
|
||||||
TenantID string
|
TenantID string
|
||||||
SpeakerID *string
|
SpeakerID *string
|
||||||
Name string
|
Name string
|
||||||
Neighbor string
|
Neighbor string
|
||||||
SessionState string // e.g. Established, Idle, Connect (intent or cached ops view)
|
RemoteASN int64
|
||||||
|
Enabled bool
|
||||||
|
SessionState string
|
||||||
|
PoliciesJSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Speaker struct {
|
type Speaker struct {
|
||||||
@@ -93,6 +106,7 @@ type Speaker struct {
|
|||||||
Role string
|
Role string
|
||||||
Endpoint string
|
Endpoint string
|
||||||
LastAppliedRevisionID *string
|
LastAppliedRevisionID *string
|
||||||
|
MetaJSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMemory() *Memory {
|
func NewMemory() *Memory {
|
||||||
@@ -103,6 +117,14 @@ func NewMemory() *Memory {
|
|||||||
speakers: make(map[string]*Speaker),
|
speakers: make(map[string]*Speaker),
|
||||||
publishedRevision: make(map[string]publishedInfo),
|
publishedRevision: make(map[string]publishedInfo),
|
||||||
peers: make(map[string]*BGPPeer),
|
peers: make(map[string]*BGPPeer),
|
||||||
|
dohProfiles: make(map[string]*DohProfile),
|
||||||
|
communities: make(map[string]*Community),
|
||||||
|
cdnSources: make(map[string]*CDNSource),
|
||||||
|
asEntries: make(map[string]*ASEntry),
|
||||||
|
domainEnt: make(map[string]*DomainEntry),
|
||||||
|
ipRanges: make(map[string]*IPRangeEntry),
|
||||||
|
settings: make(map[string]map[string]any),
|
||||||
|
revPrefixes: make(map[string][]PrefixRow),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +215,8 @@ protocol direct {
|
|||||||
SpeakerID: &sid,
|
SpeakerID: &sid,
|
||||||
Name: "demo-upstream-4",
|
Name: "demo-upstream-4",
|
||||||
Neighbor: "198.51.100.2",
|
Neighbor: "198.51.100.2",
|
||||||
|
RemoteASN: 65001,
|
||||||
|
Enabled: true,
|
||||||
SessionState: "Established",
|
SessionState: "Established",
|
||||||
}
|
}
|
||||||
p2 := uuid.NewString()
|
p2 := uuid.NewString()
|
||||||
@@ -202,10 +226,20 @@ protocol direct {
|
|||||||
SpeakerID: &sid,
|
SpeakerID: &sid,
|
||||||
Name: "demo-upstream-6",
|
Name: "demo-upstream-6",
|
||||||
Neighbor: "2001:db8::2",
|
Neighbor: "2001:db8::2",
|
||||||
|
RemoteASN: 65002,
|
||||||
|
Enabled: true,
|
||||||
SessionState: "Idle",
|
SessionState: "Idle",
|
||||||
}
|
}
|
||||||
|
|
||||||
m.demoTenantID, m.demoModuleCDN, m.demoModuleIP, m.demoRevisionID, m.demoSpeakerID = tid, mCDN, mIP, rid, sid
|
m.demoTenantID, m.demoModuleCDN, m.demoModuleIP, m.demoRevisionID, m.demoSpeakerID = tid, mCDN, mIP, rid, sid
|
||||||
|
|
||||||
|
// Demo materialized prefixes for /revisions/{id}/prefixes
|
||||||
|
cid := uuid.NewString()
|
||||||
|
m.communities[cid] = &Community{ID: cid, TenantID: tid, Name: "demo-comm", Kind: "large", ValueJSON: "{}"}
|
||||||
|
m.revPrefixes[rid] = []PrefixRow{
|
||||||
|
{Prefix: "203.0.113.0/24", CommunityID: &cid, Source: "demo"},
|
||||||
|
{Prefix: "2001:db8::/32", CommunityID: &cid, Source: "demo"},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaterializedPrefixStats returns max and sum of MaterializedPrefixCount across revisions.
|
// MaterializedPrefixStats returns max and sum of MaterializedPrefixCount across revisions.
|
||||||
@@ -256,7 +290,7 @@ func (m *Memory) ListModules(tenantID string) []*Module {
|
|||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
var out []*Module
|
var out []*Module
|
||||||
for _, mod := range m.modules {
|
for _, mod := range m.modules {
|
||||||
if mod.TenantID == tenantID {
|
if mod.TenantID == tenantID && mod.DeletedAt == nil {
|
||||||
out = append(out, mod)
|
out = append(out, mod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,7 +321,7 @@ func (m *Memory) GetModule(tenantID, moduleID string) (*Module, error) {
|
|||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
mod, ok := m.modules[moduleID]
|
mod, ok := m.modules[moduleID]
|
||||||
if !ok {
|
if !ok || mod.DeletedAt != nil {
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
if mod.TenantID != tenantID {
|
if mod.TenantID != tenantID {
|
||||||
@@ -388,6 +422,11 @@ func (m *Memory) CreateRollbackRevision(tenantID, sourceRevisionID string) (newI
|
|||||||
MaterializedPrefixCount: src.MaterializedPrefixCount,
|
MaterializedPrefixCount: src.MaterializedPrefixCount,
|
||||||
PreviewFragments: frag,
|
PreviewFragments: frag,
|
||||||
}
|
}
|
||||||
|
if px, ok := m.revPrefixes[sourceRevisionID]; ok {
|
||||||
|
cp := make([]PrefixRow, len(px))
|
||||||
|
copy(cp, px)
|
||||||
|
m.revPrefixes[newID] = cp
|
||||||
|
}
|
||||||
return newID, nil
|
return newID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,8 +462,10 @@ func (m *Memory) PublishRevisionForSpeaker(speakerID, revisionID string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RevisionDiff returns a trivial JSON-friendly diff between prefix sets (empty for memory demo).
|
// RevisionDiff returns prefix set diff from materialized snapshots when present.
|
||||||
func (m *Memory) RevisionDiff(tenantID, aID, bID string) (map[string]any, error) {
|
func (m *Memory) RevisionDiff(tenantID, aID, bID string) (map[string]any, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
_, err := m.getRevisionLocked(tenantID, aID)
|
_, err := m.getRevisionLocked(tenantID, aID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -433,13 +474,37 @@ func (m *Memory) RevisionDiff(tenantID, aID, bID string) (map[string]any, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
setA := make(map[string]struct{})
|
||||||
|
setB := make(map[string]struct{})
|
||||||
|
for _, p := range m.revPrefixes[aID] {
|
||||||
|
setA[p.Prefix] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, p := range m.revPrefixes[bID] {
|
||||||
|
setB[p.Prefix] = struct{}{}
|
||||||
|
}
|
||||||
|
var added, removed []string
|
||||||
|
unchanged := 0
|
||||||
|
for p := range setB {
|
||||||
|
if _, ok := setA[p]; !ok {
|
||||||
|
added = append(added, p)
|
||||||
|
} else {
|
||||||
|
unchanged++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for p := range setA {
|
||||||
|
if _, ok := setB[p]; !ok {
|
||||||
|
removed = append(removed, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(added)
|
||||||
|
sort.Strings(removed)
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"revision_a": aID,
|
"revision_a": aID,
|
||||||
"revision_b": bID,
|
"revision_b": bID,
|
||||||
"prefixes": map[string]any{
|
"prefixes": map[string]any{
|
||||||
"added": []string{},
|
"added": added,
|
||||||
"removed": []string{},
|
"removed": removed,
|
||||||
"unchanged_count": 0,
|
"unchanged_count": unchanged,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,798 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Backend = (*Memory)(nil)
|
||||||
|
|
||||||
|
func (m *Memory) CreateModule(tenantID string, in *Module) (*Module, error) {
|
||||||
|
if in == nil || strings.TrimSpace(in.Type) == "" || strings.TrimSpace(in.Name) == "" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, ok := m.tenants[tenantID]; !ok {
|
||||||
|
return nil, ErrTenantScope
|
||||||
|
}
|
||||||
|
id := uuid.NewString()
|
||||||
|
mod := &Module{
|
||||||
|
ID: id,
|
||||||
|
TenantID: tenantID,
|
||||||
|
Type: in.Type,
|
||||||
|
Name: strings.TrimSpace(in.Name),
|
||||||
|
Enabled: in.Enabled,
|
||||||
|
Priority: in.Priority,
|
||||||
|
RefreshIntervalSec: in.RefreshIntervalSec,
|
||||||
|
CronExpr: in.CronExpr,
|
||||||
|
DefaultCommunityID: in.DefaultCommunityID,
|
||||||
|
DohProfileID: in.DohProfileID,
|
||||||
|
}
|
||||||
|
m.modules[id] = mod
|
||||||
|
return mod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*Module, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
mod, ok := m.modules[moduleID]
|
||||||
|
if !ok || mod.DeletedAt != nil || mod.TenantID != tenantID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.Name != nil {
|
||||||
|
mod.Name = strings.TrimSpace(*patch.Name)
|
||||||
|
}
|
||||||
|
if patch.Enabled != nil {
|
||||||
|
mod.Enabled = *patch.Enabled
|
||||||
|
}
|
||||||
|
if patch.Priority != nil {
|
||||||
|
mod.Priority = *patch.Priority
|
||||||
|
}
|
||||||
|
if patch.RefreshIntervalSec != nil {
|
||||||
|
mod.RefreshIntervalSec = *patch.RefreshIntervalSec
|
||||||
|
}
|
||||||
|
if patch.CronExpr != nil {
|
||||||
|
mod.CronExpr = *patch.CronExpr
|
||||||
|
}
|
||||||
|
if patch.DefaultCommunityID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.DefaultCommunityID)
|
||||||
|
if v == "" {
|
||||||
|
mod.DefaultCommunityID = nil
|
||||||
|
} else {
|
||||||
|
mod.DefaultCommunityID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if patch.DohProfileID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.DohProfileID)
|
||||||
|
if v == "" {
|
||||||
|
mod.DohProfileID = nil
|
||||||
|
} else {
|
||||||
|
mod.DohProfileID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) SoftDeleteModule(tenantID, moduleID string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
mod, ok := m.modules[moduleID]
|
||||||
|
if !ok || mod.TenantID != tenantID {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
now := time.Now().UTC()
|
||||||
|
mod.DeletedAt = &now
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) moduleWriteOK(tenantID, moduleID string) (*Module, error) {
|
||||||
|
mod, ok := m.modules[moduleID]
|
||||||
|
if !ok || mod.DeletedAt != nil || mod.TenantID != tenantID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return mod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListCDNSources(tenantID, moduleID string) ([]*CDNSource, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "CDN_CIDRS" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
var out []*CDNSource
|
||||||
|
for _, s := range m.cdnSources {
|
||||||
|
if s.ModuleID == moduleID {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateCDNSource(tenantID, moduleID string, in *CDNSource) (*CDNSource, error) {
|
||||||
|
if in == nil || strings.TrimSpace(in.URL) == "" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "CDN_CIDRS" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
id := uuid.NewString()
|
||||||
|
s := &CDNSource{
|
||||||
|
ID: id,
|
||||||
|
ModuleID: moduleID,
|
||||||
|
SourceKind: in.SourceKind,
|
||||||
|
URL: strings.TrimSpace(in.URL),
|
||||||
|
Etag: in.Etag,
|
||||||
|
RefreshIntervalSec: in.RefreshIntervalSec,
|
||||||
|
CommunityID: in.CommunityID,
|
||||||
|
}
|
||||||
|
m.cdnSources[id] = s
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *CDNSourcePatch) (*CDNSource, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s, ok := m.cdnSources[sourceID]
|
||||||
|
if !ok || s.ModuleID != moduleID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.SourceKind != nil {
|
||||||
|
s.SourceKind = *patch.SourceKind
|
||||||
|
}
|
||||||
|
if patch.URL != nil {
|
||||||
|
s.URL = strings.TrimSpace(*patch.URL)
|
||||||
|
}
|
||||||
|
if patch.Etag != nil {
|
||||||
|
s.Etag = *patch.Etag
|
||||||
|
}
|
||||||
|
if patch.RefreshIntervalSec != nil {
|
||||||
|
s.RefreshIntervalSec = patch.RefreshIntervalSec
|
||||||
|
}
|
||||||
|
if patch.CommunityID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.CommunityID)
|
||||||
|
if v == "" {
|
||||||
|
s.CommunityID = nil
|
||||||
|
} else {
|
||||||
|
s.CommunityID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteCDNSource(tenantID, moduleID, sourceID string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s, ok := m.cdnSources[sourceID]
|
||||||
|
if !ok || s.ModuleID != moduleID {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
delete(m.cdnSources, sourceID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListASEntries(tenantID, moduleID string) ([]*ASEntry, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "AS_PREFIXES" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
var out []*ASEntry
|
||||||
|
for _, e := range m.asEntries {
|
||||||
|
if e.ModuleID == moduleID {
|
||||||
|
out = append(out, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateASEntry(tenantID, moduleID string, in *ASEntry) (*ASEntry, error) {
|
||||||
|
if in == nil || (in.ASN == nil && (in.Prefix == nil || strings.TrimSpace(*in.Prefix) == "")) {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "AS_PREFIXES" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
id := uuid.NewString()
|
||||||
|
e := &ASEntry{ID: id, ModuleID: moduleID, ASN: in.ASN, Prefix: in.Prefix, CommunityID: in.CommunityID}
|
||||||
|
m.asEntries[id] = e
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntryPatch) (*ASEntry, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e, ok := m.asEntries[entryID]
|
||||||
|
if !ok || e.ModuleID != moduleID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.ASN != nil {
|
||||||
|
e.ASN = patch.ASN
|
||||||
|
}
|
||||||
|
if patch.Prefix != nil {
|
||||||
|
p := strings.TrimSpace(*patch.Prefix)
|
||||||
|
if p == "" {
|
||||||
|
e.Prefix = nil
|
||||||
|
} else {
|
||||||
|
e.Prefix = &p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if patch.CommunityID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.CommunityID)
|
||||||
|
if v == "" {
|
||||||
|
e.CommunityID = nil
|
||||||
|
} else {
|
||||||
|
e.CommunityID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteASEntry(tenantID, moduleID, entryID string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e, ok := m.asEntries[entryID]
|
||||||
|
if !ok || e.ModuleID != moduleID {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
delete(m.asEntries, entryID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListDomainEntries(tenantID, moduleID string) ([]*DomainEntry, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "DOMAINS" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
var out []*DomainEntry
|
||||||
|
for _, e := range m.domainEnt {
|
||||||
|
if e.ModuleID == moduleID {
|
||||||
|
out = append(out, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateDomainEntry(tenantID, moduleID string, in *DomainEntry) (*DomainEntry, error) {
|
||||||
|
if in == nil || strings.TrimSpace(in.FQDN) == "" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "DOMAINS" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
id := uuid.NewString()
|
||||||
|
e := &DomainEntry{ID: id, ModuleID: moduleID, FQDN: strings.TrimSpace(in.FQDN), CommunityID: in.CommunityID}
|
||||||
|
m.domainEnt[id] = e
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *DomainEntryPatch) (*DomainEntry, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e, ok := m.domainEnt[entryID]
|
||||||
|
if !ok || e.ModuleID != moduleID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.FQDN != nil {
|
||||||
|
e.FQDN = strings.TrimSpace(*patch.FQDN)
|
||||||
|
}
|
||||||
|
if patch.CommunityID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.CommunityID)
|
||||||
|
if v == "" {
|
||||||
|
e.CommunityID = nil
|
||||||
|
} else {
|
||||||
|
e.CommunityID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteDomainEntry(tenantID, moduleID, entryID string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e, ok := m.domainEnt[entryID]
|
||||||
|
if !ok || e.ModuleID != moduleID {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
delete(m.domainEnt, entryID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListIPRangeEntries(tenantID, moduleID string) ([]*IPRangeEntry, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "IP_RANGES" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
var out []*IPRangeEntry
|
||||||
|
for _, e := range m.ipRanges {
|
||||||
|
if e.ModuleID == moduleID {
|
||||||
|
out = append(out, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateIPRangeEntry(tenantID, moduleID string, in *IPRangeEntry) (*IPRangeEntry, error) {
|
||||||
|
if in == nil || strings.TrimSpace(in.Prefix) == "" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
mod, err := m.moduleWriteOK(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mod.Type != "IP_RANGES" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
id := uuid.NewString()
|
||||||
|
e := &IPRangeEntry{ID: id, ModuleID: moduleID, Prefix: strings.TrimSpace(in.Prefix), CommunityID: in.CommunityID}
|
||||||
|
m.ipRanges[id] = e
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *IPRangePatch) (*IPRangeEntry, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e, ok := m.ipRanges[entryID]
|
||||||
|
if !ok || e.ModuleID != moduleID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.Prefix != nil {
|
||||||
|
e.Prefix = strings.TrimSpace(*patch.Prefix)
|
||||||
|
}
|
||||||
|
if patch.CommunityID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.CommunityID)
|
||||||
|
if v == "" {
|
||||||
|
e.CommunityID = nil
|
||||||
|
} else {
|
||||||
|
e.CommunityID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, err := m.moduleWriteOK(tenantID, moduleID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e, ok := m.ipRanges[entryID]
|
||||||
|
if !ok || e.ModuleID != moduleID {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
delete(m.ipRanges, entryID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListDohProfiles(tenantID string) ([]*DohProfile, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
var out []*DohProfile
|
||||||
|
for _, p := range m.dohProfiles {
|
||||||
|
if p.TenantID == tenantID {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) GetDohProfile(tenantID, id string) (*DohProfile, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
p, ok := m.dohProfiles[id]
|
||||||
|
if !ok || p.TenantID != tenantID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateDohProfile(tenantID string, in *DohProfile) (*DohProfile, error) {
|
||||||
|
if in == nil || strings.TrimSpace(in.URL) == "" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, ok := m.tenants[tenantID]; !ok {
|
||||||
|
return nil, ErrTenantScope
|
||||||
|
}
|
||||||
|
id := uuid.NewString()
|
||||||
|
p := &DohProfile{
|
||||||
|
ID: id,
|
||||||
|
TenantID: tenantID,
|
||||||
|
Name: in.Name,
|
||||||
|
URL: strings.TrimSpace(in.URL),
|
||||||
|
TimeoutMs: in.TimeoutMs,
|
||||||
|
SecretRef: in.SecretRef,
|
||||||
|
}
|
||||||
|
m.dohProfiles[id] = p
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateDohProfile(tenantID, id string, patch *DohProfilePatch) (*DohProfile, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
p, ok := m.dohProfiles[id]
|
||||||
|
if !ok || p.TenantID != tenantID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.Name != nil {
|
||||||
|
p.Name = *patch.Name
|
||||||
|
}
|
||||||
|
if patch.URL != nil {
|
||||||
|
p.URL = strings.TrimSpace(*patch.URL)
|
||||||
|
}
|
||||||
|
if patch.TimeoutMs != nil {
|
||||||
|
p.TimeoutMs = patch.TimeoutMs
|
||||||
|
}
|
||||||
|
if patch.SecretRef != nil {
|
||||||
|
p.SecretRef = patch.SecretRef
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteDohProfile(tenantID, id string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
p, ok := m.dohProfiles[id]
|
||||||
|
if !ok || p.TenantID != tenantID {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
for _, mod := range m.modules {
|
||||||
|
if mod.DohProfileID != nil && *mod.DohProfileID == id {
|
||||||
|
return ErrInvalidInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(m.dohProfiles, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListCommunities(tenantID string) ([]*Community, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
var out []*Community
|
||||||
|
for _, c := range m.communities {
|
||||||
|
if c.TenantID == tenantID {
|
||||||
|
out = append(out, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) GetCommunity(tenantID, id string) (*Community, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
c, ok := m.communities[id]
|
||||||
|
if !ok || c.TenantID != tenantID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateCommunity(tenantID string, in *Community) (*Community, error) {
|
||||||
|
if in == nil || strings.TrimSpace(in.Kind) == "" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, ok := m.tenants[tenantID]; !ok {
|
||||||
|
return nil, ErrTenantScope
|
||||||
|
}
|
||||||
|
id := uuid.NewString()
|
||||||
|
vj := in.ValueJSON
|
||||||
|
if strings.TrimSpace(vj) == "" {
|
||||||
|
vj = "{}"
|
||||||
|
}
|
||||||
|
c := &Community{ID: id, TenantID: tenantID, Name: in.Name, Kind: strings.TrimSpace(in.Kind), ValueJSON: vj}
|
||||||
|
m.communities[id] = c
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateCommunity(tenantID, id string, patch *CommunityPatch) (*Community, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
c, ok := m.communities[id]
|
||||||
|
if !ok || c.TenantID != tenantID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.Name != nil {
|
||||||
|
c.Name = *patch.Name
|
||||||
|
}
|
||||||
|
if patch.Kind != nil {
|
||||||
|
c.Kind = strings.TrimSpace(*patch.Kind)
|
||||||
|
}
|
||||||
|
if patch.ValueJSON != nil {
|
||||||
|
c.ValueJSON = *patch.ValueJSON
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteCommunity(tenantID, id string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
c, ok := m.communities[id]
|
||||||
|
if !ok || c.TenantID != tenantID {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
_ = c
|
||||||
|
// weak check: modules default_community
|
||||||
|
for _, mod := range m.modules {
|
||||||
|
if mod.DefaultCommunityID != nil && *mod.DefaultCommunityID == id {
|
||||||
|
return ErrInvalidInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(m.communities, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) GetPeer(tenantID, id string) (*BGPPeer, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
p, ok := m.peers[id]
|
||||||
|
if !ok || p.TenantID != tenantID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreatePeer(tenantID string, in *BGPPeer) (*BGPPeer, error) {
|
||||||
|
if in == nil || strings.TrimSpace(in.Neighbor) == "" || in.RemoteASN == 0 {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, ok := m.tenants[tenantID]; !ok {
|
||||||
|
return nil, ErrTenantScope
|
||||||
|
}
|
||||||
|
id := uuid.NewString()
|
||||||
|
p := &BGPPeer{
|
||||||
|
ID: id, TenantID: tenantID, SpeakerID: in.SpeakerID, Name: in.Name,
|
||||||
|
Neighbor: strings.TrimSpace(in.Neighbor), RemoteASN: in.RemoteASN, Enabled: in.Enabled,
|
||||||
|
SessionState: in.SessionState, PoliciesJSON: in.PoliciesJSON,
|
||||||
|
}
|
||||||
|
if !p.Enabled && p.SessionState == "" {
|
||||||
|
p.Enabled = true
|
||||||
|
}
|
||||||
|
m.peers[id] = p
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdatePeer(tenantID, id string, patch *PeerPatch) (*BGPPeer, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
p, ok := m.peers[id]
|
||||||
|
if !ok || p.TenantID != tenantID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.Neighbor != nil {
|
||||||
|
p.Neighbor = strings.TrimSpace(*patch.Neighbor)
|
||||||
|
}
|
||||||
|
if patch.RemoteASN != nil {
|
||||||
|
p.RemoteASN = *patch.RemoteASN
|
||||||
|
}
|
||||||
|
if patch.SpeakerID != nil {
|
||||||
|
v := strings.TrimSpace(*patch.SpeakerID)
|
||||||
|
if v == "" {
|
||||||
|
p.SpeakerID = nil
|
||||||
|
} else {
|
||||||
|
p.SpeakerID = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if patch.Enabled != nil {
|
||||||
|
p.Enabled = *patch.Enabled
|
||||||
|
}
|
||||||
|
if patch.Name != nil {
|
||||||
|
p.Name = *patch.Name
|
||||||
|
}
|
||||||
|
if patch.SessionState != nil {
|
||||||
|
p.SessionState = *patch.SessionState
|
||||||
|
}
|
||||||
|
if patch.PoliciesJSON != nil {
|
||||||
|
p.PoliciesJSON = *patch.PoliciesJSON
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeletePeer(tenantID, id string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
p, ok := m.peers[id]
|
||||||
|
if !ok || p.TenantID != tenantID {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
delete(m.peers, id)
|
||||||
|
_ = p
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateSpeaker(tenantID string, in *Speaker) (*Speaker, error) {
|
||||||
|
if in == nil || strings.TrimSpace(in.Role) == "" {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, ok := m.tenants[tenantID]; !ok {
|
||||||
|
return nil, ErrTenantScope
|
||||||
|
}
|
||||||
|
id := uuid.NewString()
|
||||||
|
sp := &Speaker{ID: id, TenantID: tenantID, Role: strings.TrimSpace(in.Role), Endpoint: in.Endpoint, MetaJSON: in.MetaJSON}
|
||||||
|
m.speakers[id] = sp
|
||||||
|
return sp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateSpeaker(tenantID, id string, patch *SpeakerPatch) (*Speaker, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
sp, ok := m.speakers[id]
|
||||||
|
if !ok || sp.TenantID != tenantID {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.Role != nil {
|
||||||
|
sp.Role = strings.TrimSpace(*patch.Role)
|
||||||
|
}
|
||||||
|
if patch.Endpoint != nil {
|
||||||
|
sp.Endpoint = *patch.Endpoint
|
||||||
|
}
|
||||||
|
if patch.MetaJSON != nil {
|
||||||
|
sp.MetaJSON = *patch.MetaJSON
|
||||||
|
}
|
||||||
|
return sp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListRevisionPrefixes(tenantID, revisionID string, cursor string, limit int) ([]PrefixRow, string, bool) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if _, err := m.getRevisionLocked(tenantID, revisionID); err != nil {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
all := m.revPrefixes[revisionID]
|
||||||
|
off := 0
|
||||||
|
if cursor != "" {
|
||||||
|
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
|
||||||
|
off = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end := off + limit
|
||||||
|
next := ""
|
||||||
|
more := false
|
||||||
|
if end > len(all) {
|
||||||
|
end = len(all)
|
||||||
|
} else {
|
||||||
|
more = true
|
||||||
|
next = fmt.Sprintf("%d", end)
|
||||||
|
}
|
||||||
|
if off >= len(all) {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
return all[off:end], next, more
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListGlobalSettings(tenantID string) (map[string]any, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if m.settings[tenantID] == nil {
|
||||||
|
return map[string]any{}, nil
|
||||||
|
}
|
||||||
|
out := make(map[string]any, len(m.settings[tenantID]))
|
||||||
|
for k, v := range m.settings[tenantID] {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) PatchGlobalSettings(tenantID string, patch map[string]any) error {
|
||||||
|
if patch == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, ok := m.tenants[tenantID]; !ok {
|
||||||
|
return ErrTenantScope
|
||||||
|
}
|
||||||
|
if m.settings[tenantID] == nil {
|
||||||
|
m.settings[tenantID] = make(map[string]any)
|
||||||
|
}
|
||||||
|
for key, val := range patch {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if val == nil {
|
||||||
|
delete(m.settings[tenantID], key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.settings[tenantID][key] = val
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE bgp_speaker
|
||||||
|
DROP COLUMN IF EXISTS published_at,
|
||||||
|
DROP COLUMN IF EXISTS published_revision_id;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- Published revision pointer for evobgp-node (plan §7.11, §10).
|
||||||
|
|
||||||
|
ALTER TABLE bgp_speaker
|
||||||
|
ADD COLUMN IF NOT EXISTS published_revision_id UUID REFERENCES config_revision (id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS published_at TIMESTAMPTZ;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- SQLite cannot DROP COLUMN easily in older versions; no-op for dev rollback.
|
||||||
|
SELECT 1;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- SQLite: add columns (IF NOT EXISTS per column not supported — use new table copy if needed).
|
||||||
|
-- pragma: skip if columns exist — simplest: ALTER TABLE ADD COLUMN (SQLite allows multiple ADD).
|
||||||
|
|
||||||
|
ALTER TABLE bgp_speaker ADD COLUMN published_revision_id TEXT REFERENCES config_revision (id) ON DELETE SET NULL;
|
||||||
|
ALTER TABLE bgp_speaker ADD COLUMN published_at TEXT;
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Resolves module dependency tree via proxy.golang.org and fetches sum.golang.org lines.
|
||||||
|
# Appends missing entries to go.sum (run from repo root: pwsh -File scripts/expand-go-sum.ps1).
|
||||||
|
|
||||||
|
Set-StrictMode -Version 3
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Escape module path for sum.golang.org / proxy.golang.org (lowercase + %XX for other UTF-8 bytes).
|
||||||
|
function Escape-GoModulePath2([string]$path) {
|
||||||
|
$lower = $path.ToLowerInvariant()
|
||||||
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($lower)
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
foreach ($b in $bytes) {
|
||||||
|
$c = [char]$b
|
||||||
|
$ok = ($b -ge 48 -and $b -le 57) -or ($b -ge 97 -and $b -le 122) -or ($c -eq '-') -or ($c -eq '.') -or ($c -eq '_') -or ($c -eq '+')
|
||||||
|
if ($ok) {
|
||||||
|
[void]$sb.Append($c)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[void]$sb.AppendFormat("%{0:x2}", $b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $sb.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SumdbLines([string]$modPath, [string]$ver) {
|
||||||
|
$enc = Escape-GoModulePath2 $modPath
|
||||||
|
$url = "https://sum.golang.org/lookup/$enc@$ver"
|
||||||
|
$raw = (Invoke-WebRequest -Uri $url -UseBasicParsing).Content
|
||||||
|
$out = New-Object System.Collections.Generic.List[string]
|
||||||
|
foreach ($line in ($raw -split "`n")) {
|
||||||
|
$t = $line.Trim()
|
||||||
|
if ($t -match '^\S+\s+v\S+\s+h1:') {
|
||||||
|
[void]$out.Add($t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-GoMod([string]$modPath, [string]$ver) {
|
||||||
|
$enc = Escape-GoModulePath2 $modPath
|
||||||
|
$url = "https://proxy.golang.org/$enc/@v/$ver.mod"
|
||||||
|
return (Invoke-WebRequest -Uri $url -UseBasicParsing).Content
|
||||||
|
}
|
||||||
|
|
||||||
|
function Parse-Requires([string]$modText) {
|
||||||
|
$req = @()
|
||||||
|
$inBlock = $false
|
||||||
|
foreach ($line in ($modText -split "`n")) {
|
||||||
|
$t = $line.Trim()
|
||||||
|
if ($t.StartsWith("//")) { continue }
|
||||||
|
if ($t -match '^require\s+\(\s*$') { $inBlock = $true; continue }
|
||||||
|
if ($inBlock) {
|
||||||
|
if ($t -eq ")") { $inBlock = $false; continue }
|
||||||
|
if ($t -match '^(\S+)\s+(v[^\s]+)') {
|
||||||
|
$req += ,@($Matches[1], $Matches[2])
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ($t -match '^require\s+(\S+)\s+(v[^\s]+)\s*(//.*)?$') {
|
||||||
|
$req += ,@($Matches[1], $Matches[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $req
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = Split-Path -Parent $PSScriptRoot
|
||||||
|
Set-Location $root
|
||||||
|
|
||||||
|
$queue = [System.Collections.Queue]::new()
|
||||||
|
$seen = @{}
|
||||||
|
$allLines = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
||||||
|
|
||||||
|
foreach ($line in Get-Content -Path (Join-Path $root "go.sum")) {
|
||||||
|
if ($line.Trim()) { [void]$allLines.Add($line.Trim()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Enqueue([string]$p, [string]$v) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($p) -or [string]::IsNullOrWhiteSpace($v)) { return }
|
||||||
|
if ($p -notmatch '\.') { return }
|
||||||
|
$k = "$p@$v"
|
||||||
|
if ($seen.ContainsKey($k)) { return }
|
||||||
|
$seen[$k] = $true
|
||||||
|
$queue.Enqueue($k)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Seed from go.mod direct requires (edit if go.mod changes)
|
||||||
|
Enqueue "github.com/google/uuid" "v1.6.0"
|
||||||
|
Enqueue "github.com/jackc/pgx/v5" "v5.7.2"
|
||||||
|
Enqueue "github.com/prometheus/client_golang" "v1.20.5"
|
||||||
|
Enqueue "modernc.org/sqlite" "v1.34.5"
|
||||||
|
|
||||||
|
while ($queue.Count -gt 0) {
|
||||||
|
$item = $queue.Dequeue()
|
||||||
|
$p, $v = $item.Split("@", 2)
|
||||||
|
|
||||||
|
foreach ($ln in (Get-SumdbLines $p $v)) {
|
||||||
|
[void]$allLines.Add($ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
$modBody = Get-GoMod $p $v
|
||||||
|
foreach ($pair in (Parse-Requires $modBody)) {
|
||||||
|
Enqueue $pair[0] $pair[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ordered = @($allLines | Sort-Object)
|
||||||
|
$pathSum = Join-Path $root "go.sum"
|
||||||
|
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||||||
|
[System.IO.File]::WriteAllLines($pathSum, $ordered, $utf8NoBom)
|
||||||
|
Write-Host "go.sum written: $($ordered.Count) lines"
|
||||||
Generated
+182
-95
@@ -8,7 +8,9 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@internationalized/date": "^3.8.1",
|
||||||
"@lucide/svelte": "^1.7.0",
|
"@lucide/svelte": "^1.7.0",
|
||||||
|
"bits-ui": "^2.17.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"svelte-sonner": "^1.1.0",
|
"svelte-sonner": "^1.1.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
@@ -37,7 +39,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -54,7 +55,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -71,7 +71,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -88,7 +87,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -105,7 +103,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -122,7 +119,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -139,7 +135,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -156,7 +151,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -173,7 +167,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -190,7 +183,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -207,7 +199,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -224,7 +215,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -241,7 +231,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -258,7 +247,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -275,7 +263,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -292,7 +279,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -309,7 +295,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -326,7 +311,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -343,7 +327,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -360,7 +343,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -377,7 +359,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -394,7 +375,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -411,7 +391,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -428,7 +407,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -445,7 +423,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -462,7 +439,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -472,6 +448,40 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||||
|
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.5",
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||||
|
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@internationalized/date": {
|
||||||
|
"version": "3.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz",
|
||||||
|
"integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -530,7 +540,7 @@
|
|||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
@@ -540,7 +550,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -554,7 +563,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -568,7 +576,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -582,7 +589,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -596,7 +602,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -610,7 +615,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -624,7 +628,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -638,7 +641,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -652,7 +654,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -666,7 +667,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -680,7 +680,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -694,7 +693,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -708,7 +706,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -722,7 +719,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -736,7 +732,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -750,7 +745,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -764,7 +758,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -778,7 +771,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -792,7 +784,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -806,7 +797,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -820,7 +810,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -834,7 +823,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -848,7 +836,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -862,7 +849,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -876,7 +862,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -887,7 +872,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/acorn-typescript": {
|
"node_modules/@sveltejs/acorn-typescript": {
|
||||||
@@ -913,7 +898,7 @@
|
|||||||
"version": "2.56.1",
|
"version": "2.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.56.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.56.1.tgz",
|
||||||
"integrity": "sha512-9hDOl3yUh8UXWt+mN29dbcdrW0vNwPvMqi01y2Mw+ceErNIISh8MeEY7fXT2Dx1CjC/kfsVqrbxw7DifYr4hsg==",
|
"integrity": "sha512-9hDOl3yUh8UXWt+mN29dbcdrW0vNwPvMqi01y2Mw+ceErNIISh8MeEY7fXT2Dx1CjC/kfsVqrbxw7DifYr4hsg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
@@ -955,7 +940,7 @@
|
|||||||
"version": "6.2.4",
|
"version": "6.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
|
||||||
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
|
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||||
@@ -976,7 +961,7 @@
|
|||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
|
||||||
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
|
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"obug": "^2.1.0"
|
"obug": "^2.1.0"
|
||||||
@@ -990,6 +975,15 @@
|
|||||||
"vite": "^6.3.0 || ^7.0.0"
|
"vite": "^6.3.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/helpers": {
|
||||||
|
"version": "0.5.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",
|
||||||
|
"integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
|
||||||
@@ -1266,7 +1260,7 @@
|
|||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
@@ -1324,6 +1318,30 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bits-ui": {
|
||||||
|
"version": "2.17.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.17.2.tgz",
|
||||||
|
"integrity": "sha512-Xbmrlf3ft/5RtMgUL/uam+rcrWYM4LoIGosklV34iSE6GGsPFWgaivzzX60rnWjWlG5F7ZuXXZ4iag949UekMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.1",
|
||||||
|
"@floating-ui/dom": "^1.7.1",
|
||||||
|
"esm-env": "^1.1.2",
|
||||||
|
"runed": "^0.35.1",
|
||||||
|
"svelte-toolbelt": "^0.10.6",
|
||||||
|
"tabbable": "^6.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/huntabyte"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@internationalized/date": "^3.8.1",
|
||||||
|
"svelte": "^5.33.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@@ -1353,7 +1371,7 @@
|
|||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@@ -1363,17 +1381,26 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -1403,7 +1430,7 @@
|
|||||||
"version": "0.27.7",
|
"version": "0.27.7",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1461,7 +1488,7 @@
|
|||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -1479,7 +1506,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -1497,6 +1523,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/inline-style-parser": {
|
||||||
|
"version": "0.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
||||||
|
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-reference": {
|
"node_modules/is-reference": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
@@ -1510,7 +1542,7 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
@@ -1520,7 +1552,7 @@
|
|||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||||
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -1530,7 +1562,7 @@
|
|||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
@@ -1563,7 +1595,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1584,7 +1615,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1605,7 +1635,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1626,7 +1655,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1647,7 +1675,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1668,7 +1695,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1689,7 +1715,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1710,7 +1735,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1731,7 +1755,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1752,7 +1775,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1773,7 +1795,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1793,6 +1814,15 @@
|
|||||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lz-string": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"lz-string": "bin/bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -1816,7 +1846,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -1826,7 +1856,7 @@
|
|||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1845,7 +1875,7 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://github.com/sponsors/sxzz",
|
"https://github.com/sponsors/sxzz",
|
||||||
"https://opencollective.com/debug"
|
"https://opencollective.com/debug"
|
||||||
@@ -1856,14 +1886,14 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -1876,7 +1906,7 @@
|
|||||||
"version": "8.5.8",
|
"version": "8.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -2025,7 +2055,7 @@
|
|||||||
"version": "4.60.1",
|
"version": "4.60.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||||
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
@@ -2066,6 +2096,30 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/runed": {
|
||||||
|
"version": "0.35.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz",
|
||||||
|
"integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"esm-env": "^1.0.0",
|
||||||
|
"lz-string": "^1.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@sveltejs/kit": "^2.21.0",
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@sveltejs/kit": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||||
@@ -2083,14 +2137,14 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
|
||||||
"integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==",
|
"integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sirv": {
|
"node_modules/sirv": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||||
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@polka/url": "^1.0.0-next.24",
|
"@polka/url": "^1.0.0-next.24",
|
||||||
@@ -2105,12 +2159,21 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-to-object": {
|
||||||
|
"version": "1.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
|
||||||
|
"integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inline-style-parser": "0.2.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.55.1",
|
"version": "5.55.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.1.tgz",
|
||||||
@@ -2190,6 +2253,32 @@
|
|||||||
"svelte": "^5.7.0"
|
"svelte": "^5.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-toolbelt": {
|
||||||
|
"version": "0.10.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
|
||||||
|
"integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"runed": "^0.35.1",
|
||||||
|
"style-to-object": "^1.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"pnpm": ">=8.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.30.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tabbable": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "3.5.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
||||||
@@ -2243,7 +2332,7 @@
|
|||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -2260,7 +2349,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -2270,9 +2359,7 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"license": "0BSD"
|
||||||
"license": "0BSD",
|
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tw-animate-css": {
|
"node_modules/tw-animate-css": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
@@ -2287,7 +2374,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -2301,7 +2388,7 @@
|
|||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
@@ -2376,7 +2463,7 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
|
||||||
"integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
|
"integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"tests/deps/*",
|
"tests/deps/*",
|
||||||
|
|||||||
@@ -28,7 +28,9 @@
|
|||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@internationalized/date": "^3.8.1",
|
||||||
"@lucide/svelte": "^1.7.0",
|
"@lucide/svelte": "^1.7.0",
|
||||||
|
"bits-ui": "^2.17.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"svelte-sonner": "^1.1.0",
|
"svelte-sonner": "^1.1.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user