quality / commitlint (push) Skipped
quality / changes (push) Successful in 8s
quality / openapi (push) Skipped
quality / web (push) Skipped
quality / docker-check (push) Skipped
quality / go (push) Successful in 1m14s
quality / bird2 (push) Successful in 18s
CD / quality (push) Successful in 1m47s
CD / publish (push) Successful in 2m53s
- Added environment variable `EVOBGP_JOB_MAX_CONCURRENT` to control job concurrency in tests. - Modified worker test to ensure proper synchronization of job processing by holding workers until both jobs are enqueued. - Updated memory store methods to return cloned module instances, preventing unintended mutations of original modules during operations.
955 lines
22 KiB
Go
955 lines
22 KiB
Go
package store
|
|
|
|
import (
|
|
"sort"
|
|
"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,
|
|
DohProfileIDs: append([]string(nil), in.DohProfileIDs...),
|
|
DohResolverPolicy: in.DohResolverPolicy,
|
|
LastRefreshedAt: in.LastRefreshedAt,
|
|
CreatedByUserID: strings.TrimSpace(in.CreatedByUserID),
|
|
}
|
|
NormalizeModuleDoh(mod)
|
|
m.modules[id] = mod
|
|
return cloneModule(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.DohProfileIDs != nil || patch.DohProfileID != nil || patch.DohResolverPolicy != nil {
|
|
ApplyModuleDohPatch(mod, patch)
|
|
}
|
|
if patch.LastRefreshedAt != nil {
|
|
t := patch.LastRefreshedAt.UTC()
|
|
mod.LastRefreshedAt = &t
|
|
}
|
|
return cloneModule(mod), nil
|
|
}
|
|
|
|
func (m *Memory) SoftDeleteModule(tenantID, moduleID string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.moduleSnapshots != nil {
|
|
delete(m.moduleSnapshots, moduleSnapshotKey(tenantID, moduleID))
|
|
}
|
|
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),
|
|
PrefixPath: strings.TrimSpace(in.PrefixPath),
|
|
Etag: in.Etag,
|
|
RefreshIntervalSec: in.RefreshIntervalSec,
|
|
CommunityID: in.CommunityID,
|
|
LastRefreshedAt: in.LastRefreshedAt,
|
|
}
|
|
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.PrefixPath != nil {
|
|
s.PrefixPath = strings.TrimSpace(*patch.PrefixPath)
|
|
}
|
|
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
|
|
}
|
|
}
|
|
if patch.LastRefreshedAt != nil {
|
|
t := patch.LastRefreshedAt.UTC()
|
|
s.LastRefreshedAt = &t
|
|
}
|
|
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 || !ValidASN(in.ASN) {
|
|
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, 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
|
|
}
|
|
prevASN := e.ASN
|
|
if patch.ASN != nil {
|
|
e.ASN = *patch.ASN
|
|
}
|
|
if patch.CommunityID != nil {
|
|
v := strings.TrimSpace(*patch.CommunityID)
|
|
if v == "" {
|
|
e.CommunityID = nil
|
|
} else {
|
|
e.CommunityID = &v
|
|
}
|
|
}
|
|
if !ValidASN(e.ASN) {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
if patch.ASN != nil && e.ASN != prevASN {
|
|
e.ASNName = ""
|
|
e.PrefixCount = nil
|
|
e.ASNResolvedAt = nil
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func (m *Memory) UpdateASEntryResolveMeta(tenantID, moduleID, entryID string, asnName string, prefixCount int64, resolvedAt time.Time) 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
|
|
}
|
|
e.ASNName = strings.TrimSpace(asnName)
|
|
pc := prefixCount
|
|
e.PrefixCount = &pc
|
|
t := resolvedAt.UTC()
|
|
e.ASNResolvedAt = &t
|
|
return nil
|
|
}
|
|
|
|
func (m *Memory) UpdateASEntryResolveMetaBatch(tenantID, moduleID string, updates []ASEntryResolveMetaUpdate, resolvedAt time.Time) error {
|
|
for _, u := range updates {
|
|
if err := m.UpdateASEntryResolveMeta(tenantID, moduleID, u.EntryID, u.ASNName, u.PrefixCount, resolvedAt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return 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 == nil || mod.DeletedAt != nil {
|
|
continue
|
|
}
|
|
for _, pid := range mod.EffectiveDohProfileIDs() {
|
|
if pid == 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, idOrKey string) (*Community, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
key := strings.TrimSpace(idOrKey)
|
|
if key == "" {
|
|
return nil, ErrNotFound
|
|
}
|
|
if c, ok := m.communities[key]; ok && c.TenantID == tenantID {
|
|
return c, nil
|
|
}
|
|
for _, c := range m.communities {
|
|
if c.TenantID != tenantID {
|
|
continue
|
|
}
|
|
if c.Community == key || c.Title == key {
|
|
return c, nil
|
|
}
|
|
if strings.TrimSpace(c.Title) != "" && c.Community+" · "+c.Title == key {
|
|
return c, nil
|
|
}
|
|
}
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
func (m *Memory) ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) ([]PrefixRow, string, bool, error) {
|
|
commRow, err := m.GetCommunity(tenantID, communityID)
|
|
if err != nil {
|
|
return nil, "", false, err
|
|
}
|
|
resolvedID := commRow.ID
|
|
if limit <= 0 {
|
|
limit = 500
|
|
}
|
|
if limit > 5000 {
|
|
limit = 5000
|
|
}
|
|
off := 0
|
|
if cursor != "" {
|
|
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
|
|
off = n
|
|
}
|
|
}
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
latestByModule := map[string]*Revision{}
|
|
for _, rev := range m.revisions {
|
|
if rev.TenantID != tenantID || strings.TrimSpace(rev.ModuleID) == "" {
|
|
continue
|
|
}
|
|
cur := latestByModule[rev.ModuleID]
|
|
if cur == nil || rev.CreatedAt.After(cur.CreatedAt) {
|
|
latestByModule[rev.ModuleID] = rev
|
|
}
|
|
}
|
|
seen := map[string]struct{}{}
|
|
var all []PrefixRow
|
|
for _, rev := range latestByModule {
|
|
for _, pr := range m.revPrefixes[rev.ID] {
|
|
if pr.CommunityID == nil || *pr.CommunityID != resolvedID {
|
|
continue
|
|
}
|
|
pfx := strings.TrimSpace(pr.Prefix)
|
|
if pfx == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[pfx]; ok {
|
|
continue
|
|
}
|
|
seen[pfx] = struct{}{}
|
|
all = append(all, PrefixRow{Prefix: pfx, CommunityID: &resolvedID, Source: pr.Source})
|
|
}
|
|
}
|
|
sort.Slice(all, func(i, j int) bool { return all[i].Prefix < all[j].Prefix })
|
|
if off > len(all) {
|
|
return nil, "", false, nil
|
|
}
|
|
end := off + limit
|
|
more := false
|
|
next := ""
|
|
if end < len(all) {
|
|
more = true
|
|
next = strconv.Itoa(end)
|
|
all = all[off:end]
|
|
} else {
|
|
all = all[off:]
|
|
}
|
|
if len(all) == 0 {
|
|
return nil, "", false, nil
|
|
}
|
|
return all, next, more, nil
|
|
}
|
|
|
|
func (m *Memory) CreateCommunity(tenantID string, in *Community) (*Community, error) {
|
|
if in == nil || strings.TrimSpace(in.Community) == "" {
|
|
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, Community: strings.TrimSpace(in.Community), Title: strings.TrimSpace(in.Title), 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.Community != nil {
|
|
c.Community = strings.TrimSpace(*patch.Community)
|
|
}
|
|
if patch.Title != nil {
|
|
c.Title = strings.TrimSpace(*patch.Title)
|
|
}
|
|
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 || in.RemoteASN == 0 {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
neighbor, ok := NormalizePeerNeighborString(in.Neighbor)
|
|
if !ok {
|
|
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: neighbor, RemoteASN: in.RemoteASN,
|
|
Enabled: EffectivePeerEnabledOnCreate(in.Enabled, in.SessionState),
|
|
SessionState: in.SessionState, PoliciesJSON: in.PoliciesJSON,
|
|
CreatedByUserID: strings.TrimSpace(in.CreatedByUserID),
|
|
}
|
|
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 {
|
|
n, ok := NormalizePeerNeighborString(*patch.Neighbor)
|
|
if !ok {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
p.Neighbor = n
|
|
}
|
|
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) DeleteSpeaker(tenantID, id string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
sp, ok := m.speakers[id]
|
|
if !ok || sp.TenantID != tenantID {
|
|
return ErrNotFound
|
|
}
|
|
delete(m.speakers, id)
|
|
delete(m.publishedRevision, id)
|
|
return nil
|
|
}
|
|
|
|
func (m *Memory) ListRevisionPrefixes(tenantID, revisionID string, cursor string, limit int) ([]PrefixRow, string, bool) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
afterID, off, useOffset := ParsePrefixPageCursor(cursor)
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if _, err := m.getRevisionLocked(tenantID, revisionID); err != nil {
|
|
return nil, "", false
|
|
}
|
|
allRows := m.revPrefixes[revisionID]
|
|
start := 0
|
|
if useOffset {
|
|
start = off
|
|
} else if afterID != nil {
|
|
start = int(*afterID) + 1
|
|
}
|
|
if start > len(allRows) {
|
|
return nil, "", false
|
|
}
|
|
end := start + limit
|
|
next := ""
|
|
more := false
|
|
if end > len(allRows) {
|
|
end = len(allRows)
|
|
} else {
|
|
more = true
|
|
next = FormatPrefixPageCursor(int64(end - 1))
|
|
}
|
|
if start >= end {
|
|
return nil, "", false
|
|
}
|
|
out := make([]PrefixRow, end-start)
|
|
copy(out, allRows[start:end])
|
|
return out, 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
|
|
}
|