proxy-pool/internal/adapters/postgresadmin/snapshot.go

223 lines
5.9 KiB
Go

package postgresadmin
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"proxy-pool/internal/domain/adminstate"
)
func (adapter *Adapter) Snapshot(ctx context.Context) (adminstate.Snapshot, error) {
if err := contextError(ctx); err != nil {
return adminstate.Snapshot{}, err
}
if !adapter.valid() {
return adminstate.Snapshot{}, adminstate.ErrInvalidCommand
}
tx, err := adapter.begin(ctx, pgx.TxOptions{
IsoLevel: pgx.RepeatableRead,
AccessMode: pgx.ReadOnly,
}, "begin snapshot")
if err != nil {
return adminstate.Snapshot{}, err
}
defer rollback(tx)
snapshot := adminstate.Snapshot{}
var revision int64
if err := tx.QueryRow(ctx, currentRevisionSQL).Scan(&revision); err != nil {
return adminstate.Snapshot{}, databaseError(ctx, "read snapshot revision", err)
}
convertedRevision, ok := domainID(revision)
if !ok {
return adminstate.Snapshot{}, unavailable("decode snapshot revision")
}
snapshot.Revision = convertedRevision
var configRevision int64
config := adminstate.ConfigRevision{}
err = tx.QueryRow(ctx, `
SELECT revision, config_version, checksum::text, source, created_at
FROM config_revisions
ORDER BY revision DESC
LIMIT 1`).Scan(
&configRevision,
&config.ConfigVersion,
&config.Checksum,
&config.Source,
&config.CreatedAt,
)
switch {
case err == nil:
converted, ok := domainID(configRevision)
if !ok {
return adminstate.Snapshot{}, unavailable("decode config revision")
}
config.Revision = converted
config.CreatedAt = utc(config.CreatedAt)
snapshot.Config = &config
case !errors.Is(err, pgx.ErrNoRows):
return adminstate.Snapshot{}, databaseError(ctx, "read config snapshot", err)
}
upstreams, err := readUpstreamSnapshot(ctx, tx)
if err != nil {
return adminstate.Snapshot{}, err
}
snapshot.Upstreams = upstreams
routings, err := readRoutingSnapshot(ctx, tx)
if err != nil {
return adminstate.Snapshot{}, err
}
snapshot.Routings = routings
if err := commit(ctx, tx, "commit snapshot"); err != nil {
return adminstate.Snapshot{}, err
}
return snapshot, nil
}
func (adapter *Adapter) ReadAudit(
ctx context.Context,
query adminstate.AuditQuery,
) ([]adminstate.AuditRecord, error) {
if err := contextError(ctx); err != nil {
return nil, err
}
if query.Validate() != nil || !adapter.valid() {
return nil, adminstate.ErrInvalidCommand
}
afterID, ok := databaseID(query.AfterID)
if !ok {
return []adminstate.AuditRecord{}, nil
}
tx, err := adapter.begin(ctx, pgx.TxOptions{AccessMode: pgx.ReadOnly}, "begin audit read")
if err != nil {
return nil, err
}
defer rollback(tx)
rows, err := tx.Query(ctx, `
SELECT id, request_id, actor_id, COALESCE(host(source_ip), ''), action,
resource_type, resource_name, changed, revision, reason, occurred_at
FROM admin_audit_log
WHERE id > $1
ORDER BY id
LIMIT $2`, afterID, query.Limit)
if err != nil {
return nil, databaseError(ctx, "read audit page", err)
}
defer rows.Close()
records := make([]adminstate.AuditRecord, 0, query.Limit)
for rows.Next() {
record := adminstate.AuditRecord{}
var id int64
var revision int64
var action string
if err := rows.Scan(
&id,
&record.RequestID,
&record.Actor.ID,
&record.Actor.SourceIP,
&action,
&record.ResourceType,
&record.ResourceName,
&record.Changed,
&revision,
&record.Reason,
&record.OccurredAt,
); err != nil {
return nil, databaseError(ctx, "decode audit page", err)
}
convertedID, idOK := domainID(id)
convertedRevision, revisionOK := domainID(revision)
if !idOK || !revisionOK {
return nil, unavailable("decode audit page")
}
record.ID = convertedID
record.Revision = convertedRevision
record.Action = adminstate.Action(action)
record.OccurredAt = utc(record.OccurredAt)
records = append(records, record)
}
if err := rows.Err(); err != nil {
return nil, databaseError(ctx, "read audit page", err)
}
if err := commit(ctx, tx, "commit audit read"); err != nil {
return nil, err
}
return records, nil
}
func readUpstreamSnapshot(ctx context.Context, tx pgx.Tx) ([]adminstate.UpstreamState, error) {
rows, err := tx.Query(ctx, `
SELECT name, enabled, revision, updated_at
FROM upstream_admin_state
ORDER BY name`)
if err != nil {
return nil, databaseError(ctx, "read upstream snapshot", err)
}
defer rows.Close()
states := make([]adminstate.UpstreamState, 0)
for rows.Next() {
state := adminstate.UpstreamState{}
var revision int64
if err := rows.Scan(&state.Name, &state.Enabled, &revision, &state.UpdatedAt); err != nil {
return nil, databaseError(ctx, "decode upstream snapshot", err)
}
converted, ok := domainID(revision)
if !ok {
return nil, unavailable("decode upstream snapshot")
}
state.Revision = converted
state.UpdatedAt = utc(state.UpdatedAt)
states = append(states, state)
}
if err := rows.Err(); err != nil {
return nil, databaseError(ctx, "read upstream snapshot", err)
}
return states, nil
}
func readRoutingSnapshot(ctx context.Context, tx pgx.Tx) ([]adminstate.RoutingState, error) {
rows, err := tx.Query(ctx, `
SELECT name, enabled, upstreams, current_upstream, revision, updated_at
FROM routing_admin_state
ORDER BY name`)
if err != nil {
return nil, databaseError(ctx, "read routing snapshot", err)
}
defer rows.Close()
states := make([]adminstate.RoutingState, 0)
for rows.Next() {
state := adminstate.RoutingState{}
var revision int64
if err := rows.Scan(
&state.Name,
&state.Enabled,
&state.Upstreams,
&state.CurrentUpstream,
&revision,
&state.UpdatedAt,
); err != nil {
return nil, databaseError(ctx, "decode routing snapshot", err)
}
converted, ok := domainID(revision)
if !ok {
return nil, unavailable("decode routing snapshot")
}
state.Revision = converted
state.UpdatedAt = utc(state.UpdatedAt)
state.Upstreams = append([]string(nil), state.Upstreams...)
states = append(states, state)
}
if err := rows.Err(); err != nil {
return nil, databaseError(ctx, "read routing snapshot", err)
}
return states, nil
}