CI / changes (push) Successful in 7s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Has been skipped
CI / go (push) Successful in 41s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m18s
Правки errcheck в тестах httpapi и gofmt полей APIKeyPatch в store. Co-authored-by: Cursor <[email protected]>
315 lines
12 KiB
Go
315 lines
12 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
|
|
|
|
// 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
|
|
|
|
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)
|
|
|
|
GetRevision(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)
|
|
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
|
|
}
|
|
|
|
// 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
|
|
CommunityID *string
|
|
Source string
|
|
}
|