Files
EvoBGP/internal/store/speaker_meta.go
T
Denozordec 9639a03bfe
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 39s
CI / go (push) Successful in 58s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 4m18s
feat(store): add test for clearing dispatch error on successful merge
- Introduced a new test case in speaker_meta_test.go to verify that the LastDispatchError is cleared when a successful dispatch status is merged.
- Updated MergeSpeakerMetaJSON function in speaker_meta.go to clear LastDispatchError if LastDispatchStatus is "ok".
- Enhanced speakerDispatchError function in network-metrics.ts to handle stale dispatch errors after successful agent sync.
2026-05-21 18:10:51 +07:00

153 lines
4.2 KiB
Go

package store
import (
"encoding/json"
"net"
"net/url"
"strings"
)
// SpeakerMeta holds well-known keys from bgp_speaker.meta_json.
type SpeakerMeta struct {
AgentDomain string `json:"agent_domain,omitempty"`
AgentSecret string `json:"agent_secret,omitempty"`
AgentPort int `json:"agent_port,omitempty"`
NodeIPv4 string `json:"node_ipv4,omitempty"`
BirdBgpSourceIPv4 string `json:"bird_bgp_source_ipv4,omitempty"`
BirdBgpSourceIPv6 string `json:"bird_bgp_source_ipv6,omitempty"`
NodeEnrolledAt string `json:"node_enrolled_at,omitempty"`
LastDispatchAt string `json:"last_dispatch_at,omitempty"`
LastDispatchError string `json:"last_dispatch_error,omitempty"`
LastDispatchStatus string `json:"last_dispatch_status,omitempty"`
SyncStatus string `json:"sync_status,omitempty"`
}
// ParseSpeakerMeta decodes meta_json object; unknown keys are ignored.
func ParseSpeakerMeta(metaJSON string) SpeakerMeta {
raw := strings.TrimSpace(metaJSON)
if raw == "" || raw == "{}" {
return SpeakerMeta{}
}
var m SpeakerMeta
_ = json.Unmarshal([]byte(raw), &m)
if m.AgentPort == 0 {
m.AgentPort = 8443
}
return m
}
// SpeakerMetaJSON marshals SpeakerMeta to a JSON object string.
func SpeakerMetaJSON(m SpeakerMeta) string {
b, err := json.Marshal(m)
if err != nil {
return "{}"
}
return string(b)
}
// MergeSpeakerMetaJSON merges patch into existing meta_json string.
func MergeSpeakerMetaJSON(existing string, patch SpeakerMeta) string {
cur := ParseSpeakerMeta(existing)
if patch.AgentDomain != "" {
cur.AgentDomain = patch.AgentDomain
}
if patch.AgentSecret != "" {
cur.AgentSecret = patch.AgentSecret
}
if patch.AgentPort != 0 {
cur.AgentPort = patch.AgentPort
}
if patch.NodeIPv4 != "" {
cur.NodeIPv4 = patch.NodeIPv4
}
if patch.BirdBgpSourceIPv4 != "" {
cur.BirdBgpSourceIPv4 = patch.BirdBgpSourceIPv4
}
if patch.BirdBgpSourceIPv6 != "" {
cur.BirdBgpSourceIPv6 = patch.BirdBgpSourceIPv6
}
if patch.NodeEnrolledAt != "" {
cur.NodeEnrolledAt = patch.NodeEnrolledAt
}
if patch.LastDispatchAt != "" {
cur.LastDispatchAt = patch.LastDispatchAt
}
if patch.LastDispatchStatus == "ok" {
cur.LastDispatchError = ""
} else if patch.LastDispatchError != "" {
cur.LastDispatchError = patch.LastDispatchError
}
if patch.LastDispatchStatus != "" {
cur.LastDispatchStatus = patch.LastDispatchStatus
}
if patch.SyncStatus != "" {
cur.SyncStatus = patch.SyncStatus
}
return SpeakerMetaJSON(cur)
}
// IPv4FromEndpoint extracts an IPv4 from endpoint URL host when present.
func IPv4FromEndpoint(endpoint string) string {
ep := strings.TrimSpace(endpoint)
if ep == "" {
return ""
}
if !strings.Contains(ep, "://") {
ep = "https://" + ep
}
u, err := url.Parse(ep)
if err != nil {
return ""
}
host := strings.TrimSpace(u.Hostname())
if host == "" {
return ""
}
if ip := net.ParseIP(host); ip != nil && ip.To4() != nil {
return ip.String()
}
return ""
}
// ValidIPv4 reports whether s is a dotted-quad IPv4 address.
func ValidIPv4(s string) bool {
ip := net.ParseIP(strings.TrimSpace(s))
return ip != nil && ip.To4() != nil
}
// AgentSyncURL returns HTTPS sync URL for a speaker with agent_domain configured.
func AgentSyncURL(meta SpeakerMeta) string {
domain := strings.TrimSpace(meta.AgentDomain)
if domain == "" {
return ""
}
return "https://" + strings.TrimSuffix(domain, "/") + "/v1/agent/sync"
}
// AgentHealthURL returns HTTPS health URL for agent_domain.
func AgentHealthURL(meta SpeakerMeta) string {
return agentHTTPSURL(meta, "/v1/agent/health")
}
// AgentBirdProtocolsURL returns HTTPS bird protocols URL for agent_domain.
func AgentBirdProtocolsURL(meta SpeakerMeta) string {
return agentHTTPSURL(meta, "/v1/agent/bird/protocols")
}
func agentHTTPSURL(meta SpeakerMeta, path string) string {
domain := strings.TrimSpace(meta.AgentDomain)
if domain == "" {
return ""
}
return "https://" + strings.TrimSuffix(domain, "/") + path
}
// SpeakerNeedsRemoteDispatch reports whether deploy_apply should wake this speaker via agent HTTP.
func SpeakerNeedsRemoteDispatch(role string, meta SpeakerMeta) bool {
r := strings.ToLower(strings.TrimSpace(role))
if r == "master" {
return false
}
return strings.TrimSpace(meta.AgentDomain) != "" && strings.TrimSpace(meta.AgentSecret) != ""
}