Skip to content

Commit

Permalink
Merge pull request #88 from sapcc/restore-as-superuser
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperSandro2000 authored Jan 5, 2024
2 parents b62f3eb + fcb023c commit 21fe2a2
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 5 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# backup-tools

This repo contains the PostgreSQL database backup tool.

Use this through the [pgbackup Helm chart](https://github.com/sapcc/helm-charts/tree/master/common/pgbackup).
31 changes: 28 additions & 3 deletions backup-tools.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env sh
#shellcheck disable=SC3040 # sh in Alpine does support pipefail
#!/usr/bin/env ash
set -euo pipefail

usage() {
Expand Down Expand Up @@ -55,7 +54,33 @@ cmd_restore() {
if [ "$BACKUP_ID" = "unset" ]; then
usage && exit 1
fi
do_curl POST "/v1/restore/${BACKUP_ID}"
if [ "${PGSQL_USER:-unset}" = backup ]; then
# when using the postgresql-ng chart, this container has a restricted user
# with read-only privileges that are enough for creating backups, but not
# for restoring them
echo "Please enter credentials for the PostgreSQL superuser to proceed."
echo -n "Username [postgres]: "
read -r PG_SUPERUSER_NAME
if [ "$PG_SUPERUSER_NAME" = "" ]; then
PG_SUPERUSER_NAME=postgres
fi
echo -n "Password (get this from the respective Kubernetes Secret): "
read -r PG_SUPERUSER_PASSWORD
if [ "$PG_SUPERUSER_PASSWORD" = "" ]; then
echo "ERROR: No password given." >&2
exit 1
fi
# to ensure that special characters in the input do not break the JSON payload,
# use jq to convert the raw input into string literals
PG_SUPERUSER_NAME_JSON="$(echo -n "$PG_SUPERUSER_NAME" | jq --raw-input --slurp .)"
PG_SUPERUSER_PASSWORD_JSON="$(echo -n "$PG_SUPERUSER_PASSWORD" | jq --raw-input --slurp .)"
do_curl POST "/v1/restore/${BACKUP_ID}" -d "{\"superuser\":{\"username\":$PG_SUPERUSER_NAME_JSON,\"password\":$PG_SUPERUSER_PASSWORD_JSON}}"
else
# when using the legacy postgres chart, this container uses the superuser
# credentials, so restore works directly
# TODO: remove this case once everyone has been migrated to postgresql-ng
do_curl POST "/v1/restore/${BACKUP_ID}" -d "{\"superuser\":null}"
fi
}

case "${1:-unset}" in
Expand Down
19 changes: 18 additions & 1 deletion internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
package api

import (
"encoding/json"
"io"
"net/http"

"github.com/gorilla/mux"
Expand Down Expand Up @@ -112,6 +114,21 @@ func (a API) handleGetBackups(w http.ResponseWriter, r *http.Request) {
func (a API) handlePostRestore(w http.ResponseWriter, r *http.Request) {
httpapi.IdentifyEndpoint(r, "/v1/restore/:id")

//superuser credentials may be supplied in the request body
buf, err := io.ReadAll(r.Body)
if respondwith.ErrorText(w, err) {
return
}
var req struct {
SuperUser *restore.SuperUserCredentials `json:"superuser"`
}
if len(buf) > 0 {
err = json.Unmarshal(buf, &req)
if respondwith.ErrorText(w, err) {
return
}
}

//find backup
backups, err := restore.ListRestorableBackups(a.Config)
if respondwith.ErrorText(w, err) {
Expand All @@ -124,7 +141,7 @@ func (a API) handlePostRestore(w http.ResponseWriter, r *http.Request) {
}

//run restore
err = bkp.Restore(a.Config)
err = bkp.Restore(a.Config, req.SuperUser)
if err == nil {
http.Error(w, "backup restored successfully", http.StatusOK)
} else {
Expand Down
19 changes: 18 additions & 1 deletion internal/restore/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,22 @@ import (
"fmt"
"os"
"os/exec"
"slices"

"github.com/kballard/go-shellquote"
"github.com/sapcc/go-bits/logg"

"github.com/sapcc/backup-tools/internal/core"
)

// SuperUserCredentials can be used to override the default credentials during Restore().
type SuperUserCredentials struct {
Username string `json:"username"`
Password string `json:"password"`
}

// Restore downloads and restores this backup into the Postgres.
func (bkp RestorableBackup) Restore(cfg *core.Configuration) error {
func (bkp RestorableBackup) Restore(cfg *core.Configuration, suCreds *SuperUserCredentials) error {
//download dumps
dirPath := fmt.Sprintf("/tmp/restore-%s", bkp.ID)
err := os.MkdirAll(dirPath, 0777)
Expand All @@ -43,12 +50,22 @@ func (bkp RestorableBackup) Restore(cfg *core.Configuration) error {
return err
}

//override config if necessary
if suCreds != nil {
cloned := *cfg
cloned.PgUsername = suCreds.Username
cfg = &cloned
}

//playback dumps
for _, filePath := range filePaths {
cmd := exec.Command("psql", cfg.ArgsForPsql("-a", "-f", filePath)...) //nolint:gosec // input is user supplied and self executed
logg.Info(">> " + shellquote.Join(cmd.Args...))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if suCreds != nil {
cmd.Env = append(slices.Clone(os.Environ()), "PGPASSWORD="+suCreds.Password)
}
err := cmd.Run()
if err != nil {
return fmt.Errorf("could not import %s with psql: %w", filePath, err)
Expand Down

0 comments on commit 21fe2a2

Please sign in to comment.