influxdb/http/backup_service.go

200 lines
5.7 KiB
Go

package http
import (
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
"time"
"github.com/NYTimes/gziphandler"
"github.com/influxdata/httprouter"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorizer"
"github.com/influxdata/influxdb/v2/kit/platform/errors"
"github.com/influxdata/influxdb/v2/kit/tracing"
"go.uber.org/zap"
)
// BackupBackend is all services and associated parameters required to construct the BackupHandler.
type BackupBackend struct {
Logger *zap.Logger
errors.HTTPErrorHandler
BackupService influxdb.BackupService
SqlBackupRestoreService influxdb.SqlBackupRestoreService
BucketManifestWriter influxdb.BucketManifestWriter
}
// 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,
SqlBackupRestoreService: b.SqlBackupRestoreService,
BucketManifestWriter: b.BucketManifestWriter,
}
}
// BackupHandler is http handler for backup service.
type BackupHandler struct {
*httprouter.Router
errors.HTTPErrorHandler
Logger *zap.Logger
BackupService influxdb.BackupService
SqlBackupRestoreService influxdb.SqlBackupRestoreService
BucketManifestWriter influxdb.BucketManifestWriter
}
const (
prefixBackup = "/api/v2/backup"
backupKVStorePath = prefixBackup + "/kv"
backupShardPath = prefixBackup + "/shards/:shardID"
backupMetadataPath = prefixBackup + "/metadata"
)
// 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,
SqlBackupRestoreService: b.SqlBackupRestoreService,
BucketManifestWriter: b.BucketManifestWriter,
}
h.HandlerFunc(http.MethodGet, backupKVStorePath, h.handleBackupKVStore) // Deprecated
h.Handler(http.MethodGet, backupShardPath, gziphandler.GzipHandler(http.HandlerFunc(h.handleBackupShard)))
h.Handler(http.MethodGet, backupMetadataPath, gziphandler.GzipHandler(h.requireOperPermissions(http.HandlerFunc(h.handleBackupMetadata))))
return h
}
// requireOperPermissions returns an "unauthorized" response for requests that do not have OperPermissions.
// This is needed for the handleBackupMetadata handler, which sets a header prior to
// accessing any methods on the BackupService which would also return an "authorized" response.
func (h *BackupHandler) requireOperPermissions(next http.Handler) http.HandlerFunc {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := authorizer.IsAllowedAll(ctx, influxdb.OperPermissions()); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func (h *BackupHandler) handleBackupKVStore(w http.ResponseWriter, r *http.Request) {
span, r := tracing.ExtractFromHTTPRequest(r, "BackupHandler.handleBackupKVStore")
defer span.Finish()
ctx := r.Context()
if err := h.BackupService.BackupKVStore(ctx, w); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
}
func (h *BackupHandler) handleBackupShard(w http.ResponseWriter, r *http.Request) {
span, r := tracing.ExtractFromHTTPRequest(r, "BackupHandler.handleBackupShard")
defer span.Finish()
ctx := r.Context()
params := httprouter.ParamsFromContext(ctx)
shardID, err := strconv.ParseUint(params.ByName("shardID"), 10, 64)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
var since time.Time
if s := r.URL.Query().Get("since"); s != "" {
if since, err = time.ParseInLocation(time.RFC3339, s, time.UTC); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
}
if err := h.BackupService.BackupShard(ctx, w, shardID, since); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
}
func (h *BackupHandler) handleBackupMetadata(w http.ResponseWriter, r *http.Request) {
span, r := tracing.ExtractFromHTTPRequest(r, "BackupHandler.handleBackupMetadata")
defer span.Finish()
ctx := r.Context()
// Lock the sqlite and bolt databases prior to writing the response to prevent
// data inconsistencies.
h.BackupService.RLockKVStore()
defer h.BackupService.RUnlockKVStore()
h.SqlBackupRestoreService.RLockSqlStore()
defer h.SqlBackupRestoreService.RUnlockSqlStore()
dataWriter := multipart.NewWriter(w)
w.Header().Set("Content-Type", "multipart/mixed; boundary="+dataWriter.Boundary())
parts := []struct {
contentType string
contentDisposition string
writeFn func(io.Writer) error
}{
{
"application/octet-stream",
fmt.Sprintf("attachment; name=%q", "kv"),
func(fw io.Writer) error {
return h.BackupService.BackupKVStore(ctx, fw)
},
},
{
"application/octet-stream",
fmt.Sprintf("attachment; name=%q", "sql"),
func(fw io.Writer) error {
return h.SqlBackupRestoreService.BackupSqlStore(ctx, fw)
},
},
{
"application/json; charset=utf-8",
fmt.Sprintf("attachment; name=%q", "buckets"),
func(fw io.Writer) error {
return h.BucketManifestWriter.WriteManifest(ctx, fw)
},
},
}
for _, p := range parts {
pw, err := dataWriter.CreatePart(map[string][]string{
"Content-Type": {p.contentType},
"Content-Disposition": {p.contentDisposition},
})
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
if err := p.writeFn(pw); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
}
if err := dataWriter.Close(); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
}