Files
EvoBGP/internal/store/memory_crud.go
T

799 lines
18 KiB
Go

package store
import (
"fmt"
"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,
DohProfileID: in.DohProfileID,
}
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
}
}
if patch.DohProfileID != nil {
v := strings.TrimSpace(*patch.DohProfileID)
if v == "" {
mod.DohProfileID = nil
} else {
mod.DohProfileID = &v
}
}
return mod, nil
}
func (m *Memory) SoftDeleteModule(tenantID, moduleID string) error {
m.mu.Lock()
defer m.mu.Unlock()
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),
Etag: in.Etag,
RefreshIntervalSec: in.RefreshIntervalSec,
CommunityID: in.CommunityID,
}
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.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
}
}
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 || (in.ASN == nil && (in.Prefix == 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 != "AS_PREFIXES" {
return nil, ErrInvalidInput
}
id := uuid.NewString()
e := &ASEntry{ID: id, ModuleID: moduleID, ASN: in.ASN, Prefix: in.Prefix, 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
}
if patch.ASN != nil {
e.ASN = patch.ASN
}
if patch.Prefix != nil {
p := strings.TrimSpace(*patch.Prefix)
if p == "" {
e.Prefix = nil
} else {
e.Prefix = &p
}
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
e.CommunityID = nil
} else {
e.CommunityID = &v
}
}
return e, 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.DohProfileID != nil && *mod.DohProfileID == 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, id string) (*Community, error) {
m.mu.RLock()
defer m.mu.RUnlock()
c, ok := m.communities[id]
if !ok || c.TenantID != tenantID {
return nil, ErrNotFound
}
return c, nil
}
func (m *Memory) CreateCommunity(tenantID string, in *Community) (*Community, error) {
if in == nil || strings.TrimSpace(in.Kind) == "" {
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, Name: in.Name, Kind: strings.TrimSpace(in.Kind), 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.Name != nil {
c.Name = *patch.Name
}
if patch.Kind != nil {
c.Kind = strings.TrimSpace(*patch.Kind)
}
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 || strings.TrimSpace(in.Neighbor) == "" || in.RemoteASN == 0 {
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: strings.TrimSpace(in.Neighbor), RemoteASN: in.RemoteASN, Enabled: in.Enabled,
SessionState: in.SessionState, PoliciesJSON: in.PoliciesJSON,
}
if !p.Enabled && p.SessionState == "" {
p.Enabled = true
}
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 {
p.Neighbor = strings.TrimSpace(*patch.Neighbor)
}
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) ListRevisionPrefixes(tenantID, revisionID string, cursor string, limit int) ([]PrefixRow, string, bool) {
if limit <= 0 {
limit = 50
}
m.mu.RLock()
defer m.mu.RUnlock()
if _, err := m.getRevisionLocked(tenantID, revisionID); err != nil {
return nil, "", false
}
all := m.revPrefixes[revisionID]
off := 0
if cursor != "" {
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
off = n
}
}
end := off + limit
next := ""
more := false
if end > len(all) {
end = len(all)
} else {
more = true
next = fmt.Sprintf("%d", end)
}
if off >= len(all) {
return nil, "", false
}
return all[off:end], 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
}