Files
EvoBGP/internal/repository/postgres_entities.go
T
Denozordec e20c9f3113
CI / changes (push) Successful in 6s
CI / openapi (push) Successful in 25s
CI / go (push) Successful in 29s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-bird (push) Successful in 42s
CI / bird2 (push) Successful in 14s
CI / docker-go-prime (push) Successful in 1m22s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 58s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m0s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m16s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m14s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m17s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m2s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m15s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m15s
feat: implement CDN source preview functionality and enhance data handling. Add a new endpoint for previewing CDN sources, allowing users to fetch and parse CIDR prefixes from specified URLs. Update OpenAPI documentation to include new request and response schemas, and modify internal logic to support JSON parsing with prefix path traversal. Enhance UI components to accommodate new preview features, improving user experience and data management.
2026-04-06 15:08:17 +07:00

595 lines
17 KiB
Go

package repository
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"evobgp/internal/store"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
func (p *Postgres) ListCDNSources(tenantID, moduleID string) ([]*store.CDNSource, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "CDN_CIDRS" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text
FROM module_cdn_source WHERE module_id=$1 ORDER BY url`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.CDNSource
for rows.Next() {
var s store.CDNSource
s.ModuleID = moduleID
var ri *int32
var comm *string
if err := rows.Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm); err != nil {
continue
}
if ri != nil {
v := int(*ri)
s.RefreshIntervalSec = &v
}
s.CommunityID = strOrNil(comm)
out = append(out, &s)
}
return out, nil
}
func (p *Postgres) CreateCDNSource(tenantID, moduleID string, in *store.CDNSource) (*store.CDNSource, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "CDN_CIDRS" {
return nil, store.ErrInvalidInput
}
if in == nil || strings.TrimSpace(in.URL) == "" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_cdn_source (id, module_id, source_kind, url, prefix_path, etag, refresh_interval_sec, community_id)
VALUES ($1,$2,$3,$4,$5,$6,$7, NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, in.SourceKind, strings.TrimSpace(in.URL), strings.TrimSpace(in.PrefixPath), in.Etag, nullInt32Ptr(in.RefreshIntervalSec), uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
return p.getCDNSource(ctx, moduleID, id)
}
func (p *Postgres) getCDNSource(ctx context.Context, moduleID, id string) (*store.CDNSource, error) {
var s store.CDNSource
s.ModuleID = moduleID
var ri *int32
var comm *string
err := p.pool.QueryRow(ctx, `
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text
FROM module_cdn_source WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm)
if err != nil {
return nil, err
}
if ri != nil {
v := int(*ri)
s.RefreshIntervalSec = &v
}
s.CommunityID = strOrNil(comm)
return &s, nil
}
func uuidOrNilPtr(s *string) any {
if s == nil || strings.TrimSpace(*s) == "" {
return nil
}
return strings.TrimSpace(*s)
}
func (p *Postgres) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *store.CDNSourcePatch) (*store.CDNSource, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getCDNSource(context.Background(), moduleID, sourceID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
if patch.SourceKind != nil {
cur.SourceKind = *patch.SourceKind
}
if patch.URL != nil {
cur.URL = strings.TrimSpace(*patch.URL)
}
if patch.PrefixPath != nil {
cur.PrefixPath = strings.TrimSpace(*patch.PrefixPath)
}
if patch.Etag != nil {
cur.Etag = *patch.Etag
}
if patch.RefreshIntervalSec != nil {
cur.RefreshIntervalSec = patch.RefreshIntervalSec
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
ctx := context.Background()
_, err = p.pool.Exec(ctx, `
UPDATE module_cdn_source SET source_kind=$3, url=$4, prefix_path=$5, etag=$6, refresh_interval_sec=$7,
community_id=NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
sourceID, moduleID, cur.SourceKind, cur.URL, cur.PrefixPath, cur.Etag, nullInt32Ptr(cur.RefreshIntervalSec), uuidOrNilPtr(cur.CommunityID))
if err != nil {
return nil, err
}
return p.getCDNSource(ctx, moduleID, sourceID)
}
func (p *Postgres) DeleteCDNSource(tenantID, moduleID, sourceID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_cdn_source WHERE id=$1 AND module_id=$2`, sourceID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) ListASEntries(tenantID, moduleID string) ([]*store.ASEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "AS_PREFIXES" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `
SELECT id::text, asn, community_id::text, asn_name, prefix_count, asn_resolved_at
FROM module_as_entry WHERE module_id=$1 ORDER BY asn`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.ASEntry
for rows.Next() {
var e store.ASEntry
e.ModuleID = moduleID
var comm, asnName *string
var pc *int64
var at *time.Time
if err := rows.Scan(&e.ID, &e.ASN, &comm, &asnName, &pc, &at); err != nil {
continue
}
e.CommunityID = strOrNil(comm)
if asnName != nil {
e.ASNName = *asnName
}
e.PrefixCount = pc
e.ASNResolvedAt = at
out = append(out, &e)
}
return out, nil
}
func (p *Postgres) CreateASEntry(tenantID, moduleID string, in *store.ASEntry) (*store.ASEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "AS_PREFIXES" {
return nil, store.ErrInvalidInput
}
if in == nil || !store.ValidASN(in.ASN) {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_as_entry (id, module_id, asn, community_id)
VALUES ($1,$2,$3,NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, in.ASN, uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
return p.getASEntry(ctx, moduleID, id)
}
func (p *Postgres) getASEntry(ctx context.Context, moduleID, id string) (*store.ASEntry, error) {
var e store.ASEntry
e.ModuleID = moduleID
var comm, asnName *string
var pc *int64
var at *time.Time
err := p.pool.QueryRow(ctx, `
SELECT id::text, asn, community_id::text, asn_name, prefix_count, asn_resolved_at
FROM module_as_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(
&e.ID, &e.ASN, &comm, &asnName, &pc, &at)
if err != nil {
return nil, err
}
e.CommunityID = strOrNil(comm)
if asnName != nil {
e.ASNName = *asnName
}
e.PrefixCount = pc
e.ASNResolvedAt = at
return &e, nil
}
func (p *Postgres) UpdateASEntry(tenantID, moduleID, entryID string, patch *store.ASEntryPatch) (*store.ASEntry, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getASEntry(context.Background(), moduleID, entryID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
prevASN := cur.ASN
if patch.ASN != nil {
cur.ASN = *patch.ASN
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
if !store.ValidASN(cur.ASN) {
return nil, store.ErrInvalidInput
}
clearResolve := patch.ASN != nil && cur.ASN != prevASN
ctx := context.Background()
if clearResolve {
_, err = p.pool.Exec(ctx, `
UPDATE module_as_entry SET asn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid),
asn_name=NULL, prefix_count=NULL, asn_resolved_at=NULL, updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.ASN, uuidOrNilPtr(cur.CommunityID))
} else {
_, err = p.pool.Exec(ctx, `
UPDATE module_as_entry SET asn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.ASN, uuidOrNilPtr(cur.CommunityID))
}
if err != nil {
return nil, err
}
return p.getASEntry(ctx, moduleID, entryID)
}
func (p *Postgres) UpdateASEntryResolveMeta(tenantID, moduleID, entryID string, asnName string, prefixCount int64, resolvedAt time.Time) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
var nameArg any
sn := strings.TrimSpace(asnName)
if sn == "" {
nameArg = nil
} else {
nameArg = sn
}
tag, err := p.pool.Exec(ctx, `
UPDATE module_as_entry SET asn_name=$3, prefix_count=$4, asn_resolved_at=$5, updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, nameArg, prefixCount, resolvedAt.UTC())
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) DeleteASEntry(tenantID, moduleID, entryID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_as_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) ListDomainEntries(tenantID, moduleID string) ([]*store.DomainEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "DOMAINS" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `SELECT id::text, fqdn, community_id::text FROM module_domain_entry WHERE module_id=$1`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.DomainEntry
for rows.Next() {
var e store.DomainEntry
e.ModuleID = moduleID
var comm *string
if err := rows.Scan(&e.ID, &e.FQDN, &comm); err != nil {
continue
}
e.CommunityID = strOrNil(comm)
out = append(out, &e)
}
return out, nil
}
func (p *Postgres) CreateDomainEntry(tenantID, moduleID string, in *store.DomainEntry) (*store.DomainEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "DOMAINS" || in == nil || strings.TrimSpace(in.FQDN) == "" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_domain_entry (id, module_id, fqdn, community_id)
VALUES ($1,$2,$3, NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, strings.TrimSpace(in.FQDN), uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
return p.getDomainEntry(ctx, moduleID, id)
}
func (p *Postgres) getDomainEntry(ctx context.Context, moduleID, id string) (*store.DomainEntry, error) {
var e store.DomainEntry
e.ModuleID = moduleID
var comm *string
err := p.pool.QueryRow(ctx, `SELECT id::text, fqdn, community_id::text FROM module_domain_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&e.ID, &e.FQDN, &comm)
if err != nil {
return nil, err
}
e.CommunityID = strOrNil(comm)
return &e, nil
}
func (p *Postgres) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *store.DomainEntryPatch) (*store.DomainEntry, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getDomainEntry(context.Background(), moduleID, entryID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
if patch.FQDN != nil {
cur.FQDN = strings.TrimSpace(*patch.FQDN)
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
ctx := context.Background()
_, err = p.pool.Exec(ctx, `
UPDATE module_domain_entry SET fqdn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.FQDN, uuidOrNilPtr(cur.CommunityID))
if err != nil {
return nil, err
}
return p.getDomainEntry(ctx, moduleID, entryID)
}
func (p *Postgres) DeleteDomainEntry(tenantID, moduleID, entryID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_domain_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) ListIPRangeEntries(tenantID, moduleID string) ([]*store.IPRangeEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "IP_RANGES" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `SELECT id::text, prefix::text, community_id::text FROM module_ip_range_entry WHERE module_id=$1`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.IPRangeEntry
for rows.Next() {
var e store.IPRangeEntry
e.ModuleID = moduleID
var comm *string
if err := rows.Scan(&e.ID, &e.Prefix, &comm); err != nil {
continue
}
e.CommunityID = strOrNil(comm)
out = append(out, &e)
}
return out, nil
}
func (p *Postgres) CreateIPRangeEntry(tenantID, moduleID string, in *store.IPRangeEntry) (*store.IPRangeEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "IP_RANGES" || in == nil || strings.TrimSpace(in.Prefix) == "" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_ip_range_entry (id, module_id, prefix, community_id)
VALUES ($1,$2,$3::cidr, NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, strings.TrimSpace(in.Prefix), uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
return p.getIPRangeEntry(ctx, moduleID, id)
}
func (p *Postgres) getIPRangeEntry(ctx context.Context, moduleID, id string) (*store.IPRangeEntry, error) {
var e store.IPRangeEntry
e.ModuleID = moduleID
var comm *string
err := p.pool.QueryRow(ctx, `SELECT id::text, prefix::text, community_id::text FROM module_ip_range_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&e.ID, &e.Prefix, &comm)
if err != nil {
return nil, err
}
e.CommunityID = strOrNil(comm)
return &e, nil
}
func (p *Postgres) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *store.IPRangePatch) (*store.IPRangeEntry, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getIPRangeEntry(context.Background(), moduleID, entryID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
if patch.Prefix != nil {
cur.Prefix = strings.TrimSpace(*patch.Prefix)
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
ctx := context.Background()
_, err = p.pool.Exec(ctx, `
UPDATE module_ip_range_entry SET prefix=$3::cidr, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.Prefix, uuidOrNilPtr(cur.CommunityID))
if err != nil {
return nil, err
}
return p.getIPRangeEntry(ctx, moduleID, entryID)
}
func (p *Postgres) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_ip_range_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) ListGlobalSettings(tenantID string) (map[string]any, error) {
ctx := context.Background()
rows, err := p.pool.Query(ctx, `SELECT key, value_json FROM global_settings WHERE tenant_id=$1`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]any)
for rows.Next() {
var k string
var vj []byte
if err := rows.Scan(&k, &vj); err != nil {
continue
}
var v any
_ = json.Unmarshal(vj, &v)
out[k] = v
}
return out, nil
}
func (p *Postgres) PatchGlobalSettings(tenantID string, patch map[string]any) error {
if patch == nil {
return nil
}
ctx := context.Background()
for k, v := range patch {
if strings.TrimSpace(k) == "" {
continue
}
if v == nil {
_, _ = p.pool.Exec(ctx, `DELETE FROM global_settings WHERE tenant_id=$1 AND key=$2`, tenantID, k)
continue
}
b, err := json.Marshal(v)
if err != nil {
return err
}
_, err = p.pool.Exec(ctx, `
INSERT INTO global_settings (tenant_id, key, value_json) VALUES ($1,$2,$3::jsonb)
ON CONFLICT (tenant_id, key) DO UPDATE SET value_json = EXCLUDED.value_json, updated_at = now()`,
tenantID, k, string(b))
if err != nil {
return err
}
}
return nil
}
var _ store.Backend = (*Postgres)(nil)