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 }