feat(store): add MaintenancePolicy CRUD backend
Типы maintenance_policy, методы store.Backend и реализации для PostgreSQL и in-memory. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,304 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"evobgp/internal/store"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maintenancePolicySelect = `
|
||||||
|
SELECT id, name, table_name, condition_sql, retention_period_sec, max_rows,
|
||||||
|
vacuum_strategy, schedule_cron, enabled, dry_run_enabled,
|
||||||
|
last_run_at, COALESCE(last_status, ''), COALESCE(last_error, ''),
|
||||||
|
created_at, updated_at
|
||||||
|
FROM maintenance_policy`
|
||||||
|
|
||||||
|
func scanMaintenancePolicy(row pgx.Row) (*store.MaintenancePolicy, error) {
|
||||||
|
var p store.MaintenancePolicy
|
||||||
|
var retention, maxRows *int32
|
||||||
|
var lastRun *time.Time
|
||||||
|
err := row.Scan(
|
||||||
|
&p.ID, &p.Name, &p.TableName, &p.Condition, &retention, &maxRows,
|
||||||
|
&p.VacuumStrategy, &p.Schedule, &p.Enabled, &p.DryRunEnabled,
|
||||||
|
&lastRun, &p.LastStatus, &p.LastError, &p.CreatedAt, &p.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if retention != nil {
|
||||||
|
v := int(*retention)
|
||||||
|
p.RetentionPeriodSec = &v
|
||||||
|
}
|
||||||
|
if maxRows != nil {
|
||||||
|
v := int(*maxRows)
|
||||||
|
p.MaxRows = &v
|
||||||
|
}
|
||||||
|
if lastRun != nil {
|
||||||
|
t := lastRun.UTC()
|
||||||
|
p.LastRunAt = &t
|
||||||
|
}
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) ListMaintenancePolicies(cursor string, limit int) ([]*store.MaintenancePolicy, string, bool, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
off := 0
|
||||||
|
if cursor != "" {
|
||||||
|
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
|
||||||
|
off = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
rows, err := p.pool.Query(ctx, maintenancePolicySelect+`
|
||||||
|
ORDER BY created_at DESC, id DESC
|
||||||
|
LIMIT $1 OFFSET $2`, limit+1, off)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []*store.MaintenancePolicy
|
||||||
|
for rows.Next() {
|
||||||
|
pol, err := scanMaintenancePolicy(rows)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, pol)
|
||||||
|
}
|
||||||
|
more := len(out) > limit
|
||||||
|
if more {
|
||||||
|
out = out[:limit]
|
||||||
|
}
|
||||||
|
next := ""
|
||||||
|
if more {
|
||||||
|
next = strconv.Itoa(off + limit)
|
||||||
|
}
|
||||||
|
return out, next, more, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) GetMaintenancePolicy(id string) (*store.MaintenancePolicy, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
row := p.pool.QueryRow(ctx, maintenancePolicySelect+` WHERE id=$1`, id)
|
||||||
|
pol, err := scanMaintenancePolicy(row)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pol, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) CreateMaintenancePolicy(in *store.MaintenancePolicy) (*store.MaintenancePolicy, error) {
|
||||||
|
if in == nil {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
vacuum := in.VacuumStrategy
|
||||||
|
if vacuum == "" {
|
||||||
|
vacuum = store.VacuumStrategyNone
|
||||||
|
}
|
||||||
|
if err := store.ValidateMaintenancePolicyInput(in.Name, in.TableName, vacuum, in.Schedule); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
id := uuid.NewString()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
condition := store.NormalizeMaintenancePolicyCondition(in.Condition)
|
||||||
|
var retention, maxRows *int32
|
||||||
|
if in.RetentionPeriodSec != nil {
|
||||||
|
v := int32(*in.RetentionPeriodSec)
|
||||||
|
retention = &v
|
||||||
|
}
|
||||||
|
if in.MaxRows != nil {
|
||||||
|
v := int32(*in.MaxRows)
|
||||||
|
maxRows = &v
|
||||||
|
}
|
||||||
|
_, err := p.pool.Exec(ctx, `
|
||||||
|
INSERT INTO maintenance_policy (
|
||||||
|
id, name, table_name, condition_sql, retention_period_sec, max_rows,
|
||||||
|
vacuum_strategy, schedule_cron, enabled, dry_run_enabled, created_at, updated_at
|
||||||
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$11)`,
|
||||||
|
id, strings.TrimSpace(in.Name), strings.TrimSpace(in.TableName), condition,
|
||||||
|
retention, maxRows, vacuum, strings.TrimSpace(in.Schedule),
|
||||||
|
in.Enabled, in.DryRunEnabled, now)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.GetMaintenancePolicy(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) UpdateMaintenancePolicy(id string, patch *store.MaintenancePolicyPatch) (*store.MaintenancePolicy, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
cur, err := p.GetMaintenancePolicy(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if patch.Name != nil {
|
||||||
|
cur.Name = strings.TrimSpace(*patch.Name)
|
||||||
|
}
|
||||||
|
if patch.TableName != nil {
|
||||||
|
cur.TableName = strings.TrimSpace(*patch.TableName)
|
||||||
|
}
|
||||||
|
if patch.Condition != nil {
|
||||||
|
cur.Condition = store.NormalizeMaintenancePolicyCondition(*patch.Condition)
|
||||||
|
}
|
||||||
|
if patch.RetentionPeriodSec != nil {
|
||||||
|
cur.RetentionPeriodSec = patch.RetentionPeriodSec
|
||||||
|
}
|
||||||
|
if patch.MaxRows != nil {
|
||||||
|
cur.MaxRows = patch.MaxRows
|
||||||
|
}
|
||||||
|
if patch.VacuumStrategy != nil {
|
||||||
|
if !store.ValidVacuumStrategy(*patch.VacuumStrategy) {
|
||||||
|
return nil, store.ErrInvalidInput
|
||||||
|
}
|
||||||
|
cur.VacuumStrategy = strings.TrimSpace(*patch.VacuumStrategy)
|
||||||
|
}
|
||||||
|
if patch.Schedule != nil {
|
||||||
|
cur.Schedule = strings.TrimSpace(*patch.Schedule)
|
||||||
|
}
|
||||||
|
if patch.Enabled != nil {
|
||||||
|
cur.Enabled = *patch.Enabled
|
||||||
|
}
|
||||||
|
if patch.DryRunEnabled != nil {
|
||||||
|
cur.DryRunEnabled = *patch.DryRunEnabled
|
||||||
|
}
|
||||||
|
if err := store.ValidateMaintenancePolicyInput(cur.Name, cur.TableName, cur.VacuumStrategy, cur.Schedule); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var retention, maxRows *int32
|
||||||
|
if cur.RetentionPeriodSec != nil {
|
||||||
|
v := int32(*cur.RetentionPeriodSec)
|
||||||
|
retention = &v
|
||||||
|
}
|
||||||
|
if cur.MaxRows != nil {
|
||||||
|
v := int32(*cur.MaxRows)
|
||||||
|
maxRows = &v
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
tag, err := p.pool.Exec(ctx, `
|
||||||
|
UPDATE maintenance_policy SET
|
||||||
|
name=$2, table_name=$3, condition_sql=$4, retention_period_sec=$5, max_rows=$6,
|
||||||
|
vacuum_strategy=$7, schedule_cron=$8, enabled=$9, dry_run_enabled=$10, updated_at=now()
|
||||||
|
WHERE id=$1`,
|
||||||
|
id, cur.Name, cur.TableName, cur.Condition, retention, maxRows,
|
||||||
|
cur.VacuumStrategy, cur.Schedule, cur.Enabled, cur.DryRunEnabled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return nil, store.ErrNotFound
|
||||||
|
}
|
||||||
|
return p.GetMaintenancePolicy(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) DeleteMaintenancePolicy(id string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
tag, err := p.pool.Exec(ctx, `DELETE FROM maintenance_policy WHERE id=$1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) TouchMaintenancePolicyRun(id, status, errMsg string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
tag, err := p.pool.Exec(ctx, `
|
||||||
|
UPDATE maintenance_policy SET
|
||||||
|
last_run_at=now(), last_status=$2, last_error=NULLIF($3,''), updated_at=now()
|
||||||
|
WHERE id=$1`, id, status, errMsg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) AppendMaintenancePolicyConfigAudit(actor, policyID, action string, before, after map[string]any) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
id := uuid.NewString()
|
||||||
|
var beforeJSON, afterJSON []byte
|
||||||
|
if before != nil {
|
||||||
|
beforeJSON, _ = json.Marshal(before)
|
||||||
|
}
|
||||||
|
if after != nil {
|
||||||
|
afterJSON, _ = json.Marshal(after)
|
||||||
|
}
|
||||||
|
_, err := p.pool.Exec(ctx, `
|
||||||
|
INSERT INTO maintenance_policy_config_audit
|
||||||
|
(id, policy_id, actor_prefix, action, before_json, after_json, created_at)
|
||||||
|
VALUES ($1, NULLIF($2,''), $3, $4, $5::jsonb, $6::jsonb, now())`,
|
||||||
|
id, policyID, strings.TrimSpace(actor), action,
|
||||||
|
nullJSONBytes(beforeJSON), nullJSONBytes(afterJSON))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullJSONBytes(b []byte) any {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) ListMaintenancePolicyConfigAudit(cursor string, limit int) ([]*store.MaintenancePolicyConfigAudit, string, bool, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
off := 0
|
||||||
|
if cursor != "" {
|
||||||
|
if n, err := strconv.Atoi(cursor); err == nil && n >= 0 {
|
||||||
|
off = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
rows, err := p.pool.Query(ctx, `
|
||||||
|
SELECT id, COALESCE(policy_id,''), actor_prefix, action,
|
||||||
|
before_json, after_json, created_at
|
||||||
|
FROM maintenance_policy_config_audit
|
||||||
|
ORDER BY created_at DESC, id DESC
|
||||||
|
LIMIT $1 OFFSET $2`, limit+1, off)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []*store.MaintenancePolicyConfigAudit
|
||||||
|
for rows.Next() {
|
||||||
|
var r store.MaintenancePolicyConfigAudit
|
||||||
|
var beforeRaw, afterRaw []byte
|
||||||
|
if err := rows.Scan(&r.ID, &r.PolicyID, &r.ActorPrefix, &r.Action, &beforeRaw, &afterRaw, &r.CreatedAt); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(beforeRaw) > 0 {
|
||||||
|
_ = json.Unmarshal(beforeRaw, &r.Before)
|
||||||
|
}
|
||||||
|
if len(afterRaw) > 0 {
|
||||||
|
_ = json.Unmarshal(afterRaw, &r.After)
|
||||||
|
}
|
||||||
|
out = append(out, &r)
|
||||||
|
}
|
||||||
|
more := len(out) > limit
|
||||||
|
if more {
|
||||||
|
out = out[:limit]
|
||||||
|
}
|
||||||
|
next := ""
|
||||||
|
if more {
|
||||||
|
next = strconv.Itoa(off + limit)
|
||||||
|
}
|
||||||
|
return out, next, more, rows.Err()
|
||||||
|
}
|
||||||
@@ -115,6 +115,16 @@ type Backend interface {
|
|||||||
|
|
||||||
// RunPeriodicMaintenance prunes stale DB rows (no-op for in-memory).
|
// RunPeriodicMaintenance prunes stale DB rows (no-op for in-memory).
|
||||||
RunPeriodicMaintenance(ctx context.Context)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ASNPrefixCacheEntry is a cached RIPEstat response for one ASN.
|
// ASNPrefixCacheEntry is a cached RIPEstat response for one ASN.
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Vacuum strategy values for maintenance_policy.vacuum_strategy.
|
||||||
|
const (
|
||||||
|
VacuumStrategyNone = "none"
|
||||||
|
VacuumStrategyVacuum = "vacuum"
|
||||||
|
VacuumStrategyAnalyze = "analyze"
|
||||||
|
VacuumStrategyVacuumAnalyze = "vacuum_analyze"
|
||||||
|
VacuumStrategyReindex = "reindex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaintenancePolicy is an instance-scoped PostgreSQL maintenance policy (control plane DB).
|
||||||
|
type MaintenancePolicy struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TableName string `json:"table_name"`
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
RetentionPeriodSec *int `json:"retention_period_sec,omitempty"`
|
||||||
|
MaxRows *int `json:"max_rows,omitempty"`
|
||||||
|
VacuumStrategy string `json:"vacuum_strategy"`
|
||||||
|
Schedule string `json:"schedule"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
DryRunEnabled bool `json:"dry_run_enabled"`
|
||||||
|
LastRunAt *time.Time `json:"last_run_at,omitempty"`
|
||||||
|
LastStatus string `json:"last_status,omitempty"`
|
||||||
|
LastError string `json:"last_error,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaintenancePolicyPatch is a partial update for maintenance_policy.
|
||||||
|
type MaintenancePolicyPatch struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
TableName *string `json:"table_name,omitempty"`
|
||||||
|
Condition *string `json:"condition,omitempty"`
|
||||||
|
RetentionPeriodSec *int `json:"retention_period_sec,omitempty"`
|
||||||
|
MaxRows *int `json:"max_rows,omitempty"`
|
||||||
|
VacuumStrategy *string `json:"vacuum_strategy,omitempty"`
|
||||||
|
Schedule *string `json:"schedule,omitempty"`
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
|
DryRunEnabled *bool `json:"dry_run_enabled,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaintenancePolicyConfigAudit is a configuration change log entry.
|
||||||
|
type MaintenancePolicyConfigAudit struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PolicyID string `json:"policy_id,omitempty"`
|
||||||
|
ActorPrefix string `json:"actor_prefix"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Before map[string]any `json:"before,omitempty"`
|
||||||
|
After map[string]any `json:"after,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidVacuumStrategy reports whether s is an allowed vacuum_strategy value.
|
||||||
|
func ValidVacuumStrategy(s string) bool {
|
||||||
|
switch strings.TrimSpace(s) {
|
||||||
|
case VacuumStrategyNone, VacuumStrategyVacuum, VacuumStrategyAnalyze,
|
||||||
|
VacuumStrategyVacuumAnalyze, VacuumStrategyReindex:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeMaintenancePolicyCondition returns a safe default WHERE fragment.
|
||||||
|
func NormalizeMaintenancePolicyCondition(condition string) string {
|
||||||
|
c := strings.TrimSpace(condition)
|
||||||
|
if c == "" {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateMaintenancePolicyInput checks required fields for create/update payloads.
|
||||||
|
func ValidateMaintenancePolicyInput(name, tableName, vacuumStrategy, schedule string) error {
|
||||||
|
if strings.TrimSpace(name) == "" || strings.TrimSpace(tableName) == "" || strings.TrimSpace(schedule) == "" {
|
||||||
|
return ErrInvalidInput
|
||||||
|
}
|
||||||
|
if !ValidVacuumStrategy(vacuumStrategy) {
|
||||||
|
return ErrInvalidInput
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+32
-28
@@ -33,17 +33,19 @@ type Memory struct {
|
|||||||
|
|
||||||
peers map[string]*BGPPeer
|
peers map[string]*BGPPeer
|
||||||
|
|
||||||
dohProfiles map[string]*DohProfile
|
dohProfiles map[string]*DohProfile
|
||||||
communities map[string]*Community
|
communities map[string]*Community
|
||||||
cdnSources map[string]*CDNSource
|
cdnSources map[string]*CDNSource
|
||||||
asEntries map[string]*ASEntry
|
asEntries map[string]*ASEntry
|
||||||
domainEnt map[string]*DomainEntry
|
domainEnt map[string]*DomainEntry
|
||||||
ipRanges map[string]*IPRangeEntry
|
ipRanges map[string]*IPRangeEntry
|
||||||
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
||||||
revPrefixes map[string][]PrefixRow
|
revPrefixes map[string][]PrefixRow
|
||||||
moduleSnapshots map[string]*moduleSnapshotRec
|
moduleSnapshots map[string]*moduleSnapshotRec
|
||||||
asnPrefixCache map[int64]*ASNPrefixCacheEntry
|
asnPrefixCache map[int64]*ASNPrefixCacheEntry
|
||||||
apiKeys map[string]*apiKeyRec
|
apiKeys map[string]*apiKeyRec
|
||||||
|
maintenancePolicies map[string]*MaintenancePolicy
|
||||||
|
maintConfigAudit []*MaintenancePolicyConfigAudit
|
||||||
|
|
||||||
// DemoIDs valid after SeedDemo()
|
// DemoIDs valid after SeedDemo()
|
||||||
demoTenantID string
|
demoTenantID string
|
||||||
@@ -123,23 +125,25 @@ type Speaker struct {
|
|||||||
|
|
||||||
func NewMemory() *Memory {
|
func NewMemory() *Memory {
|
||||||
return &Memory{
|
return &Memory{
|
||||||
tenants: make(map[string]*Tenant),
|
tenants: make(map[string]*Tenant),
|
||||||
modules: make(map[string]*Module),
|
modules: make(map[string]*Module),
|
||||||
revisions: make(map[string]*Revision),
|
revisions: make(map[string]*Revision),
|
||||||
speakers: make(map[string]*Speaker),
|
speakers: make(map[string]*Speaker),
|
||||||
publishedRevision: make(map[string]publishedInfo),
|
publishedRevision: make(map[string]publishedInfo),
|
||||||
peers: make(map[string]*BGPPeer),
|
peers: make(map[string]*BGPPeer),
|
||||||
dohProfiles: make(map[string]*DohProfile),
|
dohProfiles: make(map[string]*DohProfile),
|
||||||
communities: make(map[string]*Community),
|
communities: make(map[string]*Community),
|
||||||
cdnSources: make(map[string]*CDNSource),
|
cdnSources: make(map[string]*CDNSource),
|
||||||
asEntries: make(map[string]*ASEntry),
|
asEntries: make(map[string]*ASEntry),
|
||||||
domainEnt: make(map[string]*DomainEntry),
|
domainEnt: make(map[string]*DomainEntry),
|
||||||
ipRanges: make(map[string]*IPRangeEntry),
|
ipRanges: make(map[string]*IPRangeEntry),
|
||||||
settings: make(map[string]map[string]any),
|
settings: make(map[string]map[string]any),
|
||||||
revPrefixes: make(map[string][]PrefixRow),
|
revPrefixes: make(map[string][]PrefixRow),
|
||||||
moduleSnapshots: make(map[string]*moduleSnapshotRec),
|
moduleSnapshots: make(map[string]*moduleSnapshotRec),
|
||||||
asnPrefixCache: make(map[int64]*ASNPrefixCacheEntry),
|
asnPrefixCache: make(map[int64]*ASNPrefixCacheEntry),
|
||||||
apiKeys: make(map[string]*apiKeyRec),
|
apiKeys: make(map[string]*apiKeyRec),
|
||||||
|
maintenancePolicies: make(map[string]*MaintenancePolicy),
|
||||||
|
maintConfigAudit: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Memory) ListMaintenancePolicies(cursor string, limit int) ([]*MaintenancePolicy, string, bool, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
all := make([]*MaintenancePolicy, 0, len(m.maintenancePolicies))
|
||||||
|
for _, p := range m.maintenancePolicies {
|
||||||
|
all = append(all, p)
|
||||||
|
}
|
||||||
|
sort.Slice(all, func(i, j int) bool {
|
||||||
|
if all[i].CreatedAt.Equal(all[j].CreatedAt) {
|
||||||
|
return all[i].ID > all[j].ID
|
||||||
|
}
|
||||||
|
return all[i].CreatedAt.After(all[j].CreatedAt)
|
||||||
|
})
|
||||||
|
off := parseMaintCursor(cursor)
|
||||||
|
end := off + limit
|
||||||
|
next := ""
|
||||||
|
hasMore := false
|
||||||
|
if end > len(all) {
|
||||||
|
end = len(all)
|
||||||
|
} else if end < len(all) {
|
||||||
|
hasMore = true
|
||||||
|
next = formatMaintCursor(end)
|
||||||
|
}
|
||||||
|
if off >= len(all) {
|
||||||
|
return nil, "", false, nil
|
||||||
|
}
|
||||||
|
out := make([]*MaintenancePolicy, end-off)
|
||||||
|
copy(out, all[off:end])
|
||||||
|
return out, next, hasMore, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) GetMaintenancePolicy(id string) (*MaintenancePolicy, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
p, ok := m.maintenancePolicies[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return cloneMaintenancePolicy(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) CreateMaintenancePolicy(in *MaintenancePolicy) (*MaintenancePolicy, error) {
|
||||||
|
if in == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
vacuum := in.VacuumStrategy
|
||||||
|
if vacuum == "" {
|
||||||
|
vacuum = VacuumStrategyNone
|
||||||
|
}
|
||||||
|
if err := ValidateMaintenancePolicyInput(in.Name, in.TableName, vacuum, in.Schedule); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
id := uuid.NewString()
|
||||||
|
p := &MaintenancePolicy{
|
||||||
|
ID: id,
|
||||||
|
Name: strings.TrimSpace(in.Name),
|
||||||
|
TableName: strings.TrimSpace(in.TableName),
|
||||||
|
Condition: NormalizeMaintenancePolicyCondition(in.Condition),
|
||||||
|
RetentionPeriodSec: in.RetentionPeriodSec,
|
||||||
|
MaxRows: in.MaxRows,
|
||||||
|
VacuumStrategy: vacuum,
|
||||||
|
Schedule: strings.TrimSpace(in.Schedule),
|
||||||
|
Enabled: in.Enabled,
|
||||||
|
DryRunEnabled: in.DryRunEnabled,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
m.maintenancePolicies[id] = p
|
||||||
|
return cloneMaintenancePolicy(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) UpdateMaintenancePolicy(id string, patch *MaintenancePolicyPatch) (*MaintenancePolicy, error) {
|
||||||
|
if patch == nil {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
p, ok := m.maintenancePolicies[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if patch.Name != nil {
|
||||||
|
p.Name = strings.TrimSpace(*patch.Name)
|
||||||
|
}
|
||||||
|
if patch.TableName != nil {
|
||||||
|
p.TableName = strings.TrimSpace(*patch.TableName)
|
||||||
|
}
|
||||||
|
if patch.Condition != nil {
|
||||||
|
p.Condition = NormalizeMaintenancePolicyCondition(*patch.Condition)
|
||||||
|
}
|
||||||
|
if patch.RetentionPeriodSec != nil {
|
||||||
|
p.RetentionPeriodSec = patch.RetentionPeriodSec
|
||||||
|
}
|
||||||
|
if patch.MaxRows != nil {
|
||||||
|
p.MaxRows = patch.MaxRows
|
||||||
|
}
|
||||||
|
if patch.VacuumStrategy != nil {
|
||||||
|
if !ValidVacuumStrategy(*patch.VacuumStrategy) {
|
||||||
|
return nil, ErrInvalidInput
|
||||||
|
}
|
||||||
|
p.VacuumStrategy = strings.TrimSpace(*patch.VacuumStrategy)
|
||||||
|
}
|
||||||
|
if patch.Schedule != nil {
|
||||||
|
p.Schedule = strings.TrimSpace(*patch.Schedule)
|
||||||
|
}
|
||||||
|
if patch.Enabled != nil {
|
||||||
|
p.Enabled = *patch.Enabled
|
||||||
|
}
|
||||||
|
if patch.DryRunEnabled != nil {
|
||||||
|
p.DryRunEnabled = *patch.DryRunEnabled
|
||||||
|
}
|
||||||
|
if err := ValidateMaintenancePolicyInput(p.Name, p.TableName, p.VacuumStrategy, p.Schedule); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.UpdatedAt = time.Now().UTC()
|
||||||
|
return cloneMaintenancePolicy(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteMaintenancePolicy(id string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, ok := m.maintenancePolicies[id]; !ok {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
delete(m.maintenancePolicies, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) TouchMaintenancePolicyRun(id, status, errMsg string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
p, ok := m.maintenancePolicies[id]
|
||||||
|
if !ok {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
now := time.Now().UTC()
|
||||||
|
p.LastRunAt = &now
|
||||||
|
p.LastStatus = status
|
||||||
|
p.LastError = errMsg
|
||||||
|
p.UpdatedAt = now
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) AppendMaintenancePolicyConfigAudit(actor, policyID, action string, before, after map[string]any) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
row := &MaintenancePolicyConfigAudit{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
PolicyID: policyID,
|
||||||
|
ActorPrefix: strings.TrimSpace(actor),
|
||||||
|
Action: action,
|
||||||
|
Before: before,
|
||||||
|
After: after,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
m.maintConfigAudit = append(m.maintConfigAudit, row)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memory) ListMaintenancePolicyConfigAudit(cursor string, limit int) ([]*MaintenancePolicyConfigAudit, string, bool, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
all := append([]*MaintenancePolicyConfigAudit(nil), m.maintConfigAudit...)
|
||||||
|
sort.Slice(all, func(i, j int) bool {
|
||||||
|
if all[i].CreatedAt.Equal(all[j].CreatedAt) {
|
||||||
|
return all[i].ID > all[j].ID
|
||||||
|
}
|
||||||
|
return all[i].CreatedAt.After(all[j].CreatedAt)
|
||||||
|
})
|
||||||
|
off := parseMaintCursor(cursor)
|
||||||
|
end := off + limit
|
||||||
|
next := ""
|
||||||
|
hasMore := false
|
||||||
|
if end > len(all) {
|
||||||
|
end = len(all)
|
||||||
|
} else if end < len(all) {
|
||||||
|
hasMore = true
|
||||||
|
next = formatMaintCursor(end)
|
||||||
|
}
|
||||||
|
if off >= len(all) {
|
||||||
|
return nil, "", false, nil
|
||||||
|
}
|
||||||
|
out := make([]*MaintenancePolicyConfigAudit, end-off)
|
||||||
|
copy(out, all[off:end])
|
||||||
|
return out, next, hasMore, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneMaintenancePolicy(p *MaintenancePolicy) *MaintenancePolicy {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cp := *p
|
||||||
|
if p.RetentionPeriodSec != nil {
|
||||||
|
v := *p.RetentionPeriodSec
|
||||||
|
cp.RetentionPeriodSec = &v
|
||||||
|
}
|
||||||
|
if p.MaxRows != nil {
|
||||||
|
v := *p.MaxRows
|
||||||
|
cp.MaxRows = &v
|
||||||
|
}
|
||||||
|
if p.LastRunAt != nil {
|
||||||
|
t := *p.LastRunAt
|
||||||
|
cp.LastRunAt = &t
|
||||||
|
}
|
||||||
|
return &cp
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMaintCursor(cursor string) int {
|
||||||
|
if cursor == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var off int
|
||||||
|
for _, r := range cursor {
|
||||||
|
if r < '0' || r > '9' {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
off = off*10 + int(r-'0')
|
||||||
|
}
|
||||||
|
return off
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMaintCursor(off int) string {
|
||||||
|
return strconv.Itoa(off)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user