mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-29 12:20:07 +00:00
* fix(backup): harden tenant restore preview and lookup handling * test(pg): fix hook test migration path * test(ci): stabilize race coverage tests * fix(hooks): scope GetByID and allow loopback tests * fix(backup): harden tenant restore replace/new contracts + race-safe SSRF flag - Replace mode no longer deletes the tenants row (FK safe vs excluded diagnostic tables: traces, activity_logs, usage_snapshots, spans, embedding_cache, pairing_requests, paired_devices, channel_pending_messages, cron_run_logs). Metadata is preserved in place. - shouldRestoreTable now excludes tenants for both new and replace modes. - CLI: add validateTenantRestoreFlags guardrail. mode=new requires --new-tenant-slug and rejects --tenant/--tenant-id; upsert/replace warn on stray --new-tenant-slug; invalid --mode values rejected. TAB in help text fixed; flag descriptions clarified. - HTTP: resolveRestoreTarget rejects tenant_id for mode=new regardless of tenant_slug (matches CLI contract). New i18n key MsgRestoreNewModeRejectsTenantID (en/vi/zh). - security/ssrf: allowLoopbackForTest switched to atomic.Bool so concurrent reads from outbound dialers are race-safe. - Polish: vi backup.json key order matches en/zh; TenantRestoreOptions.Mode doc comment documents upsert/replace/new semantics including clone behavior for new. - Tests: unit coverage for validator (12 cases), HTTP guardrails (3 cases), shouldRestoreTable replace branch. Integration test tests/integration/tenant_restore_replace_test.go regression-guards the FK fix using activity_logs seed + DeleteTenantDataForTest helper. --------- Co-authored-by: Viet Tran <viettranx@gmail.com>
73 lines
1.9 KiB
Go
73 lines
1.9 KiB
Go
package cmd
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/google/uuid"
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/config"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
|
)
|
|
|
|
func openTenantBackupDB(cfg *config.Config) (*sql.DB, error) {
|
|
dsn := cfg.Database.PostgresDSN
|
|
if dsn == "" {
|
|
return nil, fmt.Errorf("GOCLAW_POSTGRES_DSN not configured")
|
|
}
|
|
|
|
db, err := sql.Open("pgx", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open db: %w", err)
|
|
}
|
|
return db, nil
|
|
}
|
|
|
|
// resolveTenantForCLI opens the DB, looks up the tenant by slug or UUID,
|
|
// and returns the resolved tenant ID, slug, and an open *sql.DB (caller must close).
|
|
func resolveTenantForCLI(cmd *cobra.Command, cfg *config.Config, rawID, slug string) (uuid.UUID, string, *sql.DB, error) {
|
|
if rawID == "" && slug == "" {
|
|
return uuid.Nil, "", nil, fmt.Errorf("--tenant <slug> or --tenant-id <uuid> is required")
|
|
}
|
|
|
|
db, err := openTenantBackupDB(cfg)
|
|
if err != nil {
|
|
return uuid.Nil, "", nil, err
|
|
}
|
|
|
|
ts := pg.NewPGTenantStore(db)
|
|
|
|
if rawID != "" {
|
|
tid, err := uuid.Parse(rawID)
|
|
if err != nil {
|
|
db.Close()
|
|
return uuid.Nil, "", nil, fmt.Errorf("invalid tenant-id: %w", err)
|
|
}
|
|
tenant, err := ts.GetTenant(cmd.Context(), tid)
|
|
if err != nil {
|
|
db.Close()
|
|
return uuid.Nil, "", nil, fmt.Errorf("tenant not found: %w", err)
|
|
}
|
|
return tenant.ID, tenant.Slug, db, nil
|
|
}
|
|
|
|
tenant, err := ts.GetTenantBySlug(cmd.Context(), slug)
|
|
if err != nil {
|
|
db.Close()
|
|
return uuid.Nil, "", nil, fmt.Errorf("tenant %q not found: %w", slug, err)
|
|
}
|
|
return tenant.ID, tenant.Slug, db, nil
|
|
}
|
|
|
|
// tenantS3Upload uploads a local archive to S3 using the system S3 config.
|
|
func tenantS3Upload(cmd *cobra.Command, cfg *config.Config, archivePath string) error {
|
|
if err := uploadBackupToS3(cmd.Context(), cfg, archivePath, Version); err != nil {
|
|
fmt.Fprintf(os.Stderr, "\nS3 upload failed: %v\n", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|