From 86a3d70455e78b3bdf9c61cfa14a441e6266b763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Eduardo=20Jer=C3=A9z=20Gir=C3=B3n?= Date: Sun, 4 Aug 2024 19:20:44 -0600 Subject: [PATCH] Add restorations service and related functions --- .../restorations/create_restoration.go | 13 ++ .../restorations/create_restoration.sql | 4 + .../restorations/get_restorations_qty.go | 7 + .../restorations/get_restorations_qty.sql | 2 + .../restorations/paginate_restorations.go | 54 ++++++ .../restorations/paginate_restorations.sql | 39 +++++ internal/service/restorations/restorations.go | 31 ++++ .../service/restorations/run_restoration.go | 155 ++++++++++++++++++ .../restorations/update_restoration.go | 13 ++ .../restorations/update_restoration.sql | 8 + internal/service/service.go | 6 + 11 files changed, 332 insertions(+) create mode 100644 internal/service/restorations/create_restoration.go create mode 100644 internal/service/restorations/create_restoration.sql create mode 100644 internal/service/restorations/get_restorations_qty.go create mode 100644 internal/service/restorations/get_restorations_qty.sql create mode 100644 internal/service/restorations/paginate_restorations.go create mode 100644 internal/service/restorations/paginate_restorations.sql create mode 100644 internal/service/restorations/restorations.go create mode 100644 internal/service/restorations/run_restoration.go create mode 100644 internal/service/restorations/update_restoration.go create mode 100644 internal/service/restorations/update_restoration.sql diff --git a/internal/service/restorations/create_restoration.go b/internal/service/restorations/create_restoration.go new file mode 100644 index 0000000..b1ce32e --- /dev/null +++ b/internal/service/restorations/create_restoration.go @@ -0,0 +1,13 @@ +package restorations + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +func (s *Service) CreateRestoration( + ctx context.Context, params dbgen.RestorationsServiceCreateRestorationParams, +) (dbgen.Restoration, error) { + return s.dbgen.RestorationsServiceCreateRestoration(ctx, params) +} diff --git a/internal/service/restorations/create_restoration.sql b/internal/service/restorations/create_restoration.sql new file mode 100644 index 0000000..1eb655f --- /dev/null +++ b/internal/service/restorations/create_restoration.sql @@ -0,0 +1,4 @@ +-- name: RestorationsServiceCreateRestoration :one +INSERT INTO restorations (execution_id, database_id, status, message) +VALUES (@execution_id, @database_id, @status, @message) +RETURNING *; diff --git a/internal/service/restorations/get_restorations_qty.go b/internal/service/restorations/get_restorations_qty.go new file mode 100644 index 0000000..118f6fd --- /dev/null +++ b/internal/service/restorations/get_restorations_qty.go @@ -0,0 +1,7 @@ +package restorations + +import "context" + +func (s *Service) GetRestorationsQty(ctx context.Context) (int64, error) { + return s.dbgen.RestorationsServiceGetRestorationsQty(ctx) +} diff --git a/internal/service/restorations/get_restorations_qty.sql b/internal/service/restorations/get_restorations_qty.sql new file mode 100644 index 0000000..f12fd35 --- /dev/null +++ b/internal/service/restorations/get_restorations_qty.sql @@ -0,0 +1,2 @@ +-- name: RestorationsServiceGetRestorationsQty :one +SELECT COUNT(*) FROM restorations; diff --git a/internal/service/restorations/paginate_restorations.go b/internal/service/restorations/paginate_restorations.go new file mode 100644 index 0000000..0a25f74 --- /dev/null +++ b/internal/service/restorations/paginate_restorations.go @@ -0,0 +1,54 @@ +package restorations + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/google/uuid" +) + +type PaginateRestorationsParams struct { + Page int + Limit int + ExecutionFilter uuid.NullUUID + DatabaseFilter uuid.NullUUID +} + +func (s *Service) PaginateRestorations( + ctx context.Context, params PaginateRestorationsParams, +) (paginateutil.PaginateResponse, []dbgen.RestorationsServicePaginateRestorationsRow, error) { + page := max(params.Page, 1) + limit := min(max(params.Limit, 1), 100) + + count, err := s.dbgen.RestorationsServicePaginateRestorationsCount( + ctx, dbgen.RestorationsServicePaginateRestorationsCountParams{ + ExecutionID: params.ExecutionFilter, + DatabaseID: params.DatabaseFilter, + }, + ) + if err != nil { + return paginateutil.PaginateResponse{}, nil, err + } + + paginateParams := paginateutil.PaginateParams{ + Page: page, + Limit: limit, + } + offset := paginateutil.CreateOffsetFromParams(paginateParams) + paginateResponse := paginateutil.CreatePaginateResponse(paginateParams, int(count)) + + restorations, err := s.dbgen.RestorationsServicePaginateRestorations( + ctx, dbgen.RestorationsServicePaginateRestorationsParams{ + ExecutionID: params.ExecutionFilter, + DatabaseID: params.DatabaseFilter, + Limit: int32(params.Limit), + Offset: int32(offset), + }, + ) + if err != nil { + return paginateutil.PaginateResponse{}, nil, err + } + + return paginateResponse, restorations, nil +} diff --git a/internal/service/restorations/paginate_restorations.sql b/internal/service/restorations/paginate_restorations.sql new file mode 100644 index 0000000..d83bb24 --- /dev/null +++ b/internal/service/restorations/paginate_restorations.sql @@ -0,0 +1,39 @@ +-- name: RestorationsServicePaginateRestorationsCount :one +SELECT COUNT(restorations.*) +FROM restorations +INNER JOIN executions ON executions.id = restorations.execution_id +LEFT JOIN databases ON databases.id = restorations.database_id +WHERE +( + sqlc.narg('execution_id')::UUID IS NULL + OR + restorations.execution_id = sqlc.narg('execution_id')::UUID +) +AND +( + sqlc.narg('database_id')::UUID IS NULL + OR + restorations.database_id = sqlc.narg('database_id')::UUID +); + +-- name: RestorationsServicePaginateRestorations :many +SELECT + restorations.*, + databases.name AS database_name +FROM restorations +INNER JOIN executions ON executions.id = restorations.execution_id +LEFT JOIN databases ON databases.id = restorations.database_id +WHERE +( + sqlc.narg('execution_id')::UUID IS NULL + OR + restorations.execution_id = sqlc.narg('execution_id')::UUID +) +AND +( + sqlc.narg('database_id')::UUID IS NULL + OR + restorations.database_id = sqlc.narg('database_id')::UUID +) +ORDER BY restorations.started_at DESC +LIMIT sqlc.arg('limit') OFFSET sqlc.arg('offset'); diff --git a/internal/service/restorations/restorations.go b/internal/service/restorations/restorations.go new file mode 100644 index 0000000..a155895 --- /dev/null +++ b/internal/service/restorations/restorations.go @@ -0,0 +1,31 @@ +package restorations + +import ( + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/integration" + "github.com/eduardolat/pgbackweb/internal/service/databases" + "github.com/eduardolat/pgbackweb/internal/service/destinations" + "github.com/eduardolat/pgbackweb/internal/service/executions" +) + +type Service struct { + dbgen *dbgen.Queries + ints *integration.Integration + executionsService *executions.Service + databasesService *databases.Service + destinationsService *destinations.Service +} + +func New( + dbgen *dbgen.Queries, ints *integration.Integration, + executionsService *executions.Service, databasesService *databases.Service, + destinationsService *destinations.Service, +) *Service { + return &Service{ + dbgen: dbgen, + ints: ints, + executionsService: executionsService, + databasesService: databasesService, + destinationsService: destinationsService, + } +} diff --git a/internal/service/restorations/run_restoration.go b/internal/service/restorations/run_restoration.go new file mode 100644 index 0000000..78edbd8 --- /dev/null +++ b/internal/service/restorations/run_restoration.go @@ -0,0 +1,155 @@ +package restorations + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/google/uuid" +) + +// RunRestoration runs a backup restoration +func (s *Service) RunRestoration( + ctx context.Context, + executionID uuid.UUID, + databaseID uuid.NullUUID, + connString string, +) error { + updateRes := func(params dbgen.RestorationsServiceUpdateRestorationParams) error { + _, err := s.dbgen.RestorationsServiceUpdateRestoration( + ctx, params, + ) + return err + } + + logError := func(err error) { + dbID := "empty" + if databaseID.Valid { + dbID = databaseID.UUID.String() + } + logger.Error("error running restoration", logger.KV{ + "execution_id": executionID.String(), + "database_id": dbID, + "error": err.Error(), + }) + } + + res, err := s.CreateRestoration(ctx, dbgen.RestorationsServiceCreateRestorationParams{ + ExecutionID: executionID, + DatabaseID: databaseID, + Status: "running", + }) + if err != nil { + logError(err) + return err + } + + if !databaseID.Valid && connString == "" { + err := fmt.Errorf("database_id or connection_string must be provided") + logError(err) + return updateRes(dbgen.RestorationsServiceUpdateRestorationParams{ + ID: res.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + + execution, err := s.executionsService.GetExecution(ctx, executionID) + if err != nil { + logError(err) + return updateRes(dbgen.RestorationsServiceUpdateRestorationParams{ + ID: res.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + + if execution.Status != "success" || !execution.Path.Valid { + err := fmt.Errorf("backup execution must be successful") + logError(err) + return updateRes(dbgen.RestorationsServiceUpdateRestorationParams{ + ID: res.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + + if databaseID.Valid { + db, err := s.databasesService.GetDatabase(ctx, databaseID.UUID) + if err != nil { + logError(err) + return updateRes(dbgen.RestorationsServiceUpdateRestorationParams{ + ID: res.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + connString = db.DecryptedConnectionString + } + + pgVersion, err := s.ints.PGClient.ParseVersion(execution.DatabasePgVersion) + if err != nil { + logError(err) + return updateRes(dbgen.RestorationsServiceUpdateRestorationParams{ + ID: res.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + + err = s.ints.PGClient.Ping(pgVersion, connString) + if err != nil { + logError(err) + return updateRes(dbgen.RestorationsServiceUpdateRestorationParams{ + ID: res.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + + isLocal, zipURLOrPath, err := s.executionsService.GetExecutionDownloadLinkOrPath( + ctx, executionID, + ) + if err != nil { + logError(err) + return updateRes(dbgen.RestorationsServiceUpdateRestorationParams{ + ID: res.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + + err = s.ints.PGClient.RestoreZip( + pgVersion, connString, isLocal, zipURLOrPath, + ) + if err != nil { + logError(err) + return updateRes(dbgen.RestorationsServiceUpdateRestorationParams{ + ID: res.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + + logger.Info("backup restored successfully", logger.KV{ + "restoration_id": res.ID.String(), + "execution_id": executionID.String(), + }) + return updateRes(dbgen.RestorationsServiceUpdateRestorationParams{ + ID: res.ID, + Status: sql.NullString{Valid: true, String: "success"}, + Message: sql.NullString{Valid: true, String: "Backup restored successfully"}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) +} diff --git a/internal/service/restorations/update_restoration.go b/internal/service/restorations/update_restoration.go new file mode 100644 index 0000000..965f0b2 --- /dev/null +++ b/internal/service/restorations/update_restoration.go @@ -0,0 +1,13 @@ +package restorations + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +func (s *Service) UpdateRestoration( + ctx context.Context, params dbgen.RestorationsServiceUpdateRestorationParams, +) (dbgen.Restoration, error) { + return s.dbgen.RestorationsServiceUpdateRestoration(ctx, params) +} diff --git a/internal/service/restorations/update_restoration.sql b/internal/service/restorations/update_restoration.sql new file mode 100644 index 0000000..1505ee7 --- /dev/null +++ b/internal/service/restorations/update_restoration.sql @@ -0,0 +1,8 @@ +-- name: RestorationsServiceUpdateRestoration :one +UPDATE restorations +SET + status = COALESCE(sqlc.narg('status'), status), + message = COALESCE(sqlc.narg('message'), message), + finished_at = COALESCE(sqlc.narg('finished_at'), finished_at) +WHERE id = @id +RETURNING *; diff --git a/internal/service/service.go b/internal/service/service.go index da12a18..2f3abc1 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -10,6 +10,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/databases" "github.com/eduardolat/pgbackweb/internal/service/destinations" "github.com/eduardolat/pgbackweb/internal/service/executions" + "github.com/eduardolat/pgbackweb/internal/service/restorations" "github.com/eduardolat/pgbackweb/internal/service/users" ) @@ -20,6 +21,7 @@ type Service struct { DestinationsService *destinations.Service ExecutionsService *executions.Service UsersService *users.Service + RestorationsService *restorations.Service } func New( @@ -32,6 +34,9 @@ func New( executionsService := executions.New(env, dbgen, ints) usersService := users.New(dbgen) backupsService := backups.New(dbgen, cr, executionsService) + restorationsService := restorations.New( + dbgen, ints, executionsService, databasesService, destinationsService, + ) return &Service{ AuthService: authService, @@ -40,5 +45,6 @@ func New( DestinationsService: destinationsService, ExecutionsService: executionsService, UsersService: usersService, + RestorationsService: restorationsService, } }