2020-01-21 22:22:45 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/influxdata/httprouter"
|
2020-04-03 17:39:20 +00:00
|
|
|
"github.com/influxdata/influxdb/v2"
|
|
|
|
"github.com/influxdata/influxdb/v2/bolt"
|
|
|
|
"github.com/influxdata/influxdb/v2/internal/fs"
|
|
|
|
"github.com/influxdata/influxdb/v2/kit/tracing"
|
2020-01-21 22:22:45 +00:00
|
|
|
"go.uber.org/multierr"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
)
|
|
|
|
|
2020-03-10 15:53:41 +00:00
|
|
|
// DefaultTokenFile is deprecated, and will be only used for migration.
|
2020-01-21 22:22:45 +00:00
|
|
|
const DefaultTokenFile = "credentials"
|
|
|
|
|
2020-03-10 15:53:41 +00:00
|
|
|
// DefaultConfigsFile stores cli credentials and hosts.
|
|
|
|
const DefaultConfigsFile = "configs"
|
|
|
|
|
2020-01-21 22:22:45 +00:00
|
|
|
// BackupBackend is all services and associated parameters required to construct the BackupHandler.
|
|
|
|
type BackupBackend struct {
|
|
|
|
Logger *zap.Logger
|
|
|
|
influxdb.HTTPErrorHandler
|
|
|
|
|
|
|
|
BackupService influxdb.BackupService
|
|
|
|
KVBackupService influxdb.KVBackupService
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewBackupBackend returns a new instance of BackupBackend.
|
|
|
|
func NewBackupBackend(b *APIBackend) *BackupBackend {
|
|
|
|
return &BackupBackend{
|
|
|
|
Logger: b.Logger.With(zap.String("handler", "backup")),
|
|
|
|
|
|
|
|
HTTPErrorHandler: b.HTTPErrorHandler,
|
|
|
|
BackupService: b.BackupService,
|
|
|
|
KVBackupService: b.KVBackupService,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-10 15:53:41 +00:00
|
|
|
// BackupHandler is http handler for backup service.
|
2020-01-21 22:22:45 +00:00
|
|
|
type BackupHandler struct {
|
|
|
|
*httprouter.Router
|
|
|
|
influxdb.HTTPErrorHandler
|
|
|
|
Logger *zap.Logger
|
|
|
|
|
|
|
|
BackupService influxdb.BackupService
|
|
|
|
KVBackupService influxdb.KVBackupService
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
prefixBackup = "/api/v2/backup"
|
|
|
|
backupIDParamName = "backup_id"
|
|
|
|
backupFileParamName = "backup_file"
|
|
|
|
backupFilePath = prefixBackup + "/:" + backupIDParamName + "/file/:" + backupFileParamName
|
|
|
|
|
|
|
|
httpClientTimeout = time.Hour
|
|
|
|
)
|
|
|
|
|
|
|
|
func composeBackupFilePath(backupID int, backupFile string) string {
|
|
|
|
return path.Join(prefixBackup, fmt.Sprint(backupID), "file", fmt.Sprint(backupFile))
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewBackupHandler creates a new handler at /api/v2/backup to receive backup requests.
|
|
|
|
func NewBackupHandler(b *BackupBackend) *BackupHandler {
|
|
|
|
h := &BackupHandler{
|
|
|
|
HTTPErrorHandler: b.HTTPErrorHandler,
|
|
|
|
Router: NewRouter(b.HTTPErrorHandler),
|
|
|
|
Logger: b.Logger,
|
|
|
|
BackupService: b.BackupService,
|
|
|
|
KVBackupService: b.KVBackupService,
|
|
|
|
}
|
|
|
|
|
|
|
|
h.HandlerFunc(http.MethodPost, prefixBackup, h.handleCreate)
|
|
|
|
h.HandlerFunc(http.MethodGet, backupFilePath, h.handleFetchFile)
|
|
|
|
|
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
|
|
|
type backup struct {
|
|
|
|
ID int `json:"id,omitempty"`
|
|
|
|
Files []string `json:"files,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *BackupHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
|
|
|
|
span, r := tracing.ExtractFromHTTPRequest(r, "BackupHandler.handleCreate")
|
|
|
|
defer span.Finish()
|
|
|
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
id, files, err := h.BackupService.CreateBackup(ctx)
|
|
|
|
if err != nil {
|
|
|
|
h.HandleHTTPError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
internalBackupPath := h.BackupService.InternalBackupPath(id)
|
|
|
|
|
|
|
|
boltPath := filepath.Join(internalBackupPath, bolt.DefaultFilename)
|
|
|
|
boltFile, err := os.OpenFile(boltPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0660)
|
|
|
|
if err != nil {
|
|
|
|
err = multierr.Append(err, os.RemoveAll(internalBackupPath))
|
|
|
|
h.HandleHTTPError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = h.KVBackupService.Backup(ctx, boltFile); err != nil {
|
|
|
|
err = multierr.Append(err, os.RemoveAll(internalBackupPath))
|
|
|
|
h.HandleHTTPError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
files = append(files, bolt.DefaultFilename)
|
|
|
|
|
2020-02-14 17:19:58 +00:00
|
|
|
credsExist, err := h.backupCredentials(internalBackupPath)
|
2020-01-21 22:22:45 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
h.HandleHTTPError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-02-14 17:19:58 +00:00
|
|
|
if credsExist {
|
2020-03-10 15:53:41 +00:00
|
|
|
files = append(files, DefaultConfigsFile)
|
2020-01-21 22:22:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
b := backup{
|
|
|
|
ID: id,
|
|
|
|
Files: files,
|
|
|
|
}
|
|
|
|
if err = json.NewEncoder(w).Encode(&b); err != nil {
|
|
|
|
err = multierr.Append(err, os.RemoveAll(internalBackupPath))
|
|
|
|
h.HandleHTTPError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-14 17:19:58 +00:00
|
|
|
func (h *BackupHandler) backupCredentials(internalBackupPath string) (bool, error) {
|
2020-03-10 15:53:41 +00:00
|
|
|
credBackupPath := filepath.Join(internalBackupPath, DefaultConfigsFile)
|
2020-02-14 17:19:58 +00:00
|
|
|
|
2020-03-10 15:53:41 +00:00
|
|
|
credPath, err := defaultConfigsPath()
|
2020-02-14 17:19:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
token, err := ioutil.ReadFile(credPath)
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
|
|
return false, err
|
|
|
|
} else if os.IsNotExist(err) {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := ioutil.WriteFile(credBackupPath, []byte(token), 0600); err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2020-01-21 22:22:45 +00:00
|
|
|
func (h *BackupHandler) handleFetchFile(w http.ResponseWriter, r *http.Request) {
|
|
|
|
span, r := tracing.ExtractFromHTTPRequest(r, "BackupHandler.handleFetchFile")
|
|
|
|
defer span.Finish()
|
|
|
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
params := httprouter.ParamsFromContext(ctx)
|
|
|
|
backupID, err := strconv.Atoi(params.ByName("backup_id"))
|
|
|
|
if err != nil {
|
|
|
|
h.HandleHTTPError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
backupFile := params.ByName("backup_file")
|
|
|
|
|
|
|
|
if err = h.BackupService.FetchBackupFile(ctx, backupID, backupFile, w); err != nil {
|
|
|
|
h.HandleHTTPError(ctx, err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// BackupService is the client implementation of influxdb.BackupService.
|
|
|
|
type BackupService struct {
|
|
|
|
Addr string
|
|
|
|
Token string
|
|
|
|
InsecureSkipVerify bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *BackupService) CreateBackup(ctx context.Context) (int, []string, error) {
|
|
|
|
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
|
|
defer span.Finish()
|
|
|
|
|
|
|
|
u, err := NewURL(s.Addr, prefixBackup)
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPost, u.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
SetToken(s.Token, req)
|
|
|
|
req = req.WithContext(ctx)
|
|
|
|
|
|
|
|
hc := NewClient(u.Scheme, s.InsecureSkipVerify)
|
|
|
|
hc.Timeout = httpClientTimeout
|
|
|
|
resp, err := hc.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if err := CheckError(resp); err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var b backup
|
|
|
|
if err = json.NewDecoder(resp.Body).Decode(&b); err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return b.ID, b.Files, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *BackupService) FetchBackupFile(ctx context.Context, backupID int, backupFile string, w io.Writer) error {
|
|
|
|
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
|
|
defer span.Finish()
|
|
|
|
|
|
|
|
u, err := NewURL(s.Addr, composeBackupFilePath(backupID, backupFile))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
SetToken(s.Token, req)
|
|
|
|
req = req.WithContext(ctx)
|
|
|
|
|
|
|
|
hc := NewClient(u.Scheme, s.InsecureSkipVerify)
|
|
|
|
hc.Timeout = httpClientTimeout
|
|
|
|
resp, err := hc.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if err := CheckError(resp); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = io.Copy(w, resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-10 15:53:41 +00:00
|
|
|
func defaultConfigsPath() (string, error) {
|
2020-01-21 22:22:45 +00:00
|
|
|
dir, err := fs.InfluxDir()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2020-03-10 15:53:41 +00:00
|
|
|
return filepath.Join(dir, DefaultConfigsFile), nil
|
2020-01-21 22:22:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *BackupService) InternalBackupPath(backupID int) string {
|
|
|
|
panic("internal method not implemented here")
|
|
|
|
}
|