Updated the GetCommunity function to accept both UUIDs and community titles for improved flexibility in community retrieval. Added error handling for invalid ID formats and adjusted related functions to ensure consistent behavior across memory and PostgreSQL storage. This change enhances the API's usability by allowing more intuitive community lookups.
953 lines
22 KiB
Go
953 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 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
|
|
}
|
|
}
|
|
ApplyModuleDohPatch(mod, patch)
|
|
if patch.LastRefreshedAt != nil {
|
|
t := patch.LastRefreshedAt.UTC()
|
|
mod.LastRefreshedAt = &t
|
|
}
|
|
return 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
|
|
}
|