RunPeriodicMaintenance и RunCleanup удалены; scheduler политик в StartBackground; deprecated /postgres/cleanup принимает policy_id. Co-authored-by: Cursor <[email protected]>
213 lines
6.0 KiB
Go
213 lines
6.0 KiB
Go
// Package dbcli implements control-plane PostgreSQL maintenance CLI (HTTP or local DSN).
|
|
package dbcli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"evobgp/internal/db"
|
|
"evobgp/internal/httpclient"
|
|
"evobgp/internal/pgmonitor"
|
|
)
|
|
|
|
// Run executes db subcommands; args exclude program name and "db".
|
|
func Run(args []string) int {
|
|
if len(args) == 0 {
|
|
printUsage()
|
|
return 2
|
|
}
|
|
switch args[0] {
|
|
case "report":
|
|
return cmdReport(args[1:])
|
|
case "vacuum":
|
|
return cmdMaint(args[1:], "vacuum", "/v1/postgres/vacuum")
|
|
case "analyze":
|
|
return cmdMaint(args[1:], "analyze", "/v1/postgres/analyze")
|
|
case "cleanup":
|
|
return cmdCleanup(args[1:])
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "dbcli: unknown command %q\n", args[0])
|
|
printUsage()
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func printUsage() {
|
|
fmt.Fprintln(os.Stderr, `usage:
|
|
evobgp-api db report [--api-url URL] [--token TOKEN] [--format json]
|
|
evobgp-api db vacuum [--table NAME] [--dry-run] [--api-url URL] [--token TOKEN]
|
|
evobgp-api db analyze [--table NAME] [--dry-run] [--api-url URL] [--token TOKEN]
|
|
evobgp-api db cleanup --policy NAME [--dry-run] [--limit N] [--api-url URL] [--token TOKEN]
|
|
Local break-glass: set EVOBGP_DATABASE_URL (report only uses direct SQL).`)
|
|
}
|
|
|
|
func cmdReport(args []string) int {
|
|
fs := flag.NewFlagSet("report", flag.ExitOnError)
|
|
apiURL := fs.String("api-url", "", "control plane base URL")
|
|
token := fs.String("token", "", "Bearer token (operator)")
|
|
format := fs.String("format", "json", "output format (json)")
|
|
_ = fs.Parse(args)
|
|
|
|
if dsn := strings.TrimSpace(os.Getenv("EVOBGP_DATABASE_URL")); dsn != "" && *apiURL == "" {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
pool, err := db.OpenPostgresPool(ctx, dsn)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
defer pool.Close()
|
|
svc := pgmonitor.NewService(pool)
|
|
ov, err := svc.Overview(ctx)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
return writeJSONStdout(ov, *format)
|
|
}
|
|
if *apiURL == "" || *token == "" {
|
|
fmt.Fprintln(os.Stderr, "report: --api-url and --token required without EVOBGP_DATABASE_URL")
|
|
return 2
|
|
}
|
|
body, err := apiGET(*apiURL, *token, "/v1/monitoring/postgres/overview")
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
var pretty any
|
|
if err := json.Unmarshal(body, &pretty); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
return writeJSONStdout(pretty, *format)
|
|
}
|
|
|
|
func cmdMaint(args []string, _ string, path string) int {
|
|
fs := flag.NewFlagSet("maint", flag.ExitOnError)
|
|
table := fs.String("table", "", "table name")
|
|
dryRun := fs.Bool("dry-run", false, "dry run only")
|
|
apiURL := fs.String("api-url", "", "control plane base URL")
|
|
token := fs.String("token", "", "Bearer token (operator)")
|
|
_ = fs.Parse(args)
|
|
if *apiURL == "" || *token == "" {
|
|
fmt.Fprintln(os.Stderr, "maintenance: --api-url and --token are required")
|
|
return 2
|
|
}
|
|
payload := map[string]any{"dry_run": *dryRun}
|
|
if *table != "" {
|
|
payload["table"] = *table
|
|
}
|
|
body, err := apiPOST(*apiURL, *token, path, payload)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
return writeRawJSON(body)
|
|
}
|
|
|
|
func cmdCleanup(args []string) int {
|
|
fs := flag.NewFlagSet("cleanup", flag.ExitOnError)
|
|
policyID := fs.String("policy-id", "", "maintenance policy UUID")
|
|
dryRun := fs.Bool("dry-run", true, "dry run")
|
|
apiURL := fs.String("api-url", "", "control plane base URL")
|
|
token := fs.String("token", "", "Bearer token (operator)")
|
|
_ = fs.Parse(args)
|
|
if *policyID == "" {
|
|
fmt.Fprintln(os.Stderr, "cleanup: --policy-id is required")
|
|
return 2
|
|
}
|
|
if *apiURL == "" || *token == "" {
|
|
fmt.Fprintln(os.Stderr, "cleanup: --api-url and --token are required")
|
|
return 2
|
|
}
|
|
path := "/v1/maintenance/run"
|
|
if *dryRun {
|
|
path = "/v1/maintenance/dry-run"
|
|
}
|
|
payload := map[string]any{"policy_id": *policyID}
|
|
body, err := apiPOST(*apiURL, *token, path, payload)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
return writeRawJSON(body)
|
|
}
|
|
|
|
func apiGET(base, token, path string) ([]byte, error) {
|
|
u := strings.TrimRight(base, "/") + path
|
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
resp, err := httpclient.DoWithRetry(ctx, httpclient.New(60*time.Second), req, 3)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
b, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("dbcli: GET %s: %s: %s", path, resp.Status, strings.TrimSpace(string(b)))
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func apiPOST(base, token, path string, payload map[string]any) ([]byte, error) {
|
|
b, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u := strings.TrimRight(base, "/") + path
|
|
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
resp, err := httpclient.DoWithRetry(ctx, httpclient.New(60*time.Second), req, 3)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
out, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("dbcli: POST %s: %s: %s", path, resp.Status, strings.TrimSpace(string(out)))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func writeJSONStdout(v any, format string) int {
|
|
if format != "json" {
|
|
fmt.Fprintln(os.Stderr, "only json format supported")
|
|
return 2
|
|
}
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(v); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func writeRawJSON(b []byte) int {
|
|
var v any
|
|
if err := json.Unmarshal(b, &v); err != nil {
|
|
_, _ = os.Stdout.Write(b)
|
|
return 0
|
|
}
|
|
return writeJSONStdout(v, "json")
|
|
}
|