storj/pkg/overlay/service.go
JT Olio 5f6607935b
captplanet (#159)
* captplanet

I kind of went overboard this weekend.

The major goal of this changeset is to provide an environment
for local development where all of the various services can
be easily run together. Developing on Storj v3 should be as
easy as running a setup command and a run command!

To do this, this changeset introduces a new tool called
captplanet, which combines the powers of the Overlay Cache,
the PointerDB, the PieceStore, Kademlia, the Minio Gateway,
etc.

Running 40 farmers and a heavy client inside the same process
forced a rethinking of the "services" that we had. To
avoid confusion by reusing prior terms, this changeset
introduces two new types: Providers and Responsibilities.
I wanted to avoid as many merge conflicts as possible, so
I left the existing Services and code for now, but if people
like this route we can clean up the duplication.

A Responsibility is a collection of gRPC methods and
corresponding state. The following systems are examples of
Responsibilities:
 * Kademlia
 * OverlayCache
 * PointerDB
 * StatDB
 * PieceStore
 * etc.

A Provider is a collection of Responsibilities that
share an Identity, such as:
 * The heavy client
 * The farmer
 * The gateway

An Identity is a public/private key pair, a node id, etc.
Farmers all need different Identities, so captplanet
needs to support running multiple concurrent Providers
with different Identities.

Each Responsibility and Provider should allow for configuration
of multiple copies on its own so creating Responsibilities and
Providers use a new workflow.

To make a Responsibility, one should create a "config"
struct, such as:

```
type Config struct {
  RepairThreshold int `help:"If redundancy falls below this number of
pieces, repair is triggered" default:"30"`
  SuccessThreshold int `help:"If redundancy is above this number then
no additional uploads are needed" default:"40"`
}
```

To use "config" structs, this changeset introduces another
new library called 'cfgstruct', which allows for the configuration
of arbitrary structs through flagsets, and thus through cobra and
viper.

cfgstruct relies on Go's "struct tags" feature to document
help information and default values. Config structs can be
configured via cfgstruct.Bind for binding the struct to a flagset.

Because this configuration system makes setup and configuration
easier *in general*, additional commands are provided that allow
for easy standup of separate Providers. Please make sure to
check out:
 * cmd/captplanet/farmer/main.go (a new farmer binary)
 * cmd/captplanet/hc/main.go (a new heavy client binary)
 * cmd/captplanet/gw/main.go (a new minio gateway binary)

Usage:

```
$ go install -v storj.io/storj/cmd/captplanet
$ captplanet setup
$ captplanet run
```

Configuration is placed by default in `~/.storj/capt/`

Other changes:

 * introduces new config structs for currently existing
   Responsibilities that conform to the new Responsibility
   interface. Please see the `pkg/*/config.go` files for
   examples.

 * integrates the PointerDB API key with other global
   configuration via flags, instead of through environment
   variables through viper like it's been doing. (ultimately
   this should also change to use the PointerDB config
   struct but this is an okay shortterm solution).

 * changes the Overlay cache to use a URL for database
   configuration instead of separate redis and bolt config
   settings.

 * stubs out some peer identity skeleton code (but not the
   meat).

 * Fixes the SegmentStore to use the overlay client and
   pointerdb clients instead of gRPC client code directly

 * Leaves a very clear spot where we need to tie the object to
   stream to segment store together. There's sort of a "golden
   spike" opportunity to connect all the train tracks together
   at the bottom of pkg/miniogw/config.go, labeled with a
   bunch of TODOs.

Future stuff:

 * I now prefer this design over the original
   pkg/process.Service thing I had been pushing before (sorry!)

 * The experience of trying to have multiple farmers
   configurable concurrently led me to prefer config structs
   over global flags (I finally came around) or using viper
   directly. I think global flags are okay sometimes but in
   general going forward we should try and get all relevant
   config into config structs.

 * If you all like this direction, I think we can go delete my
   old Service interfaces and a bunch of flags and clean up a
   bunch of stuff.

 * If you don't like this direction, it's no sweat at all, and
   despite how much code there is here I'm not very tied to any
   of this! Considering a lot of this was written between midnight
   and 6 am, it might not be any good!

* bind tests
2018-07-24 10:08:28 -06:00

248 lines
7.6 KiB
Go

// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package overlay
import (
"context"
"flag"
"fmt"
"net"
"net/http"
"path/filepath"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"google.golang.org/grpc"
"gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/storj/pkg/kademlia"
"storj.io/storj/pkg/peertls"
"storj.io/storj/pkg/process"
proto "storj.io/storj/protos/overlay"
)
var (
redisAddress, redisPassword, httpPort, bootstrapIP, bootstrapPort, localPort, boltdbPath string
db int
srvPort uint
options peertls.TLSFileOptions
)
func init() {
flag.StringVar(&httpPort, "httpPort", "", "The port for the health endpoint")
flag.StringVar(&redisAddress, "redisAddress", "", "The <IP:PORT> string to use for connection to a redis cache")
flag.StringVar(&redisPassword, "redisPassword", "", "The password used for authentication to a secured redis instance")
flag.StringVar(&boltdbPath, "boltdbPath", defaultBoltDBPath(), "The path to the boltdb file that should be loaded or created")
flag.IntVar(&db, "db", 0, "The network cache database")
flag.UintVar(&srvPort, "srvPort", 8082, "Port to listen on")
flag.StringVar(&bootstrapIP, "bootstrapIP", "", "Optional IP to bootstrap node against")
flag.StringVar(&bootstrapPort, "bootstrapPort", "", "Optional port of node to bootstrap against")
flag.StringVar(&localPort, "localPort", "8081", "Specify a different port to listen on locally")
flag.StringVar(&options.RootCertRelPath, "tlsCertBasePath", "", "The base path for TLS certificates")
flag.StringVar(&options.RootKeyRelPath, "tlsKeyBasePath", "", "The base path for TLS keys")
flag.BoolVar(&options.Create, "tlsCreate", false, "If true, generate a new TLS cert/key files")
flag.BoolVar(&options.Overwrite, "tlsOverwrite", false, "If true, overwrite existing TLS cert/key files")
}
func defaultBoltDBPath() string {
home, _ := homedir.Dir()
return filepath.Join(home, ".storj", "overlaydb.db")
}
// NewServer creates a new Overlay Service Server
func NewServer(k *kademlia.Kademlia, cache *Cache, l *zap.Logger, m *monkit.Registry) *grpc.Server {
grpcServer := grpc.NewServer()
proto.RegisterOverlayServer(grpcServer, &Server{
dht: k,
cache: cache,
logger: l,
metrics: m,
})
return grpcServer
}
// NewClient connects to grpc server at the provided address with the provided options
// returns a new instance of an overlay Client
func NewClient(serverAddr string, opts ...grpc.DialOption) (proto.OverlayClient, error) {
conn, err := grpc.Dial(serverAddr, opts...)
if err != nil {
return nil, err
}
return proto.NewOverlayClient(conn), nil
}
// NewTLSServer returns a newly initialized gRPC overlay server, configured with TLS
func NewTLSServer(k *kademlia.Kademlia, cache *Cache, l *zap.Logger, m *monkit.Registry, fopts peertls.TLSFileOptions) (_ *grpc.Server, _ error) {
t, err := peertls.NewTLSFileOptions(
fopts.RootCertRelPath,
fopts.RootKeyRelPath,
fopts.Create,
fopts.Overwrite,
)
if err != nil {
return nil, err
}
grpcServer := grpc.NewServer(t.ServerOption())
proto.RegisterOverlayServer(grpcServer, &Server{
dht: k,
cache: cache,
logger: l,
metrics: m,
})
return grpcServer, nil
}
// NewTLSClient connects to grpc server at the provided address with the provided options plus TLS option(s)
// returns a new instance of an overlay Client
func NewTLSClient(serverAddr *string, fopts peertls.TLSFileOptions, opts ...grpc.DialOption) (proto.OverlayClient, error) {
t, err := peertls.NewTLSFileOptions(
fopts.RootCertRelPath,
fopts.RootCertRelPath,
fopts.Create,
fopts.Overwrite,
)
if err != nil {
return nil, err
}
opts = append(opts, t.DialOption())
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
return nil, err
}
return proto.NewOverlayClient(conn), nil
}
// Service contains all methods needed to implement the process.Service interface
type Service struct {
logger *zap.Logger
metrics *monkit.Registry
}
// Process is the main function that executes the service
func (s *Service) Process(ctx context.Context, _ *cobra.Command, _ []string) (
err error) {
// TODO
// 1. Boostrap a node on the network
// 2. Start up the overlay gRPC service
// 3. Connect to Redis
// 4. Boostrap Redis Cache
// TODO(coyle): Should add the ability to pass a configuration to change the bootstrap node
in, err := kademlia.GetIntroNode("", bootstrapIP, bootstrapPort)
if err != nil {
return err
}
id, err := kademlia.NewID()
if err != nil {
return err
}
kad, err := kademlia.NewKademlia(id, []proto.Node{*in}, "0.0.0.0", viper.GetString("localport"))
if err != nil {
s.logger.Error("Failed to instantiate new Kademlia", zap.Error(err))
return err
}
if err := kad.ListenAndServe(); err != nil {
s.logger.Error("Failed to ListenAndServe on new Kademlia", zap.Error(err))
return err
}
if err := kad.Bootstrap(ctx); err != nil {
s.logger.Error("Failed to Bootstrap on new Kademlia", zap.Error(err))
return err
}
// bootstrap cache
var cache *Cache
if viper.GetString("redisaddress") != "" {
cache, err = NewRedisOverlayCache(viper.GetString("redisaddress"), redisPassword, db, kad)
if err != nil {
s.logger.Error("Failed to create a new redis overlay client", zap.Error(err))
return err
}
s.logger.Info("starting overlay cache with redis")
} else if viper.GetString("boltdbpath") != "" {
cache, err = NewBoltOverlayCache(viper.GetString("boltdbpath"), kad)
if err != nil {
s.logger.Error("Failed to create a new boltdb overlay client", zap.Error(err))
return err
}
s.logger.Info("starting overlay cache with boltDB")
} else {
return process.ErrUsage.New("You must specify one of `--boltdbPath` or `--redisAddress`")
}
if err := cache.Bootstrap(ctx); err != nil {
s.logger.Error("Failed to boostrap cache", zap.Error(err))
return err
}
// send off cache refreshes concurrently
go func() {
if err := cache.Refresh(ctx); err != nil {
s.logger.Error("Failed to Refresh cache", zap.Error(err))
}
}()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", viper.GetInt("srvport")))
if err != nil {
s.logger.Error("Failed to initialize TCP connection", zap.Error(err))
return err
}
grpcServer := NewServer(kad, cache, s.logger, s.metrics)
mux := http.NewServeMux()
// TODO(coyle): better health check
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, "OK") })
go func() {
if err := http.ListenAndServe(fmt.Sprintf(":%s", httpPort), mux); err != nil {
s.logger.Fatal("Failed to listen and serve", zap.Error(err))
}
}()
go func() {
if err := cache.Walk(ctx); err != nil {
s.logger.Fatal("Failed to walk cache", zap.Error(err))
}
}()
// If the passed context times out or is cancelled, shutdown the gRPC server
go func() {
if _, ok := <-ctx.Done(); !ok {
grpcServer.GracefulStop()
}
}()
// If `grpcServer.Serve(...)` returns an error, shutdown/cleanup the gRPC server
defer grpcServer.GracefulStop()
return grpcServer.Serve(lis)
}
// SetLogger adds the initialized logger to the Service
func (s *Service) SetLogger(l *zap.Logger) error {
s.logger = l
return nil
}
// SetMetricHandler adds the initialized metric handler to the Service
func (s *Service) SetMetricHandler(m *monkit.Registry) error {
s.metrics = m
return nil
}
// InstanceID implements Service.InstanceID
func (s *Service) InstanceID() string { return "" }