Compare commits
8
Commits
23910c3393
..
v1.5.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
293115e0e1 | ||
|
|
6d2051f813 | ||
|
|
b51a9ae3b3 | ||
|
|
4d4cd2301f | ||
|
|
d687881eaa | ||
|
|
87e756f34f | ||
|
|
27864fad58 | ||
|
|
dc5e777d07 |
No files matched your search
@@ -53,7 +53,7 @@ powershell -NoProfile -File scripts/commit/staged-context.ps1
|
|||||||
| `refactor` | реструктуризация **без** новой возможности и **без** исправления бага | — |
|
| `refactor` | реструктуризация **без** новой возможности и **без** исправления бага | — |
|
||||||
| `docs` | только документация | — |
|
| `docs` | только документация | — |
|
||||||
| `test` | тесты | — |
|
| `test` | тесты | — |
|
||||||
| `ci` | CI/CD (`.gitea/`, workflows) | — |
|
| `ci` | CI/CD (`.gitea/`, workflows); правки, из‑за которых нужны новые образы | patch |
|
||||||
| `chore` | обслуживание, deps, `.cursor/` | — |
|
| `chore` | обслуживание, deps, `.cursor/` | — |
|
||||||
|
|
||||||
### Выбор type: semver, а не «красивые слова»
|
### Выбор type: semver, а не «красивые слова»
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ Workflow: [workflows/ci.yaml](workflows/ci.yaml).
|
|||||||
|
|
||||||
## CI (quality gates)
|
## CI (quality gates)
|
||||||
|
|
||||||
Job **changes** вычисляет флаги по путям в diff. Изменение `.gitea/workflows/*` поднимает полный прогон.
|
Job **changes** вычисляет флаги по путям в diff. Полный прогон (все узлы openapi / web / go / bird2 в графе): `.gitea/workflows/*`, `scripts/*`, `.golangci.yml`, `.pre-commit-config.yaml`, корневой `package.json` / `.releaserc.json`. Отдельно: `migrations/*`, `docs/openapi.yaml` → `go` / `openapi` и т.д. (см. `ci.yaml`).
|
||||||
|
|
||||||
На **pull request** — **commitlint** (Conventional Commits).
|
На **pull request** — **commitlint** (Conventional Commits).
|
||||||
|
|
||||||
|
|||||||
+79
-39
@@ -8,10 +8,9 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Гранулярная детекция изменений по модулям.
|
# Детекция изменений по модулям (флаги → downstream-джобы в графе CI).
|
||||||
# Каждый флаг соответствует группе файлов; downstream-джобы запускаются
|
# Полный прогон (все флаги true): .gitea/workflows/*, scripts/*, .golangci.yml,
|
||||||
# только когда их группа затронута. Изменение CI-конфигурации (.gitea/workflows/*)
|
# .pre-commit-config.yaml — чтобы при правках CI/CD пересобирались все узлы.
|
||||||
# поднимает все флаги, чтобы гарантировать полный прогон.
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
changes:
|
changes:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -40,6 +39,23 @@ jobs:
|
|||||||
docker_web=false
|
docker_web=false
|
||||||
docker_bird=false
|
docker_bird=false
|
||||||
|
|
||||||
|
# Все флаги true → openapi, web, go, bird2 (и release на main) в графе CI.
|
||||||
|
set_all_flags_true() {
|
||||||
|
openapi=true
|
||||||
|
go=true
|
||||||
|
web=true
|
||||||
|
bird_conf=true
|
||||||
|
docker_go=true
|
||||||
|
docker_web=true
|
||||||
|
docker_bird=true
|
||||||
|
}
|
||||||
|
|
||||||
|
write_outputs() {
|
||||||
|
for v in openapi go web bird_conf docker_go docker_web docker_bird; do
|
||||||
|
eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
base="${{ github.event.pull_request.base.sha }}"
|
base="${{ github.event.pull_request.base.sha }}"
|
||||||
head="${{ github.event.pull_request.head.sha }}"
|
head="${{ github.event.pull_request.head.sha }}"
|
||||||
@@ -52,62 +68,84 @@ jobs:
|
|||||||
elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
||||||
FILES="$(git diff --name-only HEAD~1 HEAD)"
|
FILES="$(git diff --name-only HEAD~1 HEAD)"
|
||||||
else
|
else
|
||||||
openapi=true; go=true; web=true; bird_conf=true
|
set_all_flags_true
|
||||||
docker_go=true; docker_web=true; docker_bird=true
|
write_outputs
|
||||||
for v in openapi go web bird_conf docker_go docker_web docker_bird; do
|
echo "No parent commit — full pipeline (all modules)"
|
||||||
echo "$v=true" >> "$GITHUB_OUTPUT"
|
|
||||||
done
|
|
||||||
echo "No parent commit — full pipeline"
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$(printf '%s' "$FILES" | tr -d '[:space:]')" ]; then
|
if [ -z "$(printf '%s' "$FILES" | tr -d '[:space:]')" ]; then
|
||||||
go=true; web=true
|
set_all_flags_true
|
||||||
for v in openapi go web bird_conf docker_go docker_web docker_bird; do
|
write_outputs
|
||||||
eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT"
|
echo "Empty diff — full pipeline fallback"
|
||||||
done
|
|
||||||
echo "Empty diff — safe fallback: go=true web=true"
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ci_changed=false
|
full_pipeline=false
|
||||||
|
|
||||||
while IFS= read -r f || [ -n "${f:-}" ]; do
|
while IFS= read -r f || [ -n "${f:-}" ]; do
|
||||||
[ -z "${f:-}" ] && continue
|
[ -z "${f:-}" ] && continue
|
||||||
case "$f" in
|
case "$f" in
|
||||||
.gitea/workflows/*) ci_changed=true ;;
|
# CI/CD инфраструктура — все узлы quality gates
|
||||||
docs/openapi.yaml|redocly.yaml) openapi=true ;;
|
.gitea/workflows/*|.golangci.yml|.pre-commit-config.yaml|scripts/*)
|
||||||
web/README.md) ;; # doc-only
|
full_pipeline=true
|
||||||
web/*) web=true ;;
|
;;
|
||||||
deploy/bird/*) bird_conf=true ;;
|
docs/openapi.yaml|redocly.yaml)
|
||||||
deploy/docker/bird/*) docker_bird=true; docker_go=true ;;
|
openapi=true
|
||||||
deploy/docker/gobinary/*) docker_go=true ;;
|
;;
|
||||||
deploy/docker/docker-bake.hcl) docker_go=true; docker_web=true ;;
|
docs/api.md|docs/access.md)
|
||||||
deploy/docker/evobgp-agent/*) docker_go=true ;;
|
openapi=true
|
||||||
deploy/docker/evobgp-web/*) docker_web=true ;;
|
go=true
|
||||||
deploy/docker/bird2/*) docker_bird=true ;;
|
;;
|
||||||
go.mod|go.sum|go.work) go=true ;;
|
web/README.md|web/components.json)
|
||||||
migrations/*) go=true ;;
|
;;
|
||||||
*.go) go=true ;;
|
web/*)
|
||||||
cmd/*|internal/*) go=true ;;
|
web=true
|
||||||
|
;;
|
||||||
|
deploy/bird/*)
|
||||||
|
bird_conf=true
|
||||||
|
go=true
|
||||||
|
;;
|
||||||
|
deploy/compose/*|deploy/docker/*)
|
||||||
|
docker_go=true
|
||||||
|
docker_web=true
|
||||||
|
docker_bird=true
|
||||||
|
go=true
|
||||||
|
;;
|
||||||
|
go.mod|go.sum|go.work)
|
||||||
|
go=true
|
||||||
|
;;
|
||||||
|
migrations/*)
|
||||||
|
go=true
|
||||||
|
;;
|
||||||
|
cmd/*|internal/*|*.go)
|
||||||
|
go=true
|
||||||
|
bird_conf=true
|
||||||
|
;;
|
||||||
|
docs/*)
|
||||||
|
go=true
|
||||||
|
;;
|
||||||
|
package.json|package-lock.json|.releaserc.json)
|
||||||
|
full_pipeline=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
go=true
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done <<< "$FILES"
|
done <<< "$FILES"
|
||||||
|
|
||||||
if $ci_changed; then
|
if $full_pipeline; then
|
||||||
go=true; web=true; bird_conf=true
|
set_all_flags_true
|
||||||
docker_go=true; docker_web=true; docker_bird=true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for v in openapi go web bird_conf docker_go docker_web docker_bird; do
|
write_outputs
|
||||||
eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Changed files (first 30):"
|
echo "Changed files (first 30):"
|
||||||
printf '%s\n' "$FILES" | head -n 30
|
printf '%s\n' "$FILES" | head -n 30
|
||||||
echo "--- flags ---"
|
echo "--- flags ---"
|
||||||
echo "openapi=$openapi go=$go web=$web bird_conf=$bird_conf"
|
echo "openapi=$openapi go=$go web=$web bird_conf=$bird_conf"
|
||||||
echo "docker_go=$docker_go docker_web=$docker_web docker_bird=$docker_bird ci=$ci_changed"
|
echo "docker_go=$docker_go docker_web=$docker_web docker_bird=$docker_bird full_pipeline=$full_pipeline"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
openapi:
|
openapi:
|
||||||
@@ -160,10 +198,12 @@ jobs:
|
|||||||
run: sh scripts/lint-httpapi.sh
|
run: sh scripts/lint-httpapi.sh
|
||||||
- name: Check migration pairs (DEP-03)
|
- name: Check migration pairs (DEP-03)
|
||||||
run: sh scripts/check-migrations-pair.sh
|
run: sh scripts/check-migrations-pair.sh
|
||||||
|
# go.mod: go 1.24 — бинарник golangci-lint < v1.64.2 (сборка на Go 1.23) не запускается.
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: v1.62
|
version: v1.64.8
|
||||||
|
install-mode: goinstall
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test ./... -race -count=1
|
run: go test ./... -race -count=1
|
||||||
- name: Build all commands
|
- name: Build all commands
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ run:
|
|||||||
timeout: 5m
|
timeout: 5m
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- gofmt
|
- gofmt
|
||||||
- govet
|
- govet
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
{ "type": "feat", "release": "minor" },
|
{ "type": "feat", "release": "minor" },
|
||||||
{ "type": "fix", "release": "patch" },
|
{ "type": "fix", "release": "patch" },
|
||||||
{ "type": "perf", "release": "patch" },
|
{ "type": "perf", "release": "patch" },
|
||||||
|
{ "type": "ci", "release": "patch" },
|
||||||
{ "breaking": true, "release": "major" }
|
{ "breaking": true, "release": "major" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ server {
|
|||||||
gzip on;
|
gzip on;
|
||||||
gzip_types text/css application/javascript application/json image/svg+xml;
|
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||||
|
|
||||||
|
# Docker embedded DNS: без resolver nginx кэширует IP upstream при старте —
|
||||||
|
# после recreate evobgp-all остаётся 502 (connection refused на старый IP).
|
||||||
|
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||||
|
set $evobgp_upstream evobgp-api;
|
||||||
|
|
||||||
location /v1/ {
|
location /v1/ {
|
||||||
proxy_pass http://evobgp-api:8080/v1/;
|
# С переменной в proxy_pass нельзя полагаться на замену URI — передаём $request_uri целиком.
|
||||||
|
proxy_pass http://$evobgp_upstream:8080$request_uri;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -15,7 +21,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location = /metrics {
|
location = /metrics {
|
||||||
proxy_pass http://evobgp-api:8080/metrics;
|
proxy_pass http://$evobgp_upstream:8080/metrics;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -7,9 +7,11 @@ EvoBGP использует [Conventional Commits](https://www.conventionalcommi
|
|||||||
| Тип коммита | Bump |
|
| Тип коммита | Bump |
|
||||||
|-------------|------|
|
|-------------|------|
|
||||||
| `feat` | minor (1.0.0 → 1.1.0) |
|
| `feat` | minor (1.0.0 → 1.1.0) |
|
||||||
| `fix`, `perf` | patch (1.0.0 → 1.0.1) |
|
| `fix`, `perf`, `ci` | patch (1.5.1 → 1.5.2) |
|
||||||
| `feat!`, `fix!` или `BREAKING CHANGE:` в теле | major (1.0.0 → 2.0.0) |
|
| `feat!`, `fix!` или `BREAKING CHANGE:` в теле | major (1.0.0 → 2.0.0) |
|
||||||
| `docs`, `chore`, `ci`, `test`, `refactor` | без релиза |
|
| `docs`, `chore`, `test`, `refactor` | без релиза |
|
||||||
|
|
||||||
|
Отдельного суффикса `1.x.y.fix` в semver нет: «fix» в Conventional Commits означает **patch** (третья цифра). Для починки пайплайна без смены продукта — `fix(ci):` или `ci:` (оба дают patch после настройки `.releaserc.json`).
|
||||||
|
|
||||||
Первый релиз при отсутствии git-тегов — **1.0.0**, если есть releasable-коммиты.
|
Первый релиз при отсутствии git-тегов — **1.0.0**, если есть releasable-коммиты.
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func AnnouncedPrefixes(ctx context.Context, hc *http.Client, asn int64) ([]netip
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ripestat fetch AS%d: %w", asn, err)
|
return nil, fmt.Errorf("ripestat fetch AS%d: %w", asn, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -52,8 +52,8 @@ func AnnouncedPrefixes(ctx context.Context, hc *http.Client, asn int64) ([]netip
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wrap struct {
|
var wrap struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Data struct {
|
Data struct {
|
||||||
Prefixes []struct {
|
Prefixes []struct {
|
||||||
Prefix string `json:"prefix"`
|
Prefix string `json:"prefix"`
|
||||||
} `json:"prefixes"`
|
} `json:"prefixes"`
|
||||||
@@ -104,7 +104,7 @@ func ASHolderName(ctx context.Context, hc *http.Client, asn int64) (string, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("ripestat as-overview AS%d: %w", asn, err)
|
return "", fmt.Errorf("ripestat as-overview AS%d: %w", asn, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ func RenderBGPTemplates(opts BGPTemplatesOptions) (string, error) {
|
|||||||
|
|
||||||
// BGPPeerFromTemplateOptions describes protocol bgp NAME from TEMPLATE { … }.
|
// BGPPeerFromTemplateOptions describes protocol bgp NAME from TEMPLATE { … }.
|
||||||
type BGPPeerFromTemplateOptions struct {
|
type BGPPeerFromTemplateOptions struct {
|
||||||
ProtocolName string
|
ProtocolName string
|
||||||
TemplateName string
|
TemplateName string
|
||||||
NeighborIP string
|
NeighborIP string
|
||||||
NeighborASN uint32
|
NeighborASN uint32
|
||||||
// If set, emits "local … as …" before neighbor (overrides template local/ASN for this peer).
|
// If set, emits "local … as …" before neighbor (overrides template local/ASN for this peer).
|
||||||
OverrideLocalIP string
|
OverrideLocalIP string
|
||||||
OverrideLocalASN uint32
|
OverrideLocalASN uint32
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import (
|
|||||||
|
|
||||||
// Manifest describes bundle contents for evobgp-node verification.
|
// Manifest describes bundle contents for evobgp-node verification.
|
||||||
type Manifest struct {
|
type Manifest struct {
|
||||||
RevisionID string `json:"revision_id"`
|
RevisionID string `json:"revision_id"`
|
||||||
SpeakerID string `json:"speaker_id,omitempty"`
|
SpeakerID string `json:"speaker_id,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Files []FileEntry `json:"files"`
|
Files []FileEntry `json:"files"`
|
||||||
Algorithm string `json:"signature_algorithm"`
|
Algorithm string `json:"signature_algorithm"`
|
||||||
PublicKeyB64 string `json:"public_key_base64"`
|
PublicKeyB64 string `json:"public_key_base64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileEntry is one file inside the bundle archive.
|
// FileEntry is one file inside the bundle archive.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func VerifyGzippedTar(bundle []byte, pub ed25519.PublicKey) (*VerifiedContents,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer gr.Close()
|
defer func() { _ = gr.Close() }()
|
||||||
|
|
||||||
var manifestRaw []byte
|
var manifestRaw []byte
|
||||||
var sig []byte
|
var sig []byte
|
||||||
|
|||||||
+11
-11
@@ -7,21 +7,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
internalErrorDetail = "an internal error occurred"
|
internalErrorDetail = "an internal error occurred"
|
||||||
badGatewayDetail = "upstream request failed"
|
badGatewayDetail = "upstream request failed"
|
||||||
notFoundDetail = "resource not found"
|
notFoundDetail = "resource not found"
|
||||||
invalidInputDetail = "invalid request data"
|
invalidInputDetail = "invalid request data"
|
||||||
cdnExtractDetail = "could not extract prefixes from source"
|
cdnExtractDetail = "could not extract prefixes from source"
|
||||||
csvInvalidRowDetail = "invalid row in csv file"
|
csvInvalidRowDetail = "invalid row in csv file"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Problem is RFC 9457 application/problem+json.
|
// Problem is RFC 9457 application/problem+json.
|
||||||
type Problem struct {
|
type Problem struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
Detail string `json:"detail,omitempty"`
|
Detail string `json:"detail,omitempty"`
|
||||||
Instance string `json:"instance,omitempty"`
|
Instance string `json:"instance,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeProblem(w http.ResponseWriter, status int, title, detail string) {
|
func writeProblem(w http.ResponseWriter, status int, title, detail string) {
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ func (s *Server) handlePreviewCDNSource(w http.ResponseWriter, r *http.Request)
|
|||||||
writeBadGateway(w, "cdn preview fetch", err)
|
writeBadGateway(w, "cdn preview fetch", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
_, _ = io.Copy(io.Discard, resp.Body)
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
writeBadGateway(w, "cdn preview fetch", fmt.Errorf("upstream status: %s", resp.Status))
|
writeBadGateway(w, "cdn preview fetch", fmt.Errorf("upstream status: %s", resp.Status))
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestModuleEntriesCSVImportExportIPRanges(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer respList.Body.Close()
|
defer func() { _ = respList.Body.Close() }()
|
||||||
if respList.StatusCode != http.StatusOK {
|
if respList.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(respList.Body)
|
b, _ := io.ReadAll(respList.Body)
|
||||||
t.Fatalf("communities status %d: %s", respList.StatusCode, b)
|
t.Fatalf("communities status %d: %s", respList.StatusCode, b)
|
||||||
@@ -59,7 +59,7 @@ func TestModuleEntriesCSVImportExportIPRanges(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer respImport.Body.Close()
|
defer func() { _ = respImport.Body.Close() }()
|
||||||
if respImport.StatusCode != http.StatusOK {
|
if respImport.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(respImport.Body)
|
b, _ := io.ReadAll(respImport.Body)
|
||||||
t.Fatalf("import status %d: %s", respImport.StatusCode, b)
|
t.Fatalf("import status %d: %s", respImport.StatusCode, b)
|
||||||
@@ -80,7 +80,7 @@ func TestModuleEntriesCSVImportExportIPRanges(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer respExport.Body.Close()
|
defer func() { _ = respExport.Body.Close() }()
|
||||||
if respExport.StatusCode != http.StatusOK {
|
if respExport.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(respExport.Body)
|
b, _ := io.ReadAll(respExport.Body)
|
||||||
t.Fatalf("export status %d: %s", respExport.StatusCode, b)
|
t.Fatalf("export status %d: %s", respExport.StatusCode, b)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func TestNestedModuleListPagination(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, _ = io.Copy(io.Discard, resp.Body)
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusCreated {
|
if resp.StatusCode != http.StatusCreated {
|
||||||
t.Fatalf("create entry %d: status %d", i, resp.StatusCode)
|
t.Fatalf("create entry %d: status %d", i, resp.StatusCode)
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ func TestNestedModuleListPagination(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("list status %d: %s", resp.StatusCode, b)
|
t.Fatalf("list status %d: %s", resp.StatusCode, b)
|
||||||
@@ -73,7 +73,7 @@ func TestNestedModuleListPagination(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp2.Body.Close()
|
defer func() { _ = resp2.Body.Close() }()
|
||||||
var page2 struct {
|
var page2 struct {
|
||||||
Items []map[string]any `json:"items"`
|
Items []map[string]any `json:"items"`
|
||||||
HasMore bool `json:"has_more"`
|
HasMore bool `json:"has_more"`
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ type Server struct {
|
|||||||
type Options struct {
|
type Options struct {
|
||||||
APIKeys string
|
APIKeys string
|
||||||
// DatabaseURL enables PostgreSQL-backed store (migrations applied on connect).
|
// DatabaseURL enables PostgreSQL-backed store (migrations applied on connect).
|
||||||
DatabaseURL string
|
DatabaseURL string
|
||||||
InsecureDev bool
|
InsecureDev bool
|
||||||
SeedDemo bool
|
SeedDemo bool
|
||||||
BundleSeedHex string
|
BundleSeedHex string
|
||||||
CORSAllowedOrigins string
|
CORSAllowedOrigins string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -76,7 +76,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusAccepted {
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -97,7 +97,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusAccepted {
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -118,7 +118,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -132,7 +132,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -162,7 +162,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
t.Fatalf("%s status %d: %s", path, resp.StatusCode, b)
|
t.Fatalf("%s status %d: %s", path, resp.StatusCode, b)
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -215,7 +215,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -255,7 +255,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusForbidden {
|
if resp.StatusCode != http.StatusForbidden {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d want 403: %s", resp.StatusCode, b)
|
t.Fatalf("status %d want 403: %s", resp.StatusCode, b)
|
||||||
@@ -269,7 +269,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusAccepted {
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -289,7 +289,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusAccepted {
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -314,7 +314,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
@@ -341,7 +341,7 @@ func waitJob(t *testing.T, client *http.Client, base, token, jobID string) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
var body struct {
|
var body struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
@@ -405,7 +405,7 @@ func TestVersionEndpoints(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func fetchLatestRevision(base, token, speaker string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
return "", fmt.Errorf("latest revision: %s: %s", resp.Status, strings.TrimSpace(string(b)))
|
return "", fmt.Errorf("latest revision: %s: %s", resp.Status, strings.TrimSpace(string(b)))
|
||||||
@@ -94,7 +94,7 @@ func fetchBundle(base, token, speaker, revision string) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
return nil, fmt.Errorf("bundle: %s: %s", resp.Status, strings.TrimSpace(string(b)))
|
return nil, fmt.Errorf("bundle: %s: %s", resp.Status, strings.TrimSpace(string(b)))
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func Run(args []string) int {
|
|||||||
|
|
||||||
// Usage prints CLI help to w.
|
// Usage prints CLI help to w.
|
||||||
func Usage(w interface{ Write([]byte) (int, error) }) {
|
func Usage(w interface{ Write([]byte) (int, error) }) {
|
||||||
fmt.Fprintf(w, `Usage:
|
_, _ = fmt.Fprintf(w, `Usage:
|
||||||
evobgp-node pull-bundle -base-url URL -token TOKEN -speaker-id ID [-revision-id ID] [-o path]
|
evobgp-node pull-bundle -base-url URL -token TOKEN -speaker-id ID [-revision-id ID] [-o path]
|
||||||
evobgp-node verify-bundle -f bundle.tar.gz (-pubkey-base64 B64 | -pubkey-hex HEX)
|
evobgp-node verify-bundle -f bundle.tar.gz (-pubkey-base64 B64 | -pubkey-hex HEX)
|
||||||
evobgp-node apply-bundle -f bundle.tar.gz -extract-dir DIR (-pubkey-base64 B64 | -pubkey-hex HEX)
|
evobgp-node apply-bundle -f bundle.tar.gz -extract-dir DIR (-pubkey-base64 B64 | -pubkey-hex HEX)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func applyCDNSourceHTTPResult(ctx context.Context, st store.Backend, hc *http.Cl
|
|||||||
return nil, fmt.Errorf("cdn url %s: 304 without cached prefixes", u)
|
return nil, fmt.Errorf("cdn url %s: 304 without cached prefixes", u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
_, _ = io.Copy(io.Discard, resp.Body)
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
|
|||||||
@@ -46,4 +46,3 @@ func TestBuildPreviewFragments_SamePrefixDifferentCommunity(t *testing.T) {
|
|||||||
t.Fatalf("expected deterministic static preview text, got first:\n%s\nsecond:\n%s", staticV4, staticV4Second)
|
t.Fatalf("expected deterministic static preview text, got first:\n%s\nsecond:\n%s", staticV4, staticV4Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ func resolveDomainWithDOHMessage(ctx context.Context, hc *http.Client, baseURL,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||||
return nil, fmt.Errorf("doh dns-message status %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
return nil, fmt.Errorf("doh dns-message status %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
||||||
@@ -378,7 +378,7 @@ func resolveDomainWithDOHJSON(ctx context.Context, hc *http.Client, baseURL, hos
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||||
return nil, fmt.Errorf("doh status %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
return nil, fmt.Errorf("doh status %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
||||||
@@ -725,15 +725,15 @@ func dedupeSortedPrefixLines(rows []store.PrefixRow) []prefixHashLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writePrefixLinesHash(h interface{ Write([]byte) (int, error) }, tenantID string, lines []prefixHashLine) {
|
func writePrefixLinesHash(h interface{ Write([]byte) (int, error) }, tenantID string, lines []prefixHashLine) {
|
||||||
h.Write([]byte(strings.TrimSpace(tenantID)))
|
_, _ = h.Write([]byte(strings.TrimSpace(tenantID)))
|
||||||
h.Write([]byte{0})
|
_, _ = h.Write([]byte{0})
|
||||||
for _, l := range lines {
|
for _, l := range lines {
|
||||||
h.Write([]byte(l.p))
|
_, _ = h.Write([]byte(l.p))
|
||||||
h.Write([]byte{1})
|
_, _ = h.Write([]byte{1})
|
||||||
h.Write([]byte(l.c))
|
_, _ = h.Write([]byte(l.c))
|
||||||
h.Write([]byte{1})
|
_, _ = h.Write([]byte{1})
|
||||||
h.Write([]byte(l.s))
|
_, _ = h.Write([]byte(l.s))
|
||||||
h.Write([]byte{0})
|
_, _ = h.Write([]byte{0})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func agentDebugNDJSON3214(hypothesisID, location, message string, data map[strin
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer func() { _ = f.Close() }()
|
||||||
var ms runtime.MemStats
|
var ms runtime.MemStats
|
||||||
runtime.ReadMemStats(&ms)
|
runtime.ReadMemStats(&ms)
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func postTenantRefresh(ctx context.Context, deps *Deps, moduleIDs []string, idem
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusAccepted {
|
if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusAccepted {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ type Memory struct {
|
|||||||
|
|
||||||
peers map[string]*BGPPeer
|
peers map[string]*BGPPeer
|
||||||
|
|
||||||
dohProfiles map[string]*DohProfile
|
dohProfiles map[string]*DohProfile
|
||||||
communities map[string]*Community
|
communities map[string]*Community
|
||||||
cdnSources map[string]*CDNSource
|
cdnSources map[string]*CDNSource
|
||||||
asEntries map[string]*ASEntry
|
asEntries map[string]*ASEntry
|
||||||
domainEnt map[string]*DomainEntry
|
domainEnt map[string]*DomainEntry
|
||||||
ipRanges map[string]*IPRangeEntry
|
ipRanges map[string]*IPRangeEntry
|
||||||
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
||||||
revPrefixes map[string][]PrefixRow
|
revPrefixes map[string][]PrefixRow
|
||||||
moduleSnapshots map[string]*moduleSnapshotRec
|
moduleSnapshots map[string]*moduleSnapshotRec
|
||||||
asnPrefixCache map[int64]*ASNPrefixCacheEntry
|
asnPrefixCache map[int64]*ASNPrefixCacheEntry
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ func TestEffectivePeerEnabledOnCreate(t *testing.T) {
|
|||||||
|
|
||||||
func TestParsePeerNeighbor(t *testing.T) {
|
func TestParsePeerNeighbor(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
in string
|
in string
|
||||||
want string
|
want string
|
||||||
wantOK bool
|
wantOK bool
|
||||||
}{
|
}{
|
||||||
{"192.168.0.2", "192.168.0.2", true},
|
{"192.168.0.2", "192.168.0.2", true},
|
||||||
{"192.168.0.2/32", "192.168.0.2", true},
|
{"192.168.0.2/32", "192.168.0.2", true},
|
||||||
|
|||||||
@@ -11,21 +11,54 @@ list_nums() {
|
|||||||
ls "$dir" 2>/dev/null | sed -n 's/^\([0-9]\{6\}\)_.*\.up\.sql$/\1/p' | sort -u
|
ls "$dir" 2>/dev/null | sed -n 's/^\([0-9]\{6\}\)_.*\.up\.sql$/\1/p' | sort -u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
migration_only_in() {
|
||||||
|
# Prints numbers present in $1 but not in $2 (space-separated lists).
|
||||||
|
haystack="$2 "
|
||||||
|
for n in $1; do
|
||||||
|
case "$haystack" in
|
||||||
|
*" $n "*) ;;
|
||||||
|
*) echo "$n" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
pg_nums="$(list_nums "$PG")"
|
pg_nums="$(list_nums "$PG")"
|
||||||
sql_nums="$(list_nums "$SQL")"
|
sql_nums="$(list_nums "$SQL")"
|
||||||
|
|
||||||
if [ "$pg_nums" != "$sql_nums" ]; then
|
if [ "$pg_nums" != "$sql_nums" ]; then
|
||||||
echo "check-migrations-pair: postgres and sqlite migration numbers differ" >&2
|
echo "check-migrations-pair: postgres and sqlite migration numbers differ" >&2
|
||||||
echo "postgres only:" >&2
|
only_pg="$(migration_only_in "$pg_nums" "$sql_nums")"
|
||||||
comm -23 <(printf '%s\n' "$pg_nums") <(printf '%s\n' "$sql_nums") >&2 || true
|
only_sql="$(migration_only_in "$sql_nums" "$pg_nums")"
|
||||||
echo "sqlite only:" >&2
|
if [ -n "$only_pg" ]; then
|
||||||
comm -13 <(printf '%s\n' "$pg_nums") <(printf '%s\n' "$sql_nums") >&2 || true
|
echo "postgres only:" >&2
|
||||||
|
for n in $only_pg; do
|
||||||
|
echo " $n" >&2
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ -n "$only_sql" ]; then
|
||||||
|
echo "sqlite only:" >&2
|
||||||
|
for n in $only_sql; do
|
||||||
|
echo " $n" >&2
|
||||||
|
done
|
||||||
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for n in $pg_nums; do
|
for n in $pg_nums; do
|
||||||
pg_up="$(ls "$PG"/${n}_*.up.sql 2>/dev/null | head -1)"
|
pg_up=""
|
||||||
sql_up="$(ls "$SQL"/${n}_*.up.sql 2>/dev/null | head -1)"
|
sql_up=""
|
||||||
|
for f in "$PG"/${n}_*.up.sql; do
|
||||||
|
if [ -f "$f" ]; then
|
||||||
|
pg_up="$f"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for f in "$SQL"/${n}_*.up.sql; do
|
||||||
|
if [ -f "$f" ]; then
|
||||||
|
sql_up="$f"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
if [ -z "$pg_up" ] || [ -z "$sql_up" ]; then
|
if [ -z "$pg_up" ] || [ -z "$sql_up" ]; then
|
||||||
echo "check-migrations-pair: missing .up.sql for $n" >&2
|
echo "check-migrations-pair: missing .up.sql for $n" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -50,11 +50,9 @@ export const settingsKnownSchema = z.object({
|
|||||||
bird_router_id: optionalIPv4('router id'),
|
bird_router_id: optionalIPv4('router id'),
|
||||||
bird_local_ipv4: optionalIPv4('local IPv4'),
|
bird_local_ipv4: optionalIPv4('local IPv4'),
|
||||||
bird_local_ipv6: optionalIPv6('local IPv6'),
|
bird_local_ipv6: optionalIPv6('local IPv6'),
|
||||||
bird_local_asn: z
|
bird_local_asn: z.string().refine((v) => v.trim() === '' || /^[1-9]\d*$/.test(v.trim()), {
|
||||||
.string()
|
message: 'ASN должен быть целым числом больше 0'
|
||||||
.refine((v) => v.trim() === '' || /^[1-9]\d*$/.test(v.trim()), {
|
}),
|
||||||
message: 'ASN должен быть целым числом больше 0'
|
|
||||||
}),
|
|
||||||
bird_bgp_source_ipv4: optionalIPv4('BGP source IPv4'),
|
bird_bgp_source_ipv4: optionalIPv4('BGP source IPv4'),
|
||||||
bird_bgp_source_ipv6: optionalIPv6('BGP source IPv6'),
|
bird_bgp_source_ipv6: optionalIPv6('BGP source IPv6'),
|
||||||
revision_retention_minutes: z.string().refine(
|
revision_retention_minutes: z.string().refine(
|
||||||
|
|||||||
+65
-22
@@ -159,12 +159,29 @@
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
function toErrorMessage(e: unknown): string {
|
||||||
|
return e instanceof Error ? e.message : String(e);
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
if (!initialLoading) refreshing = true;
|
if (!initialLoading) refreshing = true;
|
||||||
loadError = null;
|
loadError = null;
|
||||||
try {
|
try {
|
||||||
const [h, m, p, s, r, j] = await Promise.all([
|
try {
|
||||||
apiFetch('/v1/health'),
|
const h = await apiFetch('/v1/health');
|
||||||
|
healthy = h.ok;
|
||||||
|
if (!h.ok) {
|
||||||
|
loadError = `GET /v1/health: HTTP ${h.status}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
healthy = false;
|
||||||
|
loadError = toErrorMessage(e);
|
||||||
|
notifyApiError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [m, p, s, r, j] = await Promise.allSettled([
|
||||||
apiJSON<ModulesResponse>('/v1/modules?limit=200'),
|
apiJSON<ModulesResponse>('/v1/modules?limit=200'),
|
||||||
apiJSON<PeersResponse>('/v1/peers?limit=200'),
|
apiJSON<PeersResponse>('/v1/peers?limit=200'),
|
||||||
apiJSON<SpeakersResponse>('/v1/speakers?limit=200'),
|
apiJSON<SpeakersResponse>('/v1/speakers?limit=200'),
|
||||||
@@ -172,24 +189,38 @@
|
|||||||
apiJSON<JobsResponse>('/v1/jobs?limit=20')
|
apiJSON<JobsResponse>('/v1/jobs?limit=20')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
healthy = h.ok;
|
const firstReject = [m, p, s, r, j].find((x) => x.status === 'rejected');
|
||||||
moduleItems = m.items ?? [];
|
if (firstReject?.status === 'rejected') {
|
||||||
modulesHasMore = m.has_more;
|
loadError = toErrorMessage(firstReject.reason);
|
||||||
peerItems = p.items ?? [];
|
notifyApiError(firstReject.reason);
|
||||||
peersHasMore = p.has_more;
|
}
|
||||||
speakerItems = s.items ?? [];
|
|
||||||
speakersHasMore = s.has_more;
|
if (m.status === 'fulfilled') {
|
||||||
revisionItems = r.items ?? [];
|
moduleItems = m.value.items ?? [];
|
||||||
revisionsHasMore = r.has_more;
|
modulesHasMore = m.value.has_more;
|
||||||
jobItems = j.items ?? [];
|
}
|
||||||
recentJobs = jobItems.slice(0, 10);
|
if (p.status === 'fulfilled') {
|
||||||
recentRevisions = revisionItems.slice(0, 10);
|
peerItems = p.value.items ?? [];
|
||||||
runningJobs = jobItems.filter((i) => i.status === 'running' || i.status === 'queued').length;
|
peersHasMore = p.value.has_more;
|
||||||
lastUpdated = new Date();
|
}
|
||||||
} catch (e) {
|
if (s.status === 'fulfilled') {
|
||||||
healthy = false;
|
speakerItems = s.value.items ?? [];
|
||||||
loadError = e instanceof Error ? e.message : String(e);
|
speakersHasMore = s.value.has_more;
|
||||||
notifyApiError(e);
|
}
|
||||||
|
if (r.status === 'fulfilled') {
|
||||||
|
revisionItems = r.value.items ?? [];
|
||||||
|
revisionsHasMore = r.value.has_more;
|
||||||
|
}
|
||||||
|
if (j.status === 'fulfilled') {
|
||||||
|
jobItems = j.value.items ?? [];
|
||||||
|
recentJobs = jobItems.slice(0, 10);
|
||||||
|
recentRevisions = revisionItems.slice(0, 10);
|
||||||
|
runningJobs = jobItems.filter(
|
||||||
|
(i) => i.status === 'running' || i.status === 'queued'
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loadError) lastUpdated = new Date();
|
||||||
} finally {
|
} finally {
|
||||||
initialLoading = false;
|
initialLoading = false;
|
||||||
refreshing = false;
|
refreshing = false;
|
||||||
@@ -235,18 +266,30 @@
|
|||||||
<AlertTitle>Проверка API…</AlertTitle>
|
<AlertTitle>Проверка API…</AlertTitle>
|
||||||
<AlertDescription>Запрос к <code class="text-xs">/v1/health</code></AlertDescription>
|
<AlertDescription>Запрос к <code class="text-xs">/v1/health</code></AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
{:else if healthy}
|
{:else if healthy && !loadError}
|
||||||
<Alert class="border-success/30 bg-success/5">
|
<Alert class="border-success/30 bg-success/5">
|
||||||
<CheckCircle class="text-success" />
|
<CheckCircle class="text-success" />
|
||||||
<AlertTitle>API работает</AlertTitle>
|
<AlertTitle>API работает</AlertTitle>
|
||||||
<AlertDescription>Сервер отвечает на запросы health-check.</AlertDescription>
|
<AlertDescription>Сервер отвечает на запросы health-check.</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
{:else if healthy && loadError}
|
||||||
|
<Alert class="border-warning/30 bg-warning/5">
|
||||||
|
<Info class="text-warning" />
|
||||||
|
<AlertTitle>API доступен, данные не загружены</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{loadError}. Для локального демо укажите Bearer-токен
|
||||||
|
<code class="text-xs">dev</code> в
|
||||||
|
<Button variant="link" class="h-auto p-0" href={resolve('/settings')}>Настройках</Button>
|
||||||
|
(нужен <code class="text-xs">EVOBGP_DEV_INSECURE=1</code> на API).
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
{:else}
|
{:else}
|
||||||
<Alert variant="destructive" class="border-destructive/30 bg-destructive/5">
|
<Alert variant="destructive" class="border-destructive/30 bg-destructive/5">
|
||||||
<XCircle class="text-destructive" />
|
<XCircle class="text-destructive" />
|
||||||
<AlertTitle>API недоступен</AlertTitle>
|
<AlertTitle>API недоступен</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Не удалось получить ответ от сервера. Проверьте подключение и статус API.
|
{loadError ??
|
||||||
|
'Не удалось получить ответ от сервера. Проверьте, что API запущен (порт 8080), в dev — `npm run dev` с прокси Vite, в Docker — контейнер evobgp-api / evobgp-all и nginx в evobgp-web.'}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in new issue
Block a user