CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 28s
CI / web (push) Successful in 52s
CI / go (push) Successful in 2m22s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 4m36s
Added functionality for peer discovery, including new API endpoints for listing, approving, and rejecting discovered peers. Updated the network queries and settings to support peer discovery configurations. Enhanced the UI to display discovered peers and integrated related settings in the tenant settings component. Updated OpenAPI documentation to reflect the new endpoints and parameters. This improves the network management capabilities by allowing dynamic peer discovery and management.
374 lines
17 KiB
Go
374 lines
17 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"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)
|
|
|
|
// ListTenantIDs returns distinct tenant identifiers (for background workers).
|
|
ListTenantIDs() ([]string, error)
|
|
|
|
// ListModules returns all modules for a tenant (control plane may paginate in httpapi).
|
|
ListModules(tenantID string) []*Module
|
|
// ListModulesPage returns one page of modules (limit capped by caller).
|
|
ListModulesPage(tenantID, cursor string, limit int) ([]*Module, string, bool)
|
|
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
|
|
// UpdateASEntryResolveMeta записывает имя AS, число объявленных префиксов и время успешного резолва (pipeline).
|
|
UpdateASEntryResolveMeta(tenantID, moduleID, entryID string, asnName string, prefixCount int64, resolvedAt time.Time) error
|
|
UpdateASEntryResolveMetaBatch(tenantID, moduleID string, updates []ASEntryResolveMetaUpdate, resolvedAt time.Time) 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
|
|
// ListCommunityPrefixes returns unique prefixes tagged with community from latest revision per module.
|
|
ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) (prefixes []PrefixRow, nextCursor string, hasMore bool, err error)
|
|
|
|
// ListPeers returns all BGP peers for a tenant (control plane may paginate in httpapi).
|
|
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
|
|
|
|
// Peer discovery (dynamic BGP quarantine → approve/reject).
|
|
ListPeerDiscoveries(tenantID, status string) ([]*BGPPeerDiscovery, error)
|
|
GetPeerDiscovery(tenantID, id string) (*BGPPeerDiscovery, error)
|
|
UpsertPeerDiscovery(tenantID string, in *PeerDiscoveryUpsert) (*BGPPeerDiscovery, error)
|
|
ApprovePeerDiscovery(tenantID, id string, in *PeerDiscoveryApproveInput) (*BGPPeer, *BGPPeerDiscovery, error)
|
|
RejectPeerDiscovery(tenantID, id string) (*BGPPeerDiscovery, 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)
|
|
DeleteSpeaker(tenantID, id string) error
|
|
|
|
GetRevision(tenantID, revisionID string) (*Revision, error)
|
|
// GetRevisionSummary returns revision metadata without preview_fragments payloads.
|
|
GetRevisionSummary(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)
|
|
// CreateRenderRevision inserts a new config_revision (revID must be unique) with materialized prefixes and preview fragments.
|
|
CreateRenderRevision(revisionID, tenantID, moduleID string, parentRevisionID *string, contentHash string, previewFragments map[string]string, prefixes []PrefixRow) error
|
|
RevisionDiff(tenantID, aID, bID string) (map[string]any, error)
|
|
EstimateRevisionPrune(tenantID string, cutoff time.Time, retentionMinutes int) (RevisionPruneEstimate, error)
|
|
PruneRevisionsWithStats(tenantID string, cutoff time.Time) (RevisionPruneResult, error)
|
|
PruneRevisionsBefore(tenantID string, cutoff time.Time) (deleted int, err 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
|
|
|
|
ListAPIKeys(tenantID string) ([]*APIKey, error)
|
|
GetAPIKey(tenantID, id string) (*APIKey, error)
|
|
CreateAPIKey(tenantID string, in *APIKeyCreate) (*APIKeyWithSecret, error)
|
|
UpdateAPIKey(tenantID, id string, patch *APIKeyPatch) (*APIKey, error)
|
|
RevokeAPIKey(tenantID, id string) error
|
|
RotateAPIKey(tenantID, id string) (*APIKeyWithSecret, error)
|
|
ListActiveAPIKeyHashes() ([]APIKeyAuthRow, error)
|
|
TouchAPIKeyLastUsed(id string) error
|
|
|
|
// Module prefix snapshots cache last successful collect per module (pipeline ingest/render).
|
|
GetModulePrefixSnapshot(tenantID, moduleID string) (*ModulePrefixSnapshot, bool, error)
|
|
SetModulePrefixSnapshot(tenantID, moduleID, inputHash string, prefixes []PrefixRow) error
|
|
DeleteModulePrefixSnapshot(tenantID, moduleID string) error
|
|
|
|
// ASNPrefixCache stores RIPEstat announced-prefixes per ASN (global TTL cache).
|
|
GetASNPrefixCache(asn int64) (*ASNPrefixCacheEntry, bool, error)
|
|
SetASNPrefixCache(asn int64, holder string, prefixes []string) error
|
|
|
|
// Ping verifies backend connectivity (no-op for in-memory).
|
|
Ping(ctx context.Context) error
|
|
|
|
// RunPeriodicMaintenance prunes stale DB rows (no-op for in-memory).
|
|
RunPeriodicMaintenance(ctx context.Context)
|
|
|
|
// Maintenance policies (instance-scoped PostgreSQL maintenance configuration).
|
|
ListMaintenancePolicies(cursor string, limit int) ([]*MaintenancePolicy, string, bool, error)
|
|
GetMaintenancePolicy(id string) (*MaintenancePolicy, error)
|
|
CreateMaintenancePolicy(in *MaintenancePolicy) (*MaintenancePolicy, error)
|
|
UpdateMaintenancePolicy(id string, patch *MaintenancePolicyPatch) (*MaintenancePolicy, error)
|
|
DeleteMaintenancePolicy(id string) error
|
|
TouchMaintenancePolicyRun(id, status, errMsg string) error
|
|
AppendMaintenancePolicyConfigAudit(actor, policyID, action string, before, after map[string]any) error
|
|
ListMaintenancePolicyConfigAudit(cursor string, limit int) ([]*MaintenancePolicyConfigAudit, string, bool, error)
|
|
|
|
// Runtime log cleanup audit (filesystem ops logged per tenant).
|
|
AppendRuntimeLogCleanupAudit(tenantID, actor, filename, action string, sizeBefore int64, sizeAfter *int64, detail map[string]any) (string, error)
|
|
ListRuntimeLogCleanupAudit(tenantID, cursor string, limit int) ([]*RuntimeLogCleanupAudit, string, bool, error)
|
|
|
|
// CRUD audit log (tenant-scoped; optional portal ingest push from httpapi).
|
|
AppendAudit(in AuditAppendInput) (*AuditEntry, error)
|
|
ListAudit(tenantID, cursor string, limit int, filter AuditListFilter) ([]*AuditEntry, string, bool, error)
|
|
MarkAuditPortalPushed(id string) error
|
|
|
|
// Firewall blocklist clients and policy rules.
|
|
ListFirewallClients(tenantID string) ([]*FirewallClient, error)
|
|
GetFirewallClient(tenantID, id string) (*FirewallClient, error)
|
|
CreateFirewallClient(tenantID string, in *FirewallClientCreate) (*FirewallClient, error)
|
|
UpdateFirewallClient(tenantID, id string, patch *FirewallClientPatch) (*FirewallClient, error)
|
|
ApproveFirewallClient(tenantID, id, approverAPIKeyID string) (*FirewallClient, error)
|
|
RevokeFirewallClient(tenantID, id string) error
|
|
DeleteFirewallClient(tenantID, id string) error
|
|
LookupFirewallClientByTokenHash(hash []byte) (*FirewallClient, error)
|
|
TouchFirewallClientLastSeen(id, source, clientIP, clientVersion string) error
|
|
TouchFirewallClientLastApply(id, source, status, errMsg string, prefixCount, ipCount int, packetsDropped, packetsAccepted int64) error
|
|
ListActiveFirewallClientHashes() ([]FirewallClientAuthRow, error)
|
|
ListApprovedFirewallClientsForReplication(tenantID string) ([]FirewallClientReplicationRow, error)
|
|
|
|
ListFirewallRules(tenantID string, clientID *string) ([]*FirewallRule, error)
|
|
ListAllFirewallRulesForClient(tenantID, clientID string) ([]*FirewallRule, error)
|
|
ListAllFirewallRulesForReplication(tenantID string) ([]*FirewallRule, error)
|
|
CreateFirewallRule(tenantID string, clientID *string, in *FirewallRuleCreate) (*FirewallRule, error)
|
|
GetFirewallRule(tenantID, ruleID string) (*FirewallRule, error)
|
|
UpdateFirewallRule(tenantID, ruleID string, patch *FirewallRulePatch) (*FirewallRule, error)
|
|
DeleteFirewallRule(tenantID, ruleID string) error
|
|
ReorderFirewallRules(tenantID string, clientID *string, orderedIDs []string) error
|
|
}
|
|
|
|
// ASNPrefixCacheEntry is a cached RIPEstat response for one ASN.
|
|
type ASNPrefixCacheEntry struct {
|
|
ASN int64
|
|
Holder string
|
|
Prefixes []string
|
|
FetchedAt time.Time
|
|
}
|
|
|
|
// ModulePrefixSnapshot is the cached materialization for one module between refreshes.
|
|
type ModulePrefixSnapshot struct {
|
|
InputHash string
|
|
CollectedAt time.Time
|
|
Prefixes []PrefixRow
|
|
}
|
|
|
|
// ModulePatch is a partial update for module.
|
|
type ModulePatch struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
Priority *int `json:"priority,omitempty"`
|
|
RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"`
|
|
CronExpr *string `json:"cron_expr,omitempty"`
|
|
DefaultCommunityID *string `json:"default_community_id,omitempty"`
|
|
DohProfileID *string `json:"doh_profile_id,omitempty"`
|
|
DohProfileIDs *[]string `json:"doh_profile_ids,omitempty"`
|
|
DohResolverPolicy *string `json:"doh_resolver_policy,omitempty"`
|
|
LastRefreshedAt *time.Time `json:"-"`
|
|
}
|
|
|
|
// CDNSource is a row under a CDN module.
|
|
type CDNSource struct {
|
|
ID string `json:"id,omitempty"`
|
|
ModuleID string `json:"module_id,omitempty"`
|
|
SourceKind string `json:"source_kind"`
|
|
URL string `json:"url"`
|
|
PrefixPath string `json:"prefix_path,omitempty"`
|
|
Etag string `json:"etag"`
|
|
RefreshIntervalSec *int `json:"refresh_interval_sec"`
|
|
CommunityID *string `json:"community_id"`
|
|
LastRefreshedAt *time.Time `json:"-"`
|
|
}
|
|
|
|
type CDNSourcePatch struct {
|
|
SourceKind *string `json:"source_kind,omitempty"`
|
|
URL *string `json:"url,omitempty"`
|
|
PrefixPath *string `json:"prefix_path,omitempty"`
|
|
Etag *string `json:"etag,omitempty"`
|
|
RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"`
|
|
CommunityID *string `json:"community_id,omitempty"`
|
|
LastRefreshedAt *time.Time `json:"-"`
|
|
}
|
|
|
|
type ASEntry struct {
|
|
ID string `json:"id,omitempty"`
|
|
ModuleID string `json:"module_id,omitempty"`
|
|
ASN int64 `json:"asn"`
|
|
CommunityID *string `json:"community_id"`
|
|
ASNName string `json:"asn_name,omitempty"`
|
|
PrefixCount *int64 `json:"prefix_count,omitempty"`
|
|
ASNResolvedAt *time.Time `json:"asn_resolved_at,omitempty"`
|
|
}
|
|
|
|
type ASEntryPatch struct {
|
|
ASN *int64 `json:"asn,omitempty"`
|
|
CommunityID *string `json:"community_id,omitempty"`
|
|
}
|
|
|
|
// ValidASN reports whether n is a usable BGP ASN (1..4294967295).
|
|
func ValidASN(n int64) bool {
|
|
return n >= 1 && n <= 4294967295
|
|
}
|
|
|
|
type DomainEntry struct {
|
|
ID string `json:"id,omitempty"`
|
|
ModuleID string `json:"module_id,omitempty"`
|
|
FQDN string `json:"fqdn"`
|
|
CommunityID *string `json:"community_id"`
|
|
}
|
|
|
|
type DomainEntryPatch struct {
|
|
FQDN *string `json:"fqdn,omitempty"`
|
|
CommunityID *string `json:"community_id,omitempty"`
|
|
}
|
|
|
|
type IPRangeEntry struct {
|
|
ID string `json:"id,omitempty"`
|
|
ModuleID string `json:"module_id,omitempty"`
|
|
Prefix string `json:"prefix"`
|
|
CommunityID *string `json:"community_id"`
|
|
}
|
|
|
|
type IPRangePatch struct {
|
|
Prefix *string `json:"prefix,omitempty"`
|
|
CommunityID *string `json:"community_id,omitempty"`
|
|
}
|
|
|
|
type DohProfile struct {
|
|
ID string `json:"id,omitempty"`
|
|
TenantID string `json:"tenant_id,omitempty"`
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
TimeoutMs *int `json:"timeout_ms"`
|
|
SecretRef *string `json:"vault_secret_ref"`
|
|
}
|
|
|
|
type DohProfilePatch struct {
|
|
Name *string `json:"name,omitempty"`
|
|
URL *string `json:"url,omitempty"`
|
|
TimeoutMs *int `json:"timeout_ms,omitempty"`
|
|
SecretRef *string `json:"vault_secret_ref,omitempty"`
|
|
}
|
|
|
|
type Community struct {
|
|
ID string `json:"id,omitempty"`
|
|
TenantID string `json:"tenant_id,omitempty"`
|
|
Community string `json:"community"`
|
|
Title string `json:"title"`
|
|
ValueJSON string `json:"value_json"`
|
|
}
|
|
|
|
type CommunityPatch struct {
|
|
Community *string `json:"community,omitempty"`
|
|
Title *string `json:"title,omitempty"`
|
|
ValueJSON *string `json:"value_json,omitempty"`
|
|
}
|
|
|
|
// APIKey is tenant-scoped API key metadata (secret never stored in plaintext).
|
|
type APIKey struct {
|
|
ID string `json:"id"`
|
|
TenantID string `json:"tenant_id,omitempty"`
|
|
Name string `json:"name"`
|
|
Role string `json:"role"`
|
|
Prefix string `json:"prefix"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
RevokedAt *time.Time `json:"revoked_at,omitempty"`
|
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
|
}
|
|
|
|
// APIKeyCreate is input for issuing a new key.
|
|
type APIKeyCreate struct {
|
|
Name string `json:"name"`
|
|
Role string `json:"role"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// APIKeyPatch is a partial update (role change affects auth after resolver reload).
|
|
type APIKeyPatch struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Role *string `json:"role,omitempty"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
ClearExpiresAt bool `json:"-"`
|
|
}
|
|
|
|
// APIKeyWithSecret is returned only on create/rotate.
|
|
type APIKeyWithSecret struct {
|
|
APIKey
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// APIKeyAuthRow is used to build the in-process auth index.
|
|
type APIKeyAuthRow struct {
|
|
ID string
|
|
TenantID string
|
|
Role string
|
|
TokenHash []byte
|
|
}
|
|
|
|
// ValidAPIKeyRole reports whether role is allowed for API keys.
|
|
func ValidAPIKeyRole(role string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(role)) {
|
|
case "viewer", "editor", "operator", "node":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type PeerPatch struct {
|
|
Neighbor *string `json:"neighbor,omitempty"`
|
|
RemoteASN *int64 `json:"remote_asn,omitempty"`
|
|
SpeakerID *string `json:"bgp_speaker_id,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
Name *string `json:"name,omitempty"`
|
|
SessionState *string `json:"session_state,omitempty"`
|
|
PoliciesJSON *string `json:"policies_json,omitempty"`
|
|
}
|
|
|
|
type SpeakerPatch struct {
|
|
Role *string `json:"role,omitempty"`
|
|
Endpoint *string `json:"endpoint,omitempty"`
|
|
MetaJSON *string `json:"meta_json,omitempty"`
|
|
}
|
|
|
|
// PrefixRow is one materialized prefix for GET /revisions/.../prefixes.
|
|
type PrefixRow struct {
|
|
Prefix string `json:"prefix"`
|
|
CommunityID *string `json:"community_id,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
}
|