mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-15 08:41:02 +00:00
This fixes the OriginalIP and RequestUserID in the main featurebase package, and the Access and Refresh tokens, the UserInfo, and the []string of Indexes passed with context.Context(s) in the authn package. An empty struct was used for all of these keys (and relevant helper functions we added) to avoid allocations where possible while still using the context functionality. Some of the logic in the server.GetIndexes function was fixed.
269 lines
6.5 KiB
Go
269 lines
6.5 KiB
Go
// Copyright 2022 Molecula Corp. All rights reserved.
|
|
package ctl
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
gohttp "net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
pilosa "github.com/molecula/featurebase/v3"
|
|
"github.com/molecula/featurebase/v3/authn"
|
|
"github.com/molecula/featurebase/v3/disco"
|
|
"github.com/molecula/featurebase/v3/server"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// RestoreTarCommand represents a command for restoring a backup to
|
|
type RestoreTarCommand struct {
|
|
tlsConfig *tls.Config
|
|
|
|
Host string
|
|
|
|
// Filepath to the backup file.
|
|
Path string
|
|
|
|
// Amount of time after first failed request to continue retrying.
|
|
RetryPeriod time.Duration `json:"retry-period"`
|
|
|
|
// Host:port on which to listen for pprof.
|
|
Pprof string `json:"pprof"`
|
|
|
|
// Reusable client.
|
|
client *pilosa.InternalClient
|
|
|
|
// Standard input/output
|
|
*pilosa.CmdIO
|
|
|
|
TLS server.TLSConfig
|
|
|
|
AuthToken string
|
|
}
|
|
|
|
// NewRestoreTarCommand returns a new instance of RestoreTarCommand.
|
|
func NewRestoreTarCommand(stdin io.Reader, stdout, stderr io.Writer) *RestoreTarCommand {
|
|
return &RestoreTarCommand{
|
|
CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr),
|
|
RetryPeriod: time.Second * 30,
|
|
Pprof: "localhost:0",
|
|
}
|
|
}
|
|
|
|
// Run executes the restore.
|
|
func (cmd *RestoreTarCommand) Run(ctx context.Context) (err error) {
|
|
logger := cmd.Logger()
|
|
close, err := startProfilingServer(cmd.Pprof, logger)
|
|
if err != nil {
|
|
return errors.Wrap(err, "starting profiling server")
|
|
}
|
|
defer close()
|
|
|
|
// Validate arguments.
|
|
if cmd.Path == "" {
|
|
return fmt.Errorf("-s flag required")
|
|
}
|
|
useStdin := cmd.Path == "-"
|
|
|
|
var f *os.File
|
|
// read from Stdin if path specified as -
|
|
if useStdin {
|
|
f = os.Stdin
|
|
} else {
|
|
f, err = os.Open(cmd.Path)
|
|
if err != nil {
|
|
return (err)
|
|
}
|
|
defer f.Close()
|
|
}
|
|
|
|
// Parse TLS configuration for node-specific clients.
|
|
tls := cmd.TLSConfiguration()
|
|
if cmd.tlsConfig, err = server.GetTLSConfig(&tls, logger); err != nil {
|
|
return fmt.Errorf("parsing tls config: %w", err)
|
|
}
|
|
// Create a client to the server.
|
|
client, err := commandClient(cmd, pilosa.WithClientRetryPeriod(cmd.RetryPeriod))
|
|
if err != nil {
|
|
return fmt.Errorf("creating client: %w", err)
|
|
}
|
|
cmd.client = client
|
|
|
|
if cmd.AuthToken != "" {
|
|
ctx = authn.WithAccessToken(ctx, "Bearer "+cmd.AuthToken)
|
|
}
|
|
|
|
var tarReader *tar.Reader
|
|
if strings.HasSuffix(cmd.Path, "gz") {
|
|
gzf, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tarReader = tar.NewReader(gzf)
|
|
} else {
|
|
tarReader = tar.NewReader(f)
|
|
}
|
|
nodes, err := cmd.client.Nodes(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var primary *disco.Node
|
|
for _, node := range nodes {
|
|
if node.IsPrimary {
|
|
primary = node
|
|
break
|
|
}
|
|
|
|
}
|
|
if primary == nil {
|
|
return errors.New("no primary")
|
|
}
|
|
c := &gohttp.Client{}
|
|
for {
|
|
header, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
record := strings.Split(header.Name, "/")
|
|
if len(record) == 1 {
|
|
switch record[0] {
|
|
case "schema":
|
|
logger.Printf("Load Schema")
|
|
url := primary.URI.Path("/schema")
|
|
_, err = c.Post(url, "application/json", tarReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "idalloc":
|
|
logger.Printf("Load ids")
|
|
url := primary.URI.Path("/internal/idalloc/restore")
|
|
_, err = c.Post(url, "application/octet-stream", tarReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return err
|
|
|
|
}
|
|
continue
|
|
}
|
|
indexName := record[1]
|
|
switch record[2] {
|
|
case "shards":
|
|
shard, err := strconv.ParseUint(record[3], 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fragmentNodes, err := cmd.client.FragmentNodes(ctx, indexName, shard)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot determine fragmentNodes: %w", err)
|
|
} else if len(fragmentNodes) == 0 {
|
|
return fmt.Errorf("no fragmentNodes available")
|
|
}
|
|
|
|
shardBytes, err := io.ReadAll(tarReader) // this feels wrong but works for now
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g, _ := errgroup.WithContext(ctx)
|
|
for _, node := range fragmentNodes {
|
|
node := node
|
|
g.Go(func() error {
|
|
client := &gohttp.Client{}
|
|
rd := bytes.NewReader(shardBytes)
|
|
logger.Printf("shard %v %v", shard, indexName)
|
|
url := node.URI.Path(fmt.Sprintf("/internal/restore/%v/%v", indexName, shard))
|
|
_, err = client.Post(url, "application/octet-stream", rd)
|
|
return err
|
|
})
|
|
}
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
case "translate":
|
|
partitionID, err := strconv.Atoi(record[3])
|
|
logger.Printf("column keys %v (%v)", indexName, partitionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
partitionNodes, err := cmd.client.PartitionNodes(ctx, partitionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
shardBytes, err := io.ReadAll(tarReader) // this feels wrong but works for now
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g, _ := errgroup.WithContext(ctx)
|
|
for _, node := range partitionNodes {
|
|
node := node
|
|
g.Go(func() error {
|
|
// rd := bytes.NewReader(shardBytes)
|
|
rd := func() (io.Reader, error) {
|
|
return bytes.NewReader(shardBytes), nil
|
|
}
|
|
|
|
return cmd.client.ImportIndexKeys(ctx, &node.URI, indexName, partitionID, false, rd)
|
|
})
|
|
}
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
case "attributes":
|
|
//skip
|
|
case "fields":
|
|
fieldName := record[3]
|
|
switch action := record[4]; action {
|
|
case "translate":
|
|
logger.Printf("field keys %v %v", indexName, fieldName)
|
|
//needs to go to all nodes
|
|
shardBytes, err := io.ReadAll(tarReader) // this feels wrong but works for now
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g, _ := errgroup.WithContext(ctx)
|
|
for _, node := range nodes {
|
|
node := node
|
|
g.Go(func() error {
|
|
// rd := bytes.NewReader(shardBytes)
|
|
rd := func() (io.Reader, error) {
|
|
return bytes.NewReader(shardBytes), nil
|
|
}
|
|
|
|
return cmd.client.ImportFieldKeys(ctx, &node.URI, indexName, fieldName, false, rd)
|
|
})
|
|
}
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
case "attributes":
|
|
//skip
|
|
default:
|
|
return fmt.Errorf("unknown restore action: %v", action)
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
/* Fetch the cluster nodes from the target host.
|
|
For each index:
|
|
Upload the RBF snapshot for each shard to the nodes that own the shard.
|
|
Upload the index & field translation BoltDB snapshots to each node.
|
|
If possible, trigger the node to reload itself. Otherwise a restart would be required.
|
|
*/
|
|
|
|
return nil
|
|
}
|
|
func (cmd *RestoreTarCommand) TLSHost() string { return cmd.Host }
|
|
|
|
func (cmd *RestoreTarCommand) TLSConfiguration() server.TLSConfig { return cmd.TLS }
|