diff --git a/.gitignore b/.gitignore index e3dea8a..ce2f7b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# editors +.vscode + # dependencies vendor @@ -11,3 +14,4 @@ vendor # test tmp test +temp.txt diff --git a/Makefile b/Makefile index fbfe74a..8799e38 100644 --- a/Makefile +++ b/Makefile @@ -14,10 +14,6 @@ build: install: deps go install -ldflags "-X main.Version=$(VERSION)" ./cmd/nexus -.PHONY: config -config: build - ./nexus -config ./config.example.json init - # Install dependencies .PHONY: deps deps: @@ -56,7 +52,47 @@ gen: fileb0x b0x.yml counterfeiter -o ./ipfs/mock/ipfs.mock.go \ ./ipfs/ipfs.go NodeClient + counterfeiter -o ./temporal/mock/networks.mock.go \ + ./temporal/database.go PrivateNetworks .PHONY: release release: bash .scripts/release.sh + +##################### +# DEVELOPMENT UTILS # +##################### + +NETWORK=test_network +TESTFLAGS=-dev -config ./config.dev.json + +.PHONY: example-config +example-config: build + ./nexus -config ./config.example.json init + +.PHONY: dev-config +dev-config: build + ./nexus $(TESTFLAGS) init + +.PHONY: config +config: example-config dev-config + +.PHONY: daemon +daemon: build + ./nexus $(TESTFLAGS) daemon + +.PHONY: new-network +new-network: build + ./nexus $(TESTFLAGS) dev network $(NETWORK) + +.PHONY: start-network +start-network: build + ./nexus $(TESTFLAGS) ctl --pretty StartNetwork Network=$(NETWORK) + +.PHONY: stat-network +stat-network: + ./nexus $(TESTFLAGS) ctl --pretty NetworkStats Network=$(NETWORK) + +.PHONY: diag-network +diag-network: + ./nexus $(TESTFLAGS) ctl NetworkDiagnostics Network=$(NETWORK) diff --git a/README.md b/README.md index d55190b..4c9ef68 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ -# 🦑 Nexus: IPFS Network Node Orchestrator +# 🦑 Nexus -Nexus is the IPFS private network node orchestration and registry service for -[Temporal](https://github.com/RTradeLtd/Temporal), an easy-to-use interface into -distributed and decentralized storage technologies. +> IPFS Network Node Orchestrator + +Nexus is the [IPFS](https://github.com/ipfs/go-ipfs) private network node +orchestration and registry service for [Temporal](https://github.com/RTradeLtd/Temporal), +an easy-to-use interface into distributed and decentralized storage technologies. +Nexus handles on-demand deployment, resource management, metadata persistence, +and fine-grained access control for IPFS nodes running within Docker containers. [![GoDoc](https://godoc.org/github.com/RTradeLtd/Nexus?status.svg)](https://godoc.org/github.com/RTradeLtd/Nexus) [![Build Status](https://travis-ci.com/RTradeLtd/Nexus.svg?branch=master)](https://travis-ci.com/RTradeLtd/Nexus) [![codecov](https://codecov.io/gh/RTradeLtd/Nexus/branch/master/graph/badge.svg)](https://codecov.io/gh/RTradeLtd/Nexus) [![Go Report Card](https://goreportcard.com/badge/github.com/RTradeLtd/Nexus)](https://goreportcard.com/report/github.com/RTradeLtd/Nexus) +[![Latest Release](https://img.shields.io/github/release/RTradeLtd/Nexus.svg?colorB=red)](https://github.com/RTradeLtd/Nexus/releases) ## Installation and Usage @@ -16,7 +21,8 @@ $> go get -u github.com/RTradeLtd/Nexus/cmd/nexus ``` Releases are also be available from the -[Releases](https://github.com/RTradeLtd/Nexus/releases) page. +[Releases](https://github.com/RTradeLtd/Nexus/releases) page. To start up the +Nexus daemon using the default configuration, run: ```bash $> nexus init @@ -54,6 +60,23 @@ $> make test You can remove leftover assets using `make clean`. +### Running Locally + +A few make commands make it easy to simulate a full orchestrator environment on your machine: + +```bash +$> make dev-config # make sure dev configuration is up to date +$> make testenv # initialize test environment +$> make daemon # start up daemon with dev configuration +``` + +Then, you can set up and start a network node: + +```bash +$> make new-network # create network entry in database +$> make start-network # spin up network node +``` + ### ctl An experimental, lightweight controller for the gRPC API is available via the diff --git a/client/client.go b/client/client.go index e99a62e..c605780 100644 --- a/client/client.go +++ b/client/client.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/RTradeLtd/grpc/dialer" - ipfs_orchestrator "github.com/RTradeLtd/grpc/ipfs-orchestrator" + "github.com/RTradeLtd/grpc/nexus" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -15,7 +15,7 @@ import ( // IPFSOrchestratorClient is a lighweight container for the orchestrator's // gRPC API client type IPFSOrchestratorClient struct { - ipfs_orchestrator.ServiceClient + nexus.ServiceClient grpc *grpc.ClientConn } @@ -48,7 +48,7 @@ func New(opts config.API, devMode bool) (*IPFSOrchestratorClient, error) { if err != nil { return nil, fmt.Errorf("failed to connect to core service: %s", err.Error()) } - c.ServiceClient = ipfs_orchestrator.NewServiceClient(c.grpc) + c.ServiceClient = nexus.NewServiceClient(c.grpc) return c, nil } diff --git a/cmd/nexus/daemon.go b/cmd/nexus/daemon.go index e139458..c39f924 100644 --- a/cmd/nexus/daemon.go +++ b/cmd/nexus/daemon.go @@ -5,9 +5,15 @@ import ( "os" "os/signal" "syscall" + "time" + + tcfg "github.com/RTradeLtd/config" + "github.com/RTradeLtd/database" + "github.com/RTradeLtd/database/models" "github.com/RTradeLtd/Nexus/config" "github.com/RTradeLtd/Nexus/daemon" + "github.com/RTradeLtd/Nexus/delegator" "github.com/RTradeLtd/Nexus/ipfs" "github.com/RTradeLtd/Nexus/log" "github.com/RTradeLtd/Nexus/orchestrator" @@ -41,30 +47,76 @@ func runDaemon(configPath string, devMode bool, args []string) { fatal(err.Error()) } + // set up database connection + l.Infow("intializing database connection", + "db.host", cfg.Database.URL, + "db.port", cfg.Database.Port, + "db.name", cfg.Database.Name, + "db.with_ssl", !devMode, + "db.with_migrations", devMode) + dbm, err := database.Initialize(&tcfg.TemporalConfig{ + Database: cfg.Database, + }, database.Options{ + SSLModeDisable: devMode, + RunMigrations: devMode, + }) + if err != nil { + l.Errorw("failed to connect to database", "error", err) + fatalf("unable to connect to database: %s", err.Error()) + } + l.Info("successfully connected to database") + defer func() { + if err := dbm.DB.Close(); err != nil { + l.Warnw("error occurred closing database connection", + "error", err) + } + }() + // initialize orchestrator println("initializing orchestrator") - o, err := orchestrator.New(l, cfg.Address, c, cfg.IPFS.Ports, cfg.Database, devMode) + o, err := orchestrator.New(l, cfg.Address, cfg.IPFS.Ports, devMode, + c, models.NewHostedIPFSNetworkManager(dbm.DB)) if err != nil { fatal(err.Error()) } // initialize daemon println("initializing daemon") - d := daemon.New(l, o) + dm := daemon.New(l, o) + + // initialize delegator + println("initializing delegator") + dl := delegator.New(l, Version, 1*time.Minute, []byte(cfg.Delegator.JWTKey), + o.Registry, models.NewHostedIPFSNetworkManager(dbm.DB)) - // handle graceful shutdown + // catch interrupts ctx, cancel := context.WithCancel(context.Background()) - signals := make(chan os.Signal) + var signals = make(chan os.Signal) signal.Notify(signals, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) go func() { <-signals cancel() }() - // serve endpoints - println("spinning up server") - if err := d.Run(ctx, cfg.API); err != nil { - println(err.Error()) - } - println("server shut down") + // serve gRPC endpoints + println("spinning up gRPC server...") + go func() { + if err := dm.Run(ctx, cfg.API); err != nil { + println(err.Error()) + } + cancel() + }() + + // serve delegator + println("spinning up delegator...") + go func() { + if err := dl.Run(ctx, cfg.Delegator); err != nil { + println(err.Error()) + } + cancel() + }() + + // block + <-ctx.Done() + println("orchestrator shut down") } diff --git a/cmd/nexus/dev.go b/cmd/nexus/dev.go new file mode 100644 index 0000000..bfe9746 --- /dev/null +++ b/cmd/nexus/dev.go @@ -0,0 +1,36 @@ +package main + +import ( + "github.com/RTradeLtd/Nexus/config" + tcfg "github.com/RTradeLtd/config" + "github.com/RTradeLtd/database" + "github.com/RTradeLtd/database/models" +) + +func initTestNetwork(configPath, networkName string) { + // load configuration + cfg, err := config.LoadConfig(configPath) + if err != nil { + fatal(err.Error()) + } + + println("setting up database entry for a test network") + + dbm, err := database.Initialize(&tcfg.TemporalConfig{ + Database: cfg.Database, + }, database.Options{ + SSLModeDisable: true, + RunMigrations: true, + }) + if err != nil { + fatal(err.Error()) + } + + var nm = models.NewHostedIPFSNetworkManager(dbm.DB) + if _, err := nm.CreateHostedPrivateNetwork(networkName, "", nil, models.NetworkAccessOptions{ + Users: []string{"testuser"}, + PublicGateway: true, + }); err != nil { + fatal(err.Error()) + } +} diff --git a/cmd/nexus/main.go b/cmd/nexus/main.go index 2fbbda5..a2b4e69 100644 --- a/cmd/nexus/main.go +++ b/cmd/nexus/main.go @@ -27,9 +27,11 @@ USAGE: COMMANDS: init initialize configuration - daemon spin up the Nexus daemon and related processes - ctl [EXPERIMENTAL] interact with daemon via a low-level client - version display program version + daemon spin up the Nexus daemon and related processes + version display program version + + dev [DEV] utilities for development purposes + ctl [EXPERIMENTAL] interact with daemon via a low-level client OPTIONS: @@ -54,15 +56,18 @@ func main() { if len(args) >= 1 { switch args[0] { + // version info case "version": println("Nexus " + Version) case "init": - config.GenerateConfig(*configPath) + config.GenerateConfig(*configPath, *devMode) println("orchestrator configuration generated at " + *configPath) return + // run daemon case "daemon": runDaemon(*configPath, *devMode, args[1:]) return + // run ctl case "ctl": if len(args) > 1 && (args[1] == "-pretty" || args[1] == "--pretty") { runCTL(*configPath, *devMode, true, args[2:]) @@ -70,13 +75,31 @@ func main() { runCTL(*configPath, *devMode, false, args[1:]) } return + // dev utilities + case "dev": + if *devMode != true { + fatal("do not use the dev utilities outside of dev mode!") + } + if len(args) > 1 { + switch args[1] { + case "network": + if len(args) < 2 { + fatal("additional argument required") + } + initTestNetwork(*configPath, args[2]) + default: + fatal(fmt.Sprintf("unknown command '%s' - user the --help' flag for documentation", + strings.Join(args[0:], " "))) + } + } + // default error default: fatal(fmt.Sprintf("unknown command '%s' - user the --help' flag for documentation", strings.Join(args[0:], " "))) return } } else { - fatal("no arguments provided - use the '--help' flag for documentation") + fatal("insufficient arguments provided - use the '--help' flag for documentation") } } @@ -84,3 +107,9 @@ func fatal(msg ...interface{}) { fmt.Println(msg...) os.Exit(1) } + +func fatalf(format string, msg ...interface{}) { + fmt.Printf(format, msg...) + println() + os.Exit(1) +} diff --git a/config.dev.json b/config.dev.json new file mode 100755 index 0000000..c7f401f --- /dev/null +++ b/config.dev.json @@ -0,0 +1,45 @@ +{ + "address": "", + "log_path": "", + "ipfs": { + "version": "v0.4.18", + "data_dir": "tmp", + "perm_mode": "0700", + "ports": { + "swarm": [ + "4001-5000" + ], + "api": [ + "5001-6000" + ], + "gateway": [ + "8001-9000" + ] + } + }, + "api": { + "host": "127.0.0.1", + "port": "9111", + "key": "DO_NOT_LEAVE_ME_AS_DEFAULT", + "tls": { + "cert": "", + "key": "" + } + }, + "delegator": { + "host": "127.0.0.1", + "port": "8080", + "jwt_key": "suchStrongKeyMuchProtectVerySafe", + "tls": { + "cert": "", + "key": "" + } + }, + "postgres": { + "name": "", + "url": "127.0.0.1", + "port": "5433", + "username": "postgres", + "password": "password123" + } +} diff --git a/config.example.json b/config.example.json index 12a5066..74e5dfb 100755 --- a/config.example.json +++ b/config.example.json @@ -20,7 +20,16 @@ "api": { "host": "127.0.0.1", "port": "9111", - "key": "", + "key": "DO_NOT_LEAVE_ME_AS_DEFAULT", + "tls": { + "cert": "", + "key": "" + } + }, + "delegator": { + "host": "127.0.0.1", + "port": "80", + "jwt_key": "DO_NOT_LEAVE_ME_AS_DEFAULT", "tls": { "cert": "", "key": "" diff --git a/config/config.go b/config/config.go index 924c7a6..1c87d82 100644 --- a/config/config.go +++ b/config/config.go @@ -1,11 +1,9 @@ package config import ( - "bytes" "encoding/json" "fmt" "io/ioutil" - "os" tcfg "github.com/RTradeLtd/config" ) @@ -23,6 +21,7 @@ type IPFSOrchestratorConfig struct { IPFS `json:"ipfs"` API `json:"api"` + Delegator `json:"delegator"` tcfg.Database `json:"postgres"` } @@ -50,7 +49,15 @@ type API struct { TLS `json:"tls"` } -// TLS declares HTTPS configuration for the daemon's gRPC API +// Delegator declares configuration for the orchestrator proxy +type Delegator struct { + Host string `json:"host"` + Port string `json:"port"` + JWTKey string `json:"jwt_key"` + TLS `json:"tls"` +} + +// TLS declares HTTPS configuration type TLS struct { CertPath string `json:"cert"` KeyPath string `json:"key"` @@ -59,7 +66,7 @@ type TLS struct { // New creates a new, default configuration func New() IPFSOrchestratorConfig { var cfg IPFSOrchestratorConfig - cfg.setDefaults() + cfg.SetDefaults(false) return cfg } @@ -77,49 +84,78 @@ func LoadConfig(configPath string) (IPFSOrchestratorConfig, error) { return cfg, fmt.Errorf("could not read config: %s", err.Error()) } - cfg.setDefaults() + cfg.SetDefaults(false) return cfg, nil } -// GenerateConfig writes an empty orchestrator config template to given filepath -func GenerateConfig(configPath string) error { - template := &IPFSOrchestratorConfig{} - template.setDefaults() - b, err := json.Marshal(template) - if err != nil { - return err +// SetDefaults initializes certain blank values with defaults, with special +// presets for dev +func (c *IPFSOrchestratorConfig) SetDefaults(dev bool) { + // API settings + if c.API.Host == "" { + c.API.Host = "127.0.0.1" + } + if c.API.Port == "" { + c.API.Port = "9111" + } + if c.API.Key == "" { + c.API.Key = "DO_NOT_LEAVE_ME_AS_DEFAULT" } - var pretty bytes.Buffer - if err = json.Indent(&pretty, b, "", " "); err != nil { - return err + // Proxy (delegator) settings + if c.Delegator.Host == "" { + c.Delegator.Host = "127.0.0.1" + } + if c.Delegator.Port == "" { + if dev { + c.Delegator.Port = "8080" + } else { + c.Delegator.Port = "80" + } + } + if c.Delegator.JWTKey == "" { + if dev { + c.Delegator.JWTKey = "suchStrongKeyMuchProtectVerySafe" + } else { + c.Delegator.JWTKey = "DO_NOT_LEAVE_ME_AS_DEFAULT" + } + } + + // Database settings + if c.Database.URL == "" { + c.Database.URL = "127.0.0.1" + } + if c.Database.Port == "" { + if dev { + c.Database.Port = "5433" + } else { + c.Database.Port = "5432" + } + } + if dev { + if c.Database.Username == "" { + c.Database.Username = "postgres" + } + if c.Database.Password == "" { + c.Database.Password = "password123" + } } - return ioutil.WriteFile(configPath, append(pretty.Bytes(), '\n'), os.ModePerm) -} -func (c *IPFSOrchestratorConfig) setDefaults() { + // IPFS settings if c.IPFS.Version == "" { c.IPFS.Version = DefaultIPFSVersion } if c.IPFS.DataDirectory == "" { - c.IPFS.DataDirectory = "/" + if dev { + c.IPFS.DataDirectory = "tmp" + } else { + c.IPFS.DataDirectory = "/" + } } if c.IPFS.ModePerm == "" { c.IPFS.ModePerm = "0700" } - if c.API.Host == "" { - c.API.Host = "127.0.0.1" - } - if c.API.Port == "" { - c.API.Port = "9111" - } - if c.Database.URL == "" { - c.Database.URL = "127.0.0.1" - } - if c.Database.Port == "" { - c.Database.Port = "5432" - } if c.IPFS.Ports.Swarm == nil { c.IPFS.Ports.Swarm = []string{"4001-5000"} } diff --git a/config/config_test.go b/config/config_test.go index 25c6bdb..d5ad361 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -17,7 +17,7 @@ func TestLoadConfig(t *testing.T) { }{ {"invalid path", args{""}, IPFSOrchestratorConfig{}, true}, {"invalid file", args{"./config.go"}, IPFSOrchestratorConfig{}, true}, - {"valid dev config", args{"../config.example.json"}, New(), false}, + {"valid example config", args{"../config.example.json"}, New(), false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -32,9 +32,3 @@ func TestLoadConfig(t *testing.T) { }) } } - -func TestGenerateConfig(t *testing.T) { - if err := GenerateConfig("../config.json"); err != nil { - t.Error(err.Error()) - } -} diff --git a/config/generate.go b/config/generate.go new file mode 100644 index 0000000..98d87f9 --- /dev/null +++ b/config/generate.go @@ -0,0 +1,24 @@ +package config + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "os" +) + +// GenerateConfig writes an empty orchestrator config template to given filepath +func GenerateConfig(configPath string, dev bool) error { + template := &IPFSOrchestratorConfig{} + template.SetDefaults(dev) + b, err := json.Marshal(template) + if err != nil { + return err + } + + var pretty bytes.Buffer + if err = json.Indent(&pretty, b, "", " "); err != nil { + return err + } + return ioutil.WriteFile(configPath, append(pretty.Bytes(), '\n'), os.ModePerm) +} diff --git a/config/generate_test.go b/config/generate_test.go new file mode 100644 index 0000000..bfe432a --- /dev/null +++ b/config/generate_test.go @@ -0,0 +1,12 @@ +package config + +import "testing" + +func TestGenerateConfig(t *testing.T) { + if err := GenerateConfig("../config.json", false); err != nil { + t.Error(err.Error()) + } + if err := GenerateConfig("../config.json", true); err != nil { + t.Error(err.Error()) + } +} diff --git a/daemon/daemon.go b/daemon/daemon.go index b4dc92a..22a0ade 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -8,8 +8,8 @@ import ( "github.com/RTradeLtd/Nexus/config" "github.com/RTradeLtd/Nexus/orchestrator" - ipfs_orchestrator "github.com/RTradeLtd/grpc/ipfs-orchestrator" "github.com/RTradeLtd/grpc/middleware" + "github.com/RTradeLtd/grpc/nexus" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -85,7 +85,7 @@ func (d *Daemon) Run(ctx context.Context, cfg config.API) error { // initialize server server := grpc.NewServer(serverOpts...) - ipfs_orchestrator.RegisterServiceServer(server, d) + nexus.RegisterServiceServer(server, d) // interrupt server gracefully if context is cancelled go func() { diff --git a/daemon/endpoints.go b/daemon/endpoints.go index 62eb384..73127eb 100644 --- a/daemon/endpoints.go +++ b/daemon/endpoints.go @@ -7,69 +7,103 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" - ipfs_orchestrator "github.com/RTradeLtd/grpc/ipfs-orchestrator" + "github.com/RTradeLtd/grpc/nexus" ) // Ping is useful for checking client-server connection -func (d *Daemon) Ping(c context.Context, - req *ipfs_orchestrator.Empty) (*ipfs_orchestrator.Empty, error) { - return &ipfs_orchestrator.Empty{}, nil +func (d *Daemon) Ping( + c context.Context, + req *nexus.Empty, +) (*nexus.Empty, error) { + return &nexus.Empty{}, nil } // StartNetwork brings a node for the requested network online -func (d *Daemon) StartNetwork(ctx context.Context, - req *ipfs_orchestrator.NetworkRequest) (*ipfs_orchestrator.StartNetworkResponse, error) { - +func (d *Daemon) StartNetwork( + ctx context.Context, + req *nexus.NetworkRequest, +) (*nexus.StartNetworkResponse, error) { n, err := d.o.NetworkUp(ctx, req.GetNetwork()) if err != nil { - return nil, err + return nil, grpc.Errorf(codes.Internal, err.Error()) } - return &ipfs_orchestrator.StartNetworkResponse{ - Api: n.API, - SwarmKey: n.SwarmKey, + return &nexus.StartNetworkResponse{ + PeerId: n.PeerID, + SwarmPort: n.SwarmPort, + SwarmKey: n.SwarmKey, }, nil } // UpdateNetwork updates the configuration of the given network -func (d *Daemon) UpdateNetwork(ctx context.Context, - req *ipfs_orchestrator.NetworkRequest) (*ipfs_orchestrator.Empty, error) { +func (d *Daemon) UpdateNetwork( + ctx context.Context, + req *nexus.NetworkRequest, +) (*nexus.Empty, error) { - return &ipfs_orchestrator.Empty{}, d.o.NetworkUpdate(ctx, req.GetNetwork()) + return &nexus.Empty{}, d.o.NetworkUpdate(ctx, req.GetNetwork()) } // StopNetwork brings a node for the requested network offline -func (d *Daemon) StopNetwork(ctx context.Context, - req *ipfs_orchestrator.NetworkRequest) (*ipfs_orchestrator.Empty, error) { +func (d *Daemon) StopNetwork( + ctx context.Context, + req *nexus.NetworkRequest, +) (*nexus.Empty, error) { - return &ipfs_orchestrator.Empty{}, d.o.NetworkDown(ctx, req.GetNetwork()) + return &nexus.Empty{}, d.o.NetworkDown(ctx, req.GetNetwork()) } // RemoveNetwork removes assets for requested node -func (d *Daemon) RemoveNetwork(ctx context.Context, - req *ipfs_orchestrator.NetworkRequest) (*ipfs_orchestrator.Empty, error) { +func (d *Daemon) RemoveNetwork( + ctx context.Context, + req *nexus.NetworkRequest, +) (*nexus.Empty, error) { - return &ipfs_orchestrator.Empty{}, d.o.NetworkRemove(ctx, req.GetNetwork()) + return &nexus.Empty{}, d.o.NetworkRemove(ctx, req.GetNetwork()) } // NetworkStats retrieves stats about the requested node -func (d *Daemon) NetworkStats(ctx context.Context, - req *ipfs_orchestrator.NetworkRequest) (*ipfs_orchestrator.NetworkStatusReponse, error) { +func (d *Daemon) NetworkStats( + ctx context.Context, + req *nexus.NetworkRequest, +) (*nexus.NetworkStatusReponse, error) { s, err := d.o.NetworkStatus(ctx, req.Network) if err != nil { return nil, grpc.Errorf(codes.Internal, err.Error()) } - sb, err := json.Marshal(s.Stats) + + return &nexus.NetworkStatusReponse{ + Network: s.NetworkDetails.NetworkID, + PeerId: s.NetworkDetails.PeerID, + Uptime: int64(s.Uptime), + DiskUsage: s.DiskUsage, + SwarmPort: s.NetworkDetails.SwarmPort, + }, nil +} + +// NetworkDiagnostics retrieves detailed diagnostic details about the requested +// network node +func (d *Daemon) NetworkDiagnostics( + ctx context.Context, + req *nexus.NetworkRequest, +) (*nexus.NetworkDiagnosticsResponse, error) { + + s, err := d.o.NetworkDiagnostics(ctx, req.Network) + if err != nil { + return nil, grpc.Errorf(codes.Internal, err.Error()) + } + nb, err := json.Marshal(s.NodeInfo) + if err != nil { + return nil, grpc.Errorf(codes.Internal, err.Error()) + } + sb, err := json.Marshal(s.NodeStats) if err != nil { return nil, grpc.Errorf(codes.Internal, err.Error()) } - return &ipfs_orchestrator.NetworkStatusReponse{ - Network: req.Network, - Api: s.API, - Uptime: int64(s.Uptime), - DiskUsage: s.DiskUsage, - Stats: sb, + return &nexus.NetworkDiagnosticsResponse{ + NodeInfo: nb, + Stats: sb, }, nil } diff --git a/delegator/auth.go b/delegator/auth.go new file mode 100644 index 0000000..850232d --- /dev/null +++ b/delegator/auth.go @@ -0,0 +1,49 @@ +package delegator + +import ( + "errors" + "net/http" + "strings" + + jwt "github.com/dgrijalva/jwt-go" +) + +var ( + errNoAuth = errors.New("no authentication provided") + errInvalidAuth = errors.New("invalid authentication provided") +) + +func getUserFromJWT( + r *http.Request, + keyLookup jwt.Keyfunc, +) (user string, err error) { + // Collect the token from the header. + bearerString := r.Header.Get("Authorization") + + // Split out the actual token from the header. + splitToken := strings.Split(bearerString, "Bearer ") + if len(splitToken) < 2 { + return "", errNoAuth + } + tokenString := splitToken[1] + + // Parse takes the token string and a function for looking up the key. + token, err := jwt.Parse(tokenString, keyLookup) + if err != nil { + return "", errInvalidAuth + } + + // Verify the claims + var claims jwt.MapClaims + var ok bool + if claims, ok = token.Claims.(jwt.MapClaims); !ok || !token.Valid { + return "", errInvalidAuth + } + + // Retrieve ID + if user, ok = claims["id"].(string); !ok || user == "" { + return "", errInvalidAuth + } + + return +} diff --git a/delegator/auth_test.go b/delegator/auth_test.go new file mode 100644 index 0000000..a0e89c0 --- /dev/null +++ b/delegator/auth_test.go @@ -0,0 +1,85 @@ +package delegator + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + jwt "github.com/dgrijalva/jwt-go" +) + +var ( + defaultTestKey = []byte("suchStrongKeyMuchProtectVerySafe") + + // validToken is a token generated using Temporal's gin-jwt configuration, + // signed using defaultTestKey (the same used in RTradeLtd/testenv), generated + // for user 'testuser'. + // + // You can use this for testing to try out the Delegator's access control for + // an IPFS node API: + // + // $ make testenv + // $ make testenv + // $ make daemon + // $ make new-network + // $ make start-network + // $ export TOKEN= + // $ curl --header "Authorization: Bearer $TOKEN" 127.0.0.1:8080/network/test_network/api/v0/repo/stat + // + // This should give you a response. Without a header or with a different + // token, an error will be returned. + validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDc3Nzg2MzcsImlkIjoidGVzdHVzZXIiLCJvcmlnX2lhdCI6MTU0NzY5MjIzN30.2oqQCym2mcyFl8mjOHoGNtK41SJLwX0xbWScDruDECQ" +) + +func getTestKey(*jwt.Token) (interface{}, error) { return defaultTestKey, nil } + +func Test_getUserFromJWT(t *testing.T) { + type args struct { + r *http.Request + keyLookup jwt.Keyfunc + } + tests := []struct { + name string + args args + wantUser string + wantErr bool + }{ + {"no header", + args{httptest.NewRequest("", "/", nil), getTestKey}, + "", true}, + {"invalid header", + args{func() *http.Request { + var r = httptest.NewRequest("", "/", nil) + r.Header.Set("Authorization", "asdf") + return r + }(), getTestKey}, + "", true}, + {"invalid token", + args{func() *http.Request { + var r = httptest.NewRequest("", "/", nil) + r.Header.Set("Authorization", "Bearer asdf") + return r + }(), getTestKey}, + "", true}, + {"valid token, should return correct user", + args{func() *http.Request { + var r = httptest.NewRequest("", "/", nil) + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", validToken)) + return r + }(), getTestKey}, + "testuser", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotUser, err := getUserFromJWT(tt.args.r, tt.args.keyLookup) + if (err != nil) != tt.wantErr { + t.Errorf("getUserFromJWT() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotUser != tt.wantUser { + t.Errorf("getUserFromJWT() = %v, want %v", gotUser, tt.wantUser) + } + }) + } +} diff --git a/delegator/cache.go b/delegator/cache.go new file mode 100644 index 0000000..afa75c7 --- /dev/null +++ b/delegator/cache.go @@ -0,0 +1,92 @@ +package delegator + +import ( + "net/http/httputil" + "sync" + "time" +) + +type proxy struct { + expire int64 + handler *httputil.ReverseProxy +} + +type cache struct { + dur time.Duration + store map[string]proxy + stop chan bool + + mux sync.RWMutex +} + +// newCache creates a cache +func newCache(expire, cleanInterval time.Duration) *cache { + c := &cache{ + dur: expire, + store: make(map[string]proxy), + stop: make(chan bool), + } + + // run cleaner + go c.cleaner(cleanInterval) + + return c +} + +// Cache stores given key +func (c *cache) Cache(key string, handler *httputil.ReverseProxy) { + c.mux.Lock() + c.store[key] = proxy{time.Now().Add(c.dur).UnixNano(), handler} + c.mux.Unlock() +} + +// Exists returns true if key exists +func (c *cache) Get(key string) (handler *httputil.ReverseProxy) { + c.mux.RLock() + i, found := c.store[key] + if !found { + c.mux.RUnlock() + return + } + if i.expire > 0 && time.Now().UnixNano() > i.expire { + c.mux.RUnlock() + return + } + handler = i.handler + c.mux.RUnlock() + return +} + +// Size returns the number of elements in the cache +func (c *cache) Size() int { + c.mux.RLock() + n := len(c.store) + c.mux.RUnlock() + return n +} + +// prune removes all expired items +func (c *cache) prune() { + c.mux.Lock() + now := time.Now().UnixNano() + for k, v := range c.store { + if v.expire > 0 && now > v.expire { + delete(c.store, k) + } + } + c.mux.Unlock() +} + +// cleaner runs at provided intervals to prune the store of expired items +func (c *cache) cleaner(interval time.Duration) { + ticker := time.NewTicker(interval) + for { + select { + case <-ticker.C: + c.prune() + case <-c.stop: + ticker.Stop() + return + } + } +} diff --git a/delegator/cache_test.go b/delegator/cache_test.go new file mode 100644 index 0000000..e3b6a61 --- /dev/null +++ b/delegator/cache_test.go @@ -0,0 +1,56 @@ +package delegator + +import ( + "net/http/httputil" + "testing" + "time" +) + +func Test_cache_Get(t *testing.T) { + type fields struct { + expire time.Duration + } + type args struct { + put string + get string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + {"nonexistent key", fields{time.Minute}, args{"", "key"}, false}, + {"expired key", fields{0}, args{"key", "key"}, false}, + {"ok key", fields{time.Minute}, args{"key", "key"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := newCache(tt.fields.expire, 5*time.Minute) + c.Cache(tt.args.put, &httputil.ReverseProxy{}) + time.Sleep(time.Microsecond) + if got := c.Get(tt.args.get); (got == nil) == tt.want { + t.Errorf("cache.Exists() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_cache_cleaner(t *testing.T) { + c := newCache(0, time.Microsecond) + + // cache and wait for collector to pick up + c.Cache("some_key", nil) + time.Sleep(time.Millisecond) + + // check that collector picked up key + c.mux.RLock() + if _, f := c.store["some_key"]; f { + t.Error("item was not removed by cleaner") + } + c.mux.RUnlock() + + // stop + c.stop <- true + time.Sleep(time.Millisecond) +} diff --git a/delegator/context.go b/delegator/context.go new file mode 100644 index 0000000..942ba47 --- /dev/null +++ b/delegator/context.go @@ -0,0 +1,11 @@ +package delegator + +// contextKey is used to denote context keys +type contextKey string + +func (c contextKey) String() string { return string(c) } + +const ( + keyNetwork contextKey = "network_id" + keyFeature contextKey = "feature" +) diff --git a/delegator/doc.go b/delegator/doc.go new file mode 100644 index 0000000..f753593 --- /dev/null +++ b/delegator/doc.go @@ -0,0 +1,2 @@ +// Package delegator provides the ipfs-orchestrator's network routing functionality +package delegator diff --git a/delegator/engine.go b/delegator/engine.go new file mode 100644 index 0000000..440ea0a --- /dev/null +++ b/delegator/engine.go @@ -0,0 +1,264 @@ +package delegator + +import ( + "context" + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "time" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/cors" + "github.com/go-chi/render" + "go.uber.org/zap" + + "github.com/RTradeLtd/Nexus/config" + "github.com/RTradeLtd/Nexus/ipfs" + "github.com/RTradeLtd/Nexus/log" + "github.com/RTradeLtd/Nexus/network" + "github.com/RTradeLtd/Nexus/registry" + "github.com/RTradeLtd/Nexus/temporal" +) + +// Engine manages request delegation +type Engine struct { + l *zap.SugaredLogger + reg *registry.NodeRegistry + cache *cache + + networks temporal.PrivateNetworks + + timeout time.Duration + keyLookup jwt.Keyfunc + version string +} + +// New instantiates a new delegator engine +func New(l *zap.SugaredLogger, version string, timeout time.Duration, jwtKey []byte, + reg *registry.NodeRegistry, networks temporal.PrivateNetworks) *Engine { + + return &Engine{ + l: l.Named("delegator"), + reg: reg, + cache: newCache(30*time.Minute, 30*time.Minute), + + networks: networks, + + timeout: timeout, + version: version, + keyLookup: func(t *jwt.Token) (interface{}, error) { + return jwtKey, nil + }, + } +} + +// Run spins up a server that listens for requests and proxies them appropriately +func (e *Engine) Run(ctx context.Context, opts config.Delegator) error { + var r = chi.NewRouter() + + // mount middleware + r.Use( + cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"*"}, + AllowCredentials: true, + }).Handler, + middleware.RequestID, + middleware.RealIP, + log.NewMiddleware(e.l.Named("requests")), + middleware.Recoverer, + ) + + // register endpoints + r.HandleFunc("/status", e.Status) + r.Route(fmt.Sprintf("/network/{%s}", keyNetwork), func(r chi.Router) { + r.Use(e.NetworkContext) + r.HandleFunc("/status", e.NetworkStatus) + r.Route(fmt.Sprintf("/{%s}", keyFeature), func(r chi.Router) { + r.HandleFunc("/*", e.Redirect) + }) + }) + + // set up server + var srv = &http.Server{ + Handler: r, + + Addr: opts.Host + ":" + opts.Port, + WriteTimeout: e.timeout, + ReadTimeout: e.timeout, + } + + // handle shutdown + go func() { + for { + select { + case <-ctx.Done(): + shutdown, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if err := srv.Shutdown(shutdown); err != nil { + e.l.Warnw("error encountered during shutdown", "error", err.Error()) + } + } + } + }() + + // go! + if opts.TLS.CertPath != "" { + if err := srv.ListenAndServeTLS(opts.TLS.CertPath, opts.TLS.KeyPath); err != nil && err != http.ErrServerClosed { + e.l.Errorw("error encountered - service stopped", "error", err) + return err + } + } else { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + e.l.Errorw("error encountered - service stopped", "error", err) + return err + } + } + + return nil +} + +// NetworkContext creates a handler that injects relevant network context into +// all incoming requests through URL parameters +func (e *Engine) NetworkContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var id = chi.URLParam(r, string(keyNetwork)) + n, err := e.reg.Get(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + next.ServeHTTP(w, r.WithContext( + context.WithValue(r.Context(), keyNetwork, &n), + )) + }) +} + +// Redirect manages request redirects +func (e *Engine) Redirect(w http.ResponseWriter, r *http.Request) { + // retrieve network + n, ok := r.Context().Value(keyNetwork).(*ipfs.NodeInfo) + if !ok || n == nil { + http.Error(w, http.StatusText(http.StatusUnprocessableEntity), http.StatusUnprocessableEntity) + return + } + + // retrieve requested feature + var feature string + if feature = chi.URLParam(r, string(keyFeature)); feature == "" { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + // set target port based on feature + var port string + switch feature { + case "swarm": + // Swarm access is open to all by default + port = n.Ports.Swarm + case "api": + // IPFS network API access requires an authorized user + user, err := getUserFromJWT(r, e.keyLookup) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + entry, err := e.networks.GetNetworkByName(n.NetworkID) + if err != nil { + http.Error(w, "failed to find network", http.StatusNotFound) + return + } + var found = false + for _, authorized := range entry.Users { + if user == authorized { + found = true + } + } + if !found { + http.Error(w, "user not authorized", http.StatusForbidden) + return + } + // set access rules + w.Header().Set("Vary", "Origin") + if entry.APIAllowedOrigin == "" { + w.Header().Set("Access-Control-Allow-Origin", "*") + } else { + w.Header().Set("Access-Control-Allow-Origin", entry.APIAllowedOrigin) + } + // catch preflights + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + port = n.Ports.API + case "gateway": + // Gateway is only open if configured as such + if entry, err := e.networks.GetNetworkByName(n.NetworkID); err != nil { + http.Error(w, "failed to find network", http.StatusNotFound) + return + } else if !entry.GatewayPublic { + http.Error(w, "failed to find network gateway", http.StatusNotFound) + return + } + port = n.Ports.Gateway + default: + http.Error(w, fmt.Sprintf("invalid feature '%s'", feature), http.StatusBadRequest) + return + } + + // set up target + var protocol string + if r.URL.Scheme != "" { + protocol = r.URL.Scheme + "://" + } else { + protocol = "http://" + } + + var ( + address = fmt.Sprintf("%s:%s", network.Private, port) + target = fmt.Sprintf("%s%s%s", protocol, address, r.RequestURI) + ) + + url, err := url.Parse(target) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // set up forwarder, retrieving from cache if available, otherwise set up new + var proxy *httputil.ReverseProxy + if proxy = e.cache.Get(fmt.Sprintf("%s-%s", n.NetworkID, feature)); proxy == nil { + proxy = newProxy(feature, url, e.l) + e.cache.Cache(fmt.Sprintf("%s-%s", n.NetworkID, feature), proxy) + } + + // serve proxy request + proxy.ServeHTTP(w, r) +} + +// Status reports on proxy status +func (e *Engine) Status(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + render.JSON(w, r, map[string]string{ + "status": "online", + "version": e.version, + }) +} + +// NetworkStatus reports on the status of a network +func (e *Engine) NetworkStatus(w http.ResponseWriter, r *http.Request) { + if _, ok := r.Context().Value(keyNetwork).(*ipfs.NodeInfo); !ok { + http.Error(w, http.StatusText(422), 422) + return + } + + w.WriteHeader(http.StatusOK) + render.JSON(w, r, map[string]string{ + "status": "registered", + }) +} diff --git a/delegator/engine_test.go b/delegator/engine_test.go new file mode 100644 index 0000000..92b737e --- /dev/null +++ b/delegator/engine_test.go @@ -0,0 +1,256 @@ +package delegator + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/RTradeLtd/database/models" + + "github.com/RTradeLtd/Nexus/config" + "github.com/RTradeLtd/Nexus/ipfs" + "github.com/RTradeLtd/Nexus/log" + "github.com/RTradeLtd/Nexus/registry" + "github.com/RTradeLtd/Nexus/temporal/mock" + "github.com/go-chi/chi" +) + +func TestEngine_Run(t *testing.T) { + // claim port for testing unavailable port + if port, err := net.Listen("tcp", "127.0.0.1:69"); err != nil && port != nil { + defer port.Close() + } + + var l, _ = log.NewLogger("", true) + type args struct { + opts config.Delegator + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"no cert", + args{config.Delegator{ + Host: "127.0.0.1", + Port: "", + }}, + false}, + {"invalid port", + args{config.Delegator{ + Host: "127.0.0.1", + Port: "69", + }}, + true}, + {"invalid cert", + args{config.Delegator{ + Host: "127.0.0.1", + Port: "", + TLS: config.TLS{CertPath: "../README.md"}, + }}, + true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var networks = &mock.FakePrivateNetworks{} + var e = New(l, "test", time.Minute, []byte("hello"), nil, networks) + var ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := e.Run(ctx, tt.args.opts); (err != nil) != tt.wantErr { + t.Errorf("Engine.Run() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestEngine_NetworkContext(t *testing.T) { + var l, _ = log.NewLogger("", true) + type args struct { + nodeName string + key contextKey + target string + } + tests := []struct { + name string + args args + wantNode bool + wantCode int + }{ + {"non existent node", args{"hello", keyNetwork, "bye"}, false, http.StatusNotFound}, + {"invalid key", args{"hello", keyFeature, "hello"}, false, http.StatusNotFound}, + {"find node", args{"hello", keyNetwork, "hello"}, true, http.StatusOK}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var networks = &mock.FakePrivateNetworks{} + var e = New(l, "test", time.Second, []byte("hello"), + registry.New(l, config.New().Ports, &ipfs.NodeInfo{ + NetworkID: tt.args.nodeName, + }), networks) + + // set up route context and request + var route = chi.NewRouteContext() + route.URLParams.Add(string(tt.args.key), tt.args.target) + var req = httptest.NewRequest("GET", "/", nil). + WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, route)) + + // test handler + var rec = httptest.NewRecorder() + var n *ipfs.NodeInfo + e.NetworkContext(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + var ok bool + n, ok = r.Context().Value(keyNetwork).(*ipfs.NodeInfo) + if (!ok || n == nil) && tt.wantNode { + t.Errorf("expected ipfs node, found '%v'", r.Context().Value(keyNetwork)) + } + if tt.wantNode && n.NetworkID != tt.args.nodeName { + t.Errorf("expected node named '%s', found '%s'", tt.args.nodeName, n.NetworkID) + } + return + })).ServeHTTP(rec, req) + if rec.Code != tt.wantCode { + t.Errorf("expected status '%d', found '%d'", tt.wantCode, rec.Code) + } + }) + } +} + +func TestEngine_Redirect(t *testing.T) { + var l, _ = log.NewLogger("", true) + type fields struct { + node *ipfs.NodeInfo + network *models.HostedIPFSPrivateNetwork + networkErr error + } + type args struct { + token string + route map[contextKey]string + } + tests := []struct { + name string + fields fields + args args + wantCode int + }{ + {"no network", fields{nil, nil, nil}, args{"", nil}, http.StatusUnprocessableEntity}, + {"no feature", fields{&ipfs.NodeInfo{}, nil, nil}, args{"", nil}, http.StatusBadRequest}, + {"bad feature", + fields{&ipfs.NodeInfo{}, nil, nil}, + args{"", map[contextKey]string{keyFeature: "bobheadxi"}}, + http.StatusBadRequest}, + {"OK: swarm", + fields{&ipfs.NodeInfo{Ports: ipfs.NodePorts{Swarm: "5000"}}, nil, nil}, + args{"", map[contextKey]string{keyFeature: "swarm"}}, + http.StatusBadGateway}, // badgateway because proxy points to nothing + {"api + bad token", + fields{&ipfs.NodeInfo{Ports: ipfs.NodePorts{API: "5000"}}, nil, nil}, + args{"asdf", map[contextKey]string{keyFeature: "api"}}, + http.StatusUnauthorized}, + {"api + good token + no database entry", + fields{&ipfs.NodeInfo{Ports: ipfs.NodePorts{API: "5000"}}, nil, errors.New("oh")}, + args{validToken, map[contextKey]string{keyFeature: "api"}}, + http.StatusNotFound}, + {"api + good token + no authorization", + fields{ + &ipfs.NodeInfo{Ports: ipfs.NodePorts{API: "5000"}}, + &models.HostedIPFSPrivateNetwork{}, + nil}, + args{validToken, map[contextKey]string{keyFeature: "api"}}, + http.StatusForbidden}, + {"api + good token + disallowed origin", + fields{ + &ipfs.NodeInfo{Ports: ipfs.NodePorts{API: "5000"}}, + &models.HostedIPFSPrivateNetwork{ + Users: []string{"testuser"}, + APIAllowedOrigin: "https://www.google.com", + }, + nil}, + args{validToken, map[contextKey]string{keyFeature: "api"}}, + http.StatusBadGateway}, // badgateway because proxy points to nothing + {"OK: api + good token + authorization", + fields{ + &ipfs.NodeInfo{Ports: ipfs.NodePorts{API: "5000"}}, + &models.HostedIPFSPrivateNetwork{ + Users: []string{"testuser"}, + }, + nil}, + args{validToken, map[contextKey]string{keyFeature: "api"}}, + http.StatusBadGateway}, // badgateway because proxy points to nothing + {"gateway + good token + no network", + fields{ + &ipfs.NodeInfo{Ports: ipfs.NodePorts{Gateway: "5000"}}, + nil, + errors.New("oh")}, + args{validToken, map[contextKey]string{keyFeature: "gateway"}}, + http.StatusNotFound}, + {"gateway + good token + not public", + fields{ + &ipfs.NodeInfo{Ports: ipfs.NodePorts{Gateway: "5000"}}, + &models.HostedIPFSPrivateNetwork{ + GatewayPublic: false, + }, + nil}, + args{validToken, map[contextKey]string{keyFeature: "gateway"}}, + http.StatusNotFound}, + {"OK: gateway + good token + public", + fields{ + &ipfs.NodeInfo{Ports: ipfs.NodePorts{Gateway: "5000"}}, + &models.HostedIPFSPrivateNetwork{ + GatewayPublic: true, + }, + nil}, + args{validToken, map[contextKey]string{keyFeature: "gateway"}}, + http.StatusBadGateway}, // badgateway because proxy points to nothing + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var networks = &mock.FakePrivateNetworks{} + var e = New(l, "test", time.Second, defaultTestKey, + registry.New(l, config.New().Ports), networks) + + var route = chi.NewRouteContext() + if tt.args.route != nil { + for key, val := range tt.args.route { + route.URLParams.Add(string(key), val) + } + } + + networks.GetNetworkByNameReturns(tt.fields.network, tt.fields.networkErr) + + var ( + req = httptest.NewRequest("GET", "/", nil). + WithContext( + context.WithValue( + context.WithValue( + context.Background(), + keyNetwork, tt.fields.node), + chi.RouteCtxKey, route)) + rec = httptest.NewRecorder() + ) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tt.args.token)) + e.Redirect(rec, req) + if rec.Code != tt.wantCode { + t.Logf("received '%v'", rec.Result().Status) + t.Errorf("expected status '%d', found '%d'", tt.wantCode, rec.Code) + } + }) + } +} + +func TestEngine_Status(t *testing.T) { + var l, _ = log.NewLogger("", true) + var networks = &mock.FakePrivateNetworks{} + var e = New(l, "test", time.Second, []byte("hello"), registry.New(l, config.New().Ports), networks) + var req = httptest.NewRequest("GET", "/", nil) + var rec = httptest.NewRecorder() + e.Status(rec, req) + if rec.Code != http.StatusOK { + t.Errorf("expected status '%d', found '%d'", http.StatusOK, rec.Code) + } +} diff --git a/delegator/proxy.go b/delegator/proxy.go new file mode 100644 index 0000000..66df620 --- /dev/null +++ b/delegator/proxy.go @@ -0,0 +1,32 @@ +package delegator + +import ( + "net/http" + "net/http/httputil" + "net/url" + + "go.uber.org/zap" +) + +func newProxy(feature string, target *url.URL, l *zap.SugaredLogger) *httputil.ReverseProxy { + return &httputil.ReverseProxy{ + Director: func(req *http.Request) { + // remove delgator-specific leading elements, e.g. /networks/test_network/api, + // and accomodate for specific cases + switch feature { + case "api": + req.URL.Path = "/api" + stripLeadingSegments(req.URL.Path) + default: + req.URL.Path = stripLeadingSegments(req.URL.Path) + } + + // set other URL properties + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + + l.Debugw("forwarded request", + "path", req.URL.Path, + "url", req.URL) + }, + } +} diff --git a/delegator/proxy_test.go b/delegator/proxy_test.go new file mode 100644 index 0000000..aecedd0 --- /dev/null +++ b/delegator/proxy_test.go @@ -0,0 +1,68 @@ +package delegator + +import ( + "bytes" + "io" + "io/ioutil" + "net/http/httptest" + "net/url" + "reflect" + "testing" + + "github.com/RTradeLtd/Nexus/log" +) + +func Test_newProxy(t *testing.T) { + var l, _ = log.NewLogger("", true) + var testBytes = []byte("{\"hello\":\"world\"}") + type args struct { + feature string + target *url.URL + } + type request struct { + method string + address string + body io.Reader + } + tests := []struct { + name string + args args + req request + want *url.URL + }{ + {"api redirect with GET", + args{"api", &url.URL{Scheme: "http", Host: "127.0.0.1"}}, + request{"GET", "http://127.0.0.1/networks/blah/api/v0/blah", nil}, + &url.URL{Scheme: "http", Host: "127.0.0.1", Path: "/api/v0/blah"}}, + {"api redirect with POST", + args{"api", &url.URL{Scheme: "http", Host: "127.0.0.1"}}, + request{"POST", "http://127.0.0.1/networks/blah/api/v0/blah", bytes.NewReader(testBytes)}, + &url.URL{Scheme: "http", Host: "127.0.0.1", Path: "/api/v0/blah"}}, + {"default redirect", + args{"swarm", &url.URL{Scheme: "http", Host: "127.0.0.1"}}, + request{"GET", "http://127.0.0.1/networks/blah/swarm/blah", nil}, + &url.URL{Scheme: "http", Host: "127.0.0.1", Path: "/blah"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got = newProxy(tt.args.feature, tt.args.target, l) + + // test proxy Director + var r = httptest.NewRequest(tt.req.method, tt.req.address, tt.req.body) + got.Director(r) + + // check target + if !reflect.DeepEqual(r.URL, tt.want) { + t.Errorf("expected URL '%v', got '%v'", tt.want, r.URL) + } + + // check body + if tt.req.body != nil { + found, _ := ioutil.ReadAll(r.Body) + if !reflect.DeepEqual(found, testBytes) { + t.Errorf("expected body '%v', got '%v'", testBytes, found) + } + } + }) + } +} diff --git a/delegator/util.go b/delegator/util.go new file mode 100644 index 0000000..0974b76 --- /dev/null +++ b/delegator/util.go @@ -0,0 +1,12 @@ +package delegator + +import "strings" + +func stripLeadingSegments(path string) string { + var expected = 5 + var parts = strings.SplitN(path, "/", expected) + if len(parts) == expected { + return "/" + parts[expected-1] + } + return path +} diff --git a/delegator/util_test.go b/delegator/util_test.go new file mode 100644 index 0000000..18988aa --- /dev/null +++ b/delegator/util_test.go @@ -0,0 +1,24 @@ +package delegator + +import "testing" + +func Test_stripLeadingPaths(t *testing.T) { + type args struct { + path string + } + tests := []struct { + name string + args args + want string + }{ + {"remove prefix", args{"/networks/test_network/api/version"}, "/version"}, + {"no prefix removed", args{"/api/version"}, "/api/version"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := stripLeadingSegments(tt.args.path); got != tt.want { + t.Errorf("stripLeadingPaths() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod index 9121aee..b40208d 100644 --- a/go.mod +++ b/go.mod @@ -6,16 +6,22 @@ require ( github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/RTradeLtd/config v1.0.9 github.com/RTradeLtd/ctl v0.0.0-20181106024051-2febb33f6fd1 - github.com/RTradeLtd/database v1.1.0 - github.com/RTradeLtd/grpc v1.2.0 + github.com/RTradeLtd/database v1.3.0 + github.com/RTradeLtd/gorm v1.9.2 // indirect + github.com/RTradeLtd/grpc v1.4.0 github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac // indirect + github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae // indirect github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/docker/distribution v2.7.0-rc.0.0.20181002220433-1cb4180b1a5b+incompatible // indirect github.com/docker/docker v0.7.3-0.20181013152810-ee6fc90b2c80 github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.3.3 // indirect github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect + github.com/go-chi/chi v3.3.3+incompatible + github.com/go-chi/cors v1.0.0 + github.com/go-chi/render v1.0.1 github.com/go-sql-driver/mysql v1.4.0 // indirect github.com/gogo/protobuf v1.1.1 // indirect github.com/google/go-cmp v0.2.0 // indirect diff --git a/go.sum b/go.sum index 4a7157e..6fd14ce 100644 --- a/go.sum +++ b/go.sum @@ -10,27 +10,47 @@ github.com/RTradeLtd/config v1.0.9 h1:wcoRyjkn2IedNGKEDXaKIOe3fEL/jyIx8JKQWTclHB github.com/RTradeLtd/config v1.0.9/go.mod h1:FVv/bU49cFXT3MRNrPe1VztMBxHQW6MS/DHWqtxNiRc= github.com/RTradeLtd/ctl v0.0.0-20181106024051-2febb33f6fd1 h1:O7h7S3BVpIxuu60QmXUNoHbqBEmUBvMzfDU5QdV4CrI= github.com/RTradeLtd/ctl v0.0.0-20181106024051-2febb33f6fd1/go.mod h1:9CTUuVY4izdvD0cTBrzGYULrS/If22uGQWkkAwfEAEA= -github.com/RTradeLtd/database v1.0.7 h1:OKBAa+XCQNs3KY71PIsnzr/zLNnTL7/dz3Nxgn0h0gU= -github.com/RTradeLtd/database v1.0.7/go.mod h1:LJxu9aB7DphTM74tWtRq0t30iPTkscnxuM683GuMEYY= -github.com/RTradeLtd/database v1.0.8-0.20181130234800-f972da481bb4 h1:8a2Kr6eZlxPRht36Gtn3KxO2cppOZ58Rw4dYu3mJTjY= -github.com/RTradeLtd/database v1.0.8-0.20181130234800-f972da481bb4/go.mod h1:LJxu9aB7DphTM74tWtRq0t30iPTkscnxuM683GuMEYY= github.com/RTradeLtd/database v1.1.0 h1:Yr22JWdvu+OM/26fG5kIUMymm+wD3p7mlTmPNrUppJE= github.com/RTradeLtd/database v1.1.0/go.mod h1:LJxu9aB7DphTM74tWtRq0t30iPTkscnxuM683GuMEYY= -github.com/RTradeLtd/grpc v0.0.0-20181114192314-a01354496a0c h1:/WwY/Yh5oyyUT8nBUi68Iegs7Azu+xVol6RiuBbE/bM= -github.com/RTradeLtd/grpc v0.0.0-20181114192314-a01354496a0c/go.mod h1:wtgJzW1fsgyF4iRl9Fdz5dg2Xa6uuo01msxyAPQJHks= -github.com/RTradeLtd/grpc v1.0.7-0.20181210212311-568bb7c09779 h1:s198785EtTDHr7BiL1GYhfD8UW3u40K52dvHfd+WVy8= -github.com/RTradeLtd/grpc v1.0.7-0.20181210212311-568bb7c09779/go.mod h1:wtgJzW1fsgyF4iRl9Fdz5dg2Xa6uuo01msxyAPQJHks= +github.com/RTradeLtd/database v1.2.1-0.20190115084042-489885b20ba2 h1:NyCQENND45rUtncvrxkFGEIH4tbNRGRlUI/7GCbh3Rk= +github.com/RTradeLtd/database v1.2.1-0.20190115084042-489885b20ba2/go.mod h1:LJxu9aB7DphTM74tWtRq0t30iPTkscnxuM683GuMEYY= +github.com/RTradeLtd/database v1.2.1-0.20190116023732-0ff8e31c3757 h1:M1rVYMpCoI+a8SVqEjctgy+bFB1/6d8bLMWYB9fmU1s= +github.com/RTradeLtd/database v1.2.1-0.20190116023732-0ff8e31c3757/go.mod h1:LJxu9aB7DphTM74tWtRq0t30iPTkscnxuM683GuMEYY= +github.com/RTradeLtd/database v1.2.1-0.20190116031709-d8f3d9f2e53c h1:+JCVcgR2hLPZn5TI/AtVOg4A20Z7rJZ3ZybUMuA5Hk4= +github.com/RTradeLtd/database v1.2.1-0.20190116031709-d8f3d9f2e53c/go.mod h1:LJxu9aB7DphTM74tWtRq0t30iPTkscnxuM683GuMEYY= +github.com/RTradeLtd/database v1.2.1-0.20190117233906-aef608ae6fe5 h1:s53QjOiZydCtpNN2Ndc9enR0Xqe5mvSRlYquxOIa39Q= +github.com/RTradeLtd/database v1.2.1-0.20190117233906-aef608ae6fe5/go.mod h1:LJxu9aB7DphTM74tWtRq0t30iPTkscnxuM683GuMEYY= +github.com/RTradeLtd/database v1.2.1-0.20190117234801-0805c6baabf5 h1:nLDIiSOngzjiXyUH44MQngjOMV7bN+81PujLUxLUNRA= +github.com/RTradeLtd/database v1.2.1-0.20190117234801-0805c6baabf5/go.mod h1:LJxu9aB7DphTM74tWtRq0t30iPTkscnxuM683GuMEYY= +github.com/RTradeLtd/database v1.3.0 h1:fVq1qp/STx9Pp99lRrMoIz5JbyS3MU6dQOlFTuA/byU= +github.com/RTradeLtd/database v1.3.0/go.mod h1:LJxu9aB7DphTM74tWtRq0t30iPTkscnxuM683GuMEYY= +github.com/RTradeLtd/gorm v1.9.2 h1:pbk8/C7k2h6fwYXXVGjrceJ0zTri7vAQY0QcOICT4tI= +github.com/RTradeLtd/gorm v1.9.2/go.mod h1:FaVJ0C0yhoQGrKSsMaTl1p05GfuV+63S+P18+H9ETAg= github.com/RTradeLtd/grpc v1.2.0 h1:wiznfut+nEJiueC6AYldeFP666A0+oW9NzFckno+zm0= github.com/RTradeLtd/grpc v1.2.0/go.mod h1:wtgJzW1fsgyF4iRl9Fdz5dg2Xa6uuo01msxyAPQJHks= +github.com/RTradeLtd/grpc v1.3.0 h1:Iao6opai9SokXcYH7mEdS6r5vhDkmwLFmQueHp2gN9U= +github.com/RTradeLtd/grpc v1.3.0/go.mod h1:wtgJzW1fsgyF4iRl9Fdz5dg2Xa6uuo01msxyAPQJHks= +github.com/RTradeLtd/grpc v1.3.1-0.20190117020150-d2c0499663f5 h1:Rbge8qc0tUgaJhpR88uQPhz47k0YJ3Bvcj1WiLjuAXg= +github.com/RTradeLtd/grpc v1.3.1-0.20190117020150-d2c0499663f5/go.mod h1:wtgJzW1fsgyF4iRl9Fdz5dg2Xa6uuo01msxyAPQJHks= +github.com/RTradeLtd/grpc v1.3.1-0.20190117234053-c6013f9f3a6d h1:pfM+jWu1+8LC8eNJLUIot3sOV2B4tLGkvuN/KQSXrSc= +github.com/RTradeLtd/grpc v1.3.1-0.20190117234053-c6013f9f3a6d/go.mod h1:wtgJzW1fsgyF4iRl9Fdz5dg2Xa6uuo01msxyAPQJHks= +github.com/RTradeLtd/grpc v1.3.1-0.20190118001554-710761d260fe h1:6Ns2UnHGmq6/ba6hnF4x19qC30VXup9wRrN9HMSfYr0= +github.com/RTradeLtd/grpc v1.3.1-0.20190118001554-710761d260fe/go.mod h1:wtgJzW1fsgyF4iRl9Fdz5dg2Xa6uuo01msxyAPQJHks= +github.com/RTradeLtd/grpc v1.4.0 h1:p9/Firm+Qi8W7O+11VzPTyex6IifRxj6e3i3oNpifvw= +github.com/RTradeLtd/grpc v1.4.0/go.mod h1:wtgJzW1fsgyF4iRl9Fdz5dg2Xa6uuo01msxyAPQJHks= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac h1:/zx+Hglw2JN/pwVam1Z8cTCTl4pWyrbvOn2oooqCQSs= github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= +github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae h1:2Zmk+8cNvAGuY8AyvZuWpUdpQUAXwfom4ReVMe/CTIo= +github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f h1:WH0w/R4Yoey+04HhFxqZ6VX6I0d7RMyw5aXQ9UTvQPs= github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/distribution v2.7.0-rc.0.0.20181002220433-1cb4180b1a5b+incompatible h1:n4a7qrsSAlu07ALkG84Ve8cmiDA2KDv2Zbf4OpsOcn8= github.com/docker/distribution v2.7.0-rc.0.0.20181002220433-1cb4180b1a5b+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.7.3-0.20181013152810-ee6fc90b2c80 h1:0Y54ClqYJM6BBfku2gfFchxFdnp8ddz388VuJBhh5Fs= @@ -41,6 +61,12 @@ github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/go-chi/chi v3.3.3+incompatible h1:KHkmBEMNkwKuK4FdQL7N2wOeB9jnIx7jR5wsuSBEFI8= +github.com/go-chi/chi v3.3.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0= +github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I= +github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= +github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= diff --git a/ipfs/client.go b/ipfs/client.go index e387bae..b839abc 100644 --- a/ipfs/client.go +++ b/ipfs/client.go @@ -7,15 +7,18 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "time" - "github.com/RTradeLtd/Nexus/log" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" docker "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "go.uber.org/zap" + + "github.com/RTradeLtd/Nexus/log" + "github.com/RTradeLtd/Nexus/network" ) // Client is the primary implementation of the NodeClient interface. Instantiate @@ -58,13 +61,21 @@ func (c *Client) Nodes(ctx context.Context) ([]*NodeInfo, error) { failed = 0 ) for _, container := range ctrs { + var l = c.l.With("container.id", container.ID, "container.name", container.Names[0]) n, err := newNode(container.ID, container.Names[0], container.Labels) if err != nil { + l.Debugw("container ignored", "reason", err) ignored++ continue } - if isStopped(container.Status) { + n.updateFromContainerDetails(&container) + l = l.With("node", n) + if isStopped(container.State) { if err := restartNode(n); err != nil { + l.Errorw("node container failed to restart - removing", "error", err) + if err := c.StopNode(ctx, &n); err != nil { + l.Warn("failed to stop node", "error", err) + } failed++ continue } @@ -75,8 +86,9 @@ func (c *Client) Nodes(ctx context.Context) ([]*NodeInfo, error) { // report activity c.l.Infow("all nodes checked", + "found", len(ctrs), + "valid", len(nodes), "ignored", ignored, - "found", len(nodes), "restarts", restarts, "failed_restarts", failed) @@ -111,12 +123,22 @@ func (c *Client) CreateNode(ctx context.Context, n *NodeInfo, opts NodeOpts) err // set up basic configuration var ( ports = nat.PortMap{ - // public ports - "4001/tcp": []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: n.Ports.Swarm}}, - "5001/tcp": []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: n.Ports.API}}, - - // private ports - "8080/tcp": []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: n.Ports.Gateway}}, + // TODO: make this private - blocked by lack of multiaddr support for /http + // paths, which means delegator can't work with go-ipfs swarm. + // See https://github.com/multiformats/multiaddr/issues/63 + containerSwarmPort + "/tcp": []nat.PortBinding{ + {HostIP: network.Public, HostPort: n.Ports.Swarm}}, + + // API server connections can be made via delegator. Suffers from same + // issue as above, but direct API exposure is dangeorous since it is + // authenticated. Delegator can handle authentication + containerAPIPort + "/tcp": []nat.PortBinding{ + {HostIP: network.Private, HostPort: n.Ports.API}}, + + // Gateway connections can be made via delegator, with access controlled + // by database + containerGatewayPort + "/tcp": []nat.PortBinding{ + {HostIP: network.Private, HostPort: n.Ports.Gateway}}, } volumes = []string{ c.getDataDir(n.NetworkID) + ":/data/ipfs", @@ -157,7 +179,7 @@ func (c *Client) CreateNode(ctx context.Context, n *NodeInfo, opts NodeOpts) err var start = time.Now() l = l.With("container.name", n.ContainerName) - l.Infow("creating network container", + l.Debugw("creating network container", "container.config", containerConfig, "container.host_config", containerHostConfig) resp, err := c.d.ContainerCreate(ctx, containerConfig, containerHostConfig, nil, n.ContainerName) @@ -199,7 +221,7 @@ func (c *Client) CreateNode(ctx context.Context, n *NodeInfo, opts NodeOpts) err // bootstrap peers if required if len(n.BootstrapPeers) > 0 { - l.Infow("bootstrapping network node with provided peers") + l.Debugw("bootstrapping network node with provided peers") if err := c.bootstrapNode(ctx, n.DockerID, n.BootstrapPeers...); err != nil { l.Warnw("failed to bootstrap node - stopping container", "error", err, "start.duration", time.Since(start)) @@ -233,7 +255,7 @@ func (c *Client) UpdateNode(ctx context.Context, n *NodeInfo) error { // update Docker-managed configuration var res = containerResources(n) - l.Infow("updaing docker-based configuration", + l.Debugw("updating docker-based configuration", "container.resources", containerResources(n)) resp, err = c.d.ContainerUpdate(ctx, n.DockerID, container.UpdateConfig{Resources: res}) if err != nil { @@ -247,7 +269,7 @@ func (c *Client) UpdateNode(ctx context.Context, n *NodeInfo) error { } // update IPFS configuration - currently requires restart, see function docs - l.Infow("updating IPFS node configuration", + l.Debugw("updating IPFS node configuration", "node.disk", n.Resources.DiskGB) if err = c.updateIPFSConfig(ctx, n); err != nil { l.Errorw("failed to update IPFS daemon configuration", @@ -311,7 +333,7 @@ func (c *Client) RemoveNode(ctx context.Context, network string) error { l = c.l.With("network_id", network, "data_dir", dir) ) - l.Info("removing node assets") + l.Debug("removing node assets") if err := os.RemoveAll(dir); err != nil { l.Warnw("error encountered removing node directories", "error", err, @@ -326,6 +348,8 @@ func (c *Client) RemoveNode(ctx context.Context, network string) error { // NodeStats provides details about a node container type NodeStats struct { + PeerID string + PeerKey string Uptime time.Duration DiskUsage int64 Stats interface{} @@ -334,44 +358,61 @@ type NodeStats struct { // NodeStats retrieves statistics about the provided node func (c *Client) NodeStats(ctx context.Context, n *NodeInfo) (NodeStats, error) { var start = time.Now() + var l = c.l.With("node", n) // retrieve details from stats API s, err := c.d.ContainerStats(ctx, n.DockerID, false) if err != nil { - return NodeStats{}, err + l.Errorw("failed to get container stats", "error", err) + return NodeStats{}, errors.New("failed to get node stats") } defer s.Body.Close() b, err := ioutil.ReadAll(s.Body) if err != nil { - return NodeStats{}, err + l.Errorw("failed to read container stats", "error", err) + return NodeStats{}, errors.New("failed to get node stats") } var stats rawContainerStats if err = json.Unmarshal(b, &stats); err != nil { - return NodeStats{}, err + l.Errorw("failed to read container stats", "error", err) + return NodeStats{}, errors.New("failed to get node stats") } // retrieve details from container inspection info, err := c.d.ContainerInspect(ctx, n.DockerID) if err != nil { - return NodeStats{}, err + l.Errorw("failed to inspect container", "error", err) + return NodeStats{}, errors.New("failed to get node stats") } created, err := time.Parse(time.RFC3339, info.Created) if err != nil { - return NodeStats{}, err + l.Errorw("failed to read container detail", "error", err) + return NodeStats{}, errors.New("failed to get node stats") } // check disk usage usage, err := dirSize(n.DataDir) if err != nil { - return NodeStats{}, fmt.Errorf("failed to calculate disk usage: %s", err.Error()) + l.Errorw("failed to calculate disk usage", "error", err) + return NodeStats{}, errors.New("failed to calculate disk usage") + } + + // get peer ID + var cfgPath = filepath.Join(n.DataDir, "config") + peer, err := getConfig(cfgPath) + if err != nil { + l.Errorw("failed to read node configuration", "error", err, "path", cfgPath) + return NodeStats{}, fmt.Errorf("failed to get network node configuration") } - c.l.Infow("retrieved node container data", + c.l.Debugw("retrieved node container data", "network_id", n.NetworkID, "docker_id", n.DockerID, "stat.duration", time.Since(start)) return NodeStats{ + PeerID: peer.Identity.PeerID, + PeerKey: peer.Identity.PrivKey, Uptime: time.Since(created), Stats: stats, DiskUsage: usage, diff --git a/ipfs/config.go b/ipfs/config.go new file mode 100644 index 0000000..cffa3da --- /dev/null +++ b/ipfs/config.go @@ -0,0 +1,41 @@ +package ipfs + +import ( + "encoding/json" + "fmt" + "io/ioutil" + + internal "github.com/RTradeLtd/Nexus/ipfs/internal" +) + +// GoIPFSConfig is a subset of go-ipfs's configuration structure +type GoIPFSConfig struct { + Identity struct { + PeerID string + PrivKey string + } +} + +func getConfig(path string) (*GoIPFSConfig, error) { + /* #nosec */ + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to open IPFS configuration at '%s': %s", path, err.Error()) + } + var c GoIPFSConfig + if err := json.Unmarshal(b, &c); err != nil { + return nil, fmt.Errorf("error encountered reading contents of configuration at '%s': '%s'", + path, err.Error()) + } + return &c, nil +} + +func newNodeStartScript(diskMax int) (string, error) { + f, err := internal.ReadFile("ipfs/internal/ipfs_start.sh") + if err != nil { + return "", fmt.Errorf("failed to generate startup script: %s", err.Error()) + } + return fmt.Sprintf(string(f), + diskMax, + ), nil +} diff --git a/ipfs/config_test.go b/ipfs/config_test.go new file mode 100644 index 0000000..62c1ace --- /dev/null +++ b/ipfs/config_test.go @@ -0,0 +1,57 @@ +package ipfs + +import ( + "fmt" + "io/ioutil" + "reflect" + "testing" +) + +func Test_getConfig(t *testing.T) { + type args struct { + path string + } + tests := []struct { + name string + args args + want *GoIPFSConfig + wantErr bool + }{ + {"no file", args{"./what"}, nil, true}, + {"wrong file", args{"./ipfs.go"}, nil, true}, + {"right file", + args{"../testenv/ipfs/nodes/1/config"}, + &GoIPFSConfig{struct{ PeerID, PrivKey string }{"QmbzqXEoWcAEWC2AERztspfZzgLSUqGoFiWEvC2XsEGnCG", "CAASpgkwggSiAgEAAoIBAQCqbaLjmbfDoABV3FxXlYkiiAWzQee2k+kFBRgKiDdGORH9eaH951ifv2vHe3xKUROMNaGTRvN4sIeI7tBSCrBSDRJ7PgS5zcdLwKPl8DID7/kkZ+QK7BmEnIcFvCCFxOc3DUROu7oT16kH4chWqoEWPH0SWp3DjIN3zN6FC0oizCKTZeg2HZDZX1nkIhpkKPs3Bp0X4kQVA6EW5xugez3uzbkVcxGzBvPtArisXlTE/GLG31SwrF8OnYbAqZB+TtEm5i7r5kHj4iJScsv1xBVxrdzBDOE16KW3inDGxPnW6vwOPzcN4fTMZ3wgRk89Cm5iI/MmtkiaUjGJPr08+xWzAgMBAAECggEAbla5BN36qX6neO9IIbRAqsih2CKtH/m2/XcEz5zNHHvKd+8Nv9LN/+7wmqAKIhtHqpj2WOGws8ymkzL6UIN3EEhCVOQcLydZBmRcOHxABWiSRs20SJX/F2o3yLC55aFLiMrgFJFZsYsIdn/pMqMFHB5hY0ajqX0JiMBsuHpMryWnm/5sllp7MFc6yEQF0UQ+z+Dx83hYoh/40qwHiuohm7i1MQYx9qKjMMKLMoHdGlg6g3nMRgeXw1xI8ry50A2AQjcQsbpYN+/FxASnf9l1VuIFgOnzGmbBGGC5I+SStWoupinrNH68MuQxCqmIiexYPCFvPW/YjThIMqYZ37+ooQKBgQDDxDdJ6G899qqbrR4ly/XWbe+CDxf8ueMvWsirvkL3ozufsvNPEIrXXKxuMex0JB5x7wPPr7jhwSNVuGD1bsu8R2b66n985rbTgFT1B5eGuW9FHuhPoC+bNeawi/W7ZsCQKCsUHY1orA2NAanaSnzSS9ico3F6gQ/txafwoIMlCwKBgQDe3aBC4TFZa79nRpOyPiTU8y5iPSJ8xf/6f2GA9voXLLnII8RLZXVtc71zj1OIDks//fNwDnqdwcczalqCfNSdrXWavRJ7dqKOHPQBpO8eGNHtYBd876ushbUR4gjrkxgXZ2uyrXe+D8TcDHWCdCJ3aa3FjzuMv/JJ4Fe/ErDq+QKBgDRdNdTFIYxXgIcnpVrC1b1HprsJQodNSaGPDQIzYEJRHU+4VDCf4iN9HHpVTEQ8rRAYuNJC1Jc+TC9PpE/CFSkFiFwxgWxtYhXsy8zG/RcCXusEO2uhE1rW7h/nMBGyiGuG8w7sYLjQ3McM3NwQ9JZjx0sOxPnZr+MP7b4FkU7FAoGASFS5rLsVnyX/Ku+XA+RzY8HBLhUVWlWQrKYm6Qo/RMI5UaF6FdZJ9En6FMVRoPiyp4QuPBIW7Zh0pFVCJtOI1dv0LVJr6zIns+PltZroGGaJy3bCaMQIfaeviqxHpN1Kll30cDsof8DybVCF2t8CSKs9wL6p3xZ09lEfaV4RmVECgYASBPs0qAdJ+WS2i8OErkN/HZRfNQwRo83YPeYTbhhe1oHfXyQqBM1CC965+Xbh82t8xYPG3Nc1Av63Rfh/D9+J3ylduUqpNxN0xgdQWYXzDA7Lzz+GkHwGGVAlbabK966m6PDZIpCxgRU/pLbUQ9YIJUORCPXp6psl/yWTgn9mgA=="}}, + false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getConfig(tt.args.path) + if (err != nil) != tt.wantErr { + t.Errorf("getConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getConfig() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newNodeStartScript(t *testing.T) { + var ( + disk = 10 + ) + + f, _ := ioutil.ReadFile("./internal/ipfs_start.sh") + expected := fmt.Sprintf(string(f), disk) + + got, err := newNodeStartScript(disk) + if err != nil { + t.Error("unexpected err:", err.Error()) + return + } + if got != expected { + t.Errorf("expected '%s', got '%s'", expected, got) + } +} diff --git a/ipfs/docker.go b/ipfs/docker.go index 4a5d8c8..a55296a 100644 --- a/ipfs/docker.go +++ b/ipfs/docker.go @@ -6,6 +6,12 @@ import ( "github.com/docker/docker/api/types/container" ) +const ( + containerSwarmPort = "4001" + containerAPIPort = "5001" + containerGatewayPort = "8080" +) + // containerResources generates Docker resource constraints for a container, // based on documentation: // https://docs.docker.com/config/containers/resource_constraints/ diff --git a/ipfs/internal/ipfs_start.sh b/ipfs/internal/ipfs_start.sh index 0b36e41..b4c3f7c 100644 --- a/ipfs/internal/ipfs_start.sh +++ b/ipfs/internal/ipfs_start.sh @@ -9,24 +9,27 @@ set -e # arguments provided through string templates DISK_MAX=%dGB +# set variables user=ipfs repo="$IPFS_PATH" +# set user if [ "$(id -u)" -eq 0 ]; then - echo "Changing user to $user" + echo "changing user to $user" # ensure folder is writable su-exec "$user" test -w "$repo" || chown -R -- "$user" "$repo" # restart script with new privileges exec su-exec "$user" "$0" "$@" fi -# 2nd invocation with regular user +# check exec, report version ipfs version +# check for existing repo - otherwise init new one if [ -e "$repo/config" ]; then - echo "Found IPFS fs-repo at $repo" + echo "found IPFS fs-repo at $repo" else - ipfs init + ipfs init --profile server ipfs config Addresses.API /ip4/0.0.0.0/tcp/5001 ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/8080 fi @@ -34,6 +37,9 @@ fi # set datastore quota ipfs config Datastore.StorageMax $DISK_MAX +# release locks +ipfs repo fsck + # if the first argument is daemon if [ "$1" = "daemon" ]; then # filter the first argument until diff --git a/ipfs/internal/scripts.go b/ipfs/internal/scripts.go index 41a37b1..f8c5c0a 100644 --- a/ipfs/internal/scripts.go +++ b/ipfs/internal/scripts.go @@ -1,5 +1,5 @@ -// Code generated by fileb0x at "2019-01-15 18:10:42.338204 -0800 PST m=+0.019009681" from config file "b0x.yml" DO NOT EDIT. -// modification hash(e70c3de5287995db23120d0409369823.e5979db15ff7a7144261cbf60c4e3094) +// Code generated by fileb0x at "2019-01-17 15:54:49.309678 -0800 PST m=+0.017769061" from config file "b0x.yml" DO NOT EDIT. +// modification hash(859c16709295669728355424da49db77.e5979db15ff7a7144261cbf60c4e3094) package internal @@ -36,7 +36,7 @@ type HTTPFS struct { } // FileIpfsInternalIpfsStartSh is "ipfs/internal/ipfs_start.sh" -var FileIpfsInternalIpfsStartSh = []byte("\x23\x21\x2f\x62\x69\x6e\x2f\x73\x68\x0a\x0a\x23\x20\x4d\x6f\x64\x69\x66\x69\x65\x64\x20\x49\x50\x46\x53\x20\x6e\x6f\x64\x65\x20\x69\x6e\x69\x74\x69\x61\x6c\x69\x7a\x61\x74\x69\x6f\x6e\x20\x73\x63\x72\x69\x70\x74\x2e\x0a\x23\x20\x4d\x6f\x75\x6e\x74\x20\x74\x6f\x20\x2f\x75\x73\x72\x2f\x6c\x6f\x63\x61\x6c\x2f\x62\x69\x6e\x2f\x73\x74\x61\x72\x74\x5f\x69\x70\x66\x73\x0a\x23\x20\x53\x6f\x75\x72\x63\x65\x3a\x20\x68\x74\x74\x70\x73\x3a\x2f\x2f\x67\x69\x74\x68\x75\x62\x2e\x63\x6f\x6d\x2f\x69\x70\x66\x73\x2f\x67\x6f\x2d\x69\x70\x66\x73\x2f\x62\x6c\x6f\x62\x2f\x24\x7b\x49\x50\x46\x53\x5f\x56\x45\x52\x53\x49\x4f\x4e\x7d\x2f\x62\x69\x6e\x2f\x63\x6f\x6e\x74\x61\x69\x6e\x65\x72\x5f\x64\x61\x65\x6d\x6f\x6e\x0a\x0a\x73\x65\x74\x20\x2d\x65\x0a\x0a\x23\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x20\x70\x72\x6f\x76\x69\x64\x65\x64\x20\x74\x68\x72\x6f\x75\x67\x68\x20\x73\x74\x72\x69\x6e\x67\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x73\x0a\x44\x49\x53\x4b\x5f\x4d\x41\x58\x3d\x25\x64\x47\x42\x0a\x0a\x75\x73\x65\x72\x3d\x69\x70\x66\x73\x0a\x72\x65\x70\x6f\x3d\x22\x24\x49\x50\x46\x53\x5f\x50\x41\x54\x48\x22\x0a\x0a\x69\x66\x20\x5b\x20\x22\x24\x28\x69\x64\x20\x2d\x75\x29\x22\x20\x2d\x65\x71\x20\x30\x20\x5d\x3b\x20\x74\x68\x65\x6e\x0a\x20\x20\x65\x63\x68\x6f\x20\x22\x43\x68\x61\x6e\x67\x69\x6e\x67\x20\x75\x73\x65\x72\x20\x74\x6f\x20\x24\x75\x73\x65\x72\x22\x0a\x20\x20\x23\x20\x65\x6e\x73\x75\x72\x65\x20\x66\x6f\x6c\x64\x65\x72\x20\x69\x73\x20\x77\x72\x69\x74\x61\x62\x6c\x65\x0a\x20\x20\x73\x75\x2d\x65\x78\x65\x63\x20\x22\x24\x75\x73\x65\x72\x22\x20\x74\x65\x73\x74\x20\x2d\x77\x20\x22\x24\x72\x65\x70\x6f\x22\x20\x7c\x7c\x20\x63\x68\x6f\x77\x6e\x20\x2d\x52\x20\x2d\x2d\x20\x22\x24\x75\x73\x65\x72\x22\x20\x22\x24\x72\x65\x70\x6f\x22\x0a\x20\x20\x23\x20\x72\x65\x73\x74\x61\x72\x74\x20\x73\x63\x72\x69\x70\x74\x20\x77\x69\x74\x68\x20\x6e\x65\x77\x20\x70\x72\x69\x76\x69\x6c\x65\x67\x65\x73\x0a\x20\x20\x65\x78\x65\x63\x20\x73\x75\x2d\x65\x78\x65\x63\x20\x22\x24\x75\x73\x65\x72\x22\x20\x22\x24\x30\x22\x20\x22\x24\x40\x22\x0a\x66\x69\x0a\x0a\x23\x20\x32\x6e\x64\x20\x69\x6e\x76\x6f\x63\x61\x74\x69\x6f\x6e\x20\x77\x69\x74\x68\x20\x72\x65\x67\x75\x6c\x61\x72\x20\x75\x73\x65\x72\x0a\x69\x70\x66\x73\x20\x76\x65\x72\x73\x69\x6f\x6e\x0a\x0a\x69\x66\x20\x5b\x20\x2d\x65\x20\x22\x24\x72\x65\x70\x6f\x2f\x63\x6f\x6e\x66\x69\x67\x22\x20\x5d\x3b\x20\x74\x68\x65\x6e\x0a\x20\x20\x65\x63\x68\x6f\x20\x22\x46\x6f\x75\x6e\x64\x20\x49\x50\x46\x53\x20\x66\x73\x2d\x72\x65\x70\x6f\x20\x61\x74\x20\x24\x72\x65\x70\x6f\x22\x0a\x65\x6c\x73\x65\x0a\x20\x20\x69\x70\x66\x73\x20\x69\x6e\x69\x74\x0a\x20\x20\x69\x70\x66\x73\x20\x63\x6f\x6e\x66\x69\x67\x20\x41\x64\x64\x72\x65\x73\x73\x65\x73\x2e\x41\x50\x49\x20\x2f\x69\x70\x34\x2f\x30\x2e\x30\x2e\x30\x2e\x30\x2f\x74\x63\x70\x2f\x35\x30\x30\x31\x0a\x20\x20\x69\x70\x66\x73\x20\x63\x6f\x6e\x66\x69\x67\x20\x41\x64\x64\x72\x65\x73\x73\x65\x73\x2e\x47\x61\x74\x65\x77\x61\x79\x20\x2f\x69\x70\x34\x2f\x30\x2e\x30\x2e\x30\x2e\x30\x2f\x74\x63\x70\x2f\x38\x30\x38\x30\x0a\x66\x69\x0a\x0a\x23\x20\x73\x65\x74\x20\x64\x61\x74\x61\x73\x74\x6f\x72\x65\x20\x71\x75\x6f\x74\x61\x0a\x69\x70\x66\x73\x20\x63\x6f\x6e\x66\x69\x67\x20\x44\x61\x74\x61\x73\x74\x6f\x72\x65\x2e\x53\x74\x6f\x72\x61\x67\x65\x4d\x61\x78\x20\x24\x44\x49\x53\x4b\x5f\x4d\x41\x58\x0a\x0a\x23\x20\x69\x66\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x20\x69\x73\x20\x64\x61\x65\x6d\x6f\x6e\x0a\x69\x66\x20\x5b\x20\x22\x24\x31\x22\x20\x3d\x20\x22\x64\x61\x65\x6d\x6f\x6e\x22\x20\x5d\x3b\x20\x74\x68\x65\x6e\x0a\x20\x20\x23\x20\x66\x69\x6c\x74\x65\x72\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x20\x75\x6e\x74\x69\x6c\x0a\x20\x20\x23\x20\x68\x74\x74\x70\x73\x3a\x2f\x2f\x67\x69\x74\x68\x75\x62\x2e\x63\x6f\x6d\x2f\x69\x70\x66\x73\x2f\x67\x6f\x2d\x69\x70\x66\x73\x2f\x70\x75\x6c\x6c\x2f\x33\x35\x37\x33\x0a\x20\x20\x23\x20\x68\x61\x73\x20\x62\x65\x65\x6e\x20\x72\x65\x73\x6f\x6c\x76\x65\x64\x0a\x20\x20\x73\x68\x69\x66\x74\x0a\x65\x6c\x73\x65\x0a\x20\x20\x23\x20\x70\x72\x69\x6e\x74\x20\x64\x65\x70\x72\x65\x63\x61\x74\x69\x6f\x6e\x20\x77\x61\x72\x6e\x69\x6e\x67\x0a\x20\x20\x23\x20\x67\x6f\x2d\x69\x70\x66\x73\x20\x75\x73\x65\x64\x20\x74\x6f\x20\x68\x61\x72\x64\x63\x6f\x64\x65\x20\x22\x69\x70\x66\x73\x20\x64\x61\x65\x6d\x6f\x6e\x22\x20\x69\x6e\x20\x69\x74\x27\x73\x20\x65\x6e\x74\x72\x79\x70\x6f\x69\x6e\x74\x0a\x20\x20\x23\x20\x74\x68\x69\x73\x20\x77\x6f\x72\x6b\x61\x72\x6f\x75\x6e\x64\x20\x73\x75\x70\x70\x6f\x72\x74\x73\x20\x74\x68\x65\x20\x6e\x65\x77\x20\x73\x79\x6e\x74\x61\x78\x20\x73\x6f\x20\x70\x65\x6f\x70\x6c\x65\x20\x73\x74\x61\x72\x74\x20\x73\x65\x74\x74\x69\x6e\x67\x20\x64\x61\x65\x6d\x6f\x6e\x20\x65\x78\x70\x6c\x69\x63\x69\x74\x6c\x79\x0a\x20\x20\x23\x20\x77\x68\x65\x6e\x20\x6f\x76\x65\x72\x77\x72\x69\x74\x69\x6e\x67\x20\x43\x4d\x44\x0a\x20\x20\x65\x63\x68\x6f\x20\x22\x44\x45\x50\x52\x45\x43\x41\x54\x45\x44\x3a\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x20\x68\x61\x76\x65\x20\x62\x65\x65\x6e\x20\x73\x65\x74\x20\x62\x75\x74\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x20\x69\x73\x6e\x27\x74\x20\x27\x64\x61\x65\x6d\x6f\x6e\x27\x22\x20\x3e\x26\x32\x0a\x66\x69\x0a\x0a\x65\x78\x65\x63\x20\x69\x70\x66\x73\x20\x64\x61\x65\x6d\x6f\x6e\x20\x22\x24\x40\x22\x0a") +var FileIpfsInternalIpfsStartSh = []byte("\x23\x21\x2f\x62\x69\x6e\x2f\x73\x68\x0a\x0a\x23\x20\x4d\x6f\x64\x69\x66\x69\x65\x64\x20\x49\x50\x46\x53\x20\x6e\x6f\x64\x65\x20\x69\x6e\x69\x74\x69\x61\x6c\x69\x7a\x61\x74\x69\x6f\x6e\x20\x73\x63\x72\x69\x70\x74\x2e\x0a\x23\x20\x4d\x6f\x75\x6e\x74\x20\x74\x6f\x20\x2f\x75\x73\x72\x2f\x6c\x6f\x63\x61\x6c\x2f\x62\x69\x6e\x2f\x73\x74\x61\x72\x74\x5f\x69\x70\x66\x73\x0a\x23\x20\x53\x6f\x75\x72\x63\x65\x3a\x20\x68\x74\x74\x70\x73\x3a\x2f\x2f\x67\x69\x74\x68\x75\x62\x2e\x63\x6f\x6d\x2f\x69\x70\x66\x73\x2f\x67\x6f\x2d\x69\x70\x66\x73\x2f\x62\x6c\x6f\x62\x2f\x24\x7b\x49\x50\x46\x53\x5f\x56\x45\x52\x53\x49\x4f\x4e\x7d\x2f\x62\x69\x6e\x2f\x63\x6f\x6e\x74\x61\x69\x6e\x65\x72\x5f\x64\x61\x65\x6d\x6f\x6e\x0a\x0a\x73\x65\x74\x20\x2d\x65\x0a\x0a\x23\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x20\x70\x72\x6f\x76\x69\x64\x65\x64\x20\x74\x68\x72\x6f\x75\x67\x68\x20\x73\x74\x72\x69\x6e\x67\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x73\x0a\x44\x49\x53\x4b\x5f\x4d\x41\x58\x3d\x25\x64\x47\x42\x0a\x0a\x23\x20\x73\x65\x74\x20\x76\x61\x72\x69\x61\x62\x6c\x65\x73\x0a\x75\x73\x65\x72\x3d\x69\x70\x66\x73\x0a\x72\x65\x70\x6f\x3d\x22\x24\x49\x50\x46\x53\x5f\x50\x41\x54\x48\x22\x0a\x0a\x23\x20\x73\x65\x74\x20\x75\x73\x65\x72\x0a\x69\x66\x20\x5b\x20\x22\x24\x28\x69\x64\x20\x2d\x75\x29\x22\x20\x2d\x65\x71\x20\x30\x20\x5d\x3b\x20\x74\x68\x65\x6e\x0a\x20\x20\x65\x63\x68\x6f\x20\x22\x63\x68\x61\x6e\x67\x69\x6e\x67\x20\x75\x73\x65\x72\x20\x74\x6f\x20\x24\x75\x73\x65\x72\x22\x0a\x20\x20\x23\x20\x65\x6e\x73\x75\x72\x65\x20\x66\x6f\x6c\x64\x65\x72\x20\x69\x73\x20\x77\x72\x69\x74\x61\x62\x6c\x65\x0a\x20\x20\x73\x75\x2d\x65\x78\x65\x63\x20\x22\x24\x75\x73\x65\x72\x22\x20\x74\x65\x73\x74\x20\x2d\x77\x20\x22\x24\x72\x65\x70\x6f\x22\x20\x7c\x7c\x20\x63\x68\x6f\x77\x6e\x20\x2d\x52\x20\x2d\x2d\x20\x22\x24\x75\x73\x65\x72\x22\x20\x22\x24\x72\x65\x70\x6f\x22\x0a\x20\x20\x23\x20\x72\x65\x73\x74\x61\x72\x74\x20\x73\x63\x72\x69\x70\x74\x20\x77\x69\x74\x68\x20\x6e\x65\x77\x20\x70\x72\x69\x76\x69\x6c\x65\x67\x65\x73\x0a\x20\x20\x65\x78\x65\x63\x20\x73\x75\x2d\x65\x78\x65\x63\x20\x22\x24\x75\x73\x65\x72\x22\x20\x22\x24\x30\x22\x20\x22\x24\x40\x22\x0a\x66\x69\x0a\x0a\x23\x20\x63\x68\x65\x63\x6b\x20\x65\x78\x65\x63\x2c\x20\x72\x65\x70\x6f\x72\x74\x20\x76\x65\x72\x73\x69\x6f\x6e\x0a\x69\x70\x66\x73\x20\x76\x65\x72\x73\x69\x6f\x6e\x0a\x0a\x23\x20\x63\x68\x65\x63\x6b\x20\x66\x6f\x72\x20\x65\x78\x69\x73\x74\x69\x6e\x67\x20\x72\x65\x70\x6f\x20\x2d\x20\x6f\x74\x68\x65\x72\x77\x69\x73\x65\x20\x69\x6e\x69\x74\x20\x6e\x65\x77\x20\x6f\x6e\x65\x0a\x69\x66\x20\x5b\x20\x2d\x65\x20\x22\x24\x72\x65\x70\x6f\x2f\x63\x6f\x6e\x66\x69\x67\x22\x20\x5d\x3b\x20\x74\x68\x65\x6e\x0a\x20\x20\x65\x63\x68\x6f\x20\x22\x66\x6f\x75\x6e\x64\x20\x49\x50\x46\x53\x20\x66\x73\x2d\x72\x65\x70\x6f\x20\x61\x74\x20\x24\x72\x65\x70\x6f\x22\x0a\x65\x6c\x73\x65\x0a\x20\x20\x69\x70\x66\x73\x20\x69\x6e\x69\x74\x20\x2d\x2d\x70\x72\x6f\x66\x69\x6c\x65\x20\x73\x65\x72\x76\x65\x72\x0a\x20\x20\x69\x70\x66\x73\x20\x63\x6f\x6e\x66\x69\x67\x20\x41\x64\x64\x72\x65\x73\x73\x65\x73\x2e\x41\x50\x49\x20\x2f\x69\x70\x34\x2f\x30\x2e\x30\x2e\x30\x2e\x30\x2f\x74\x63\x70\x2f\x35\x30\x30\x31\x0a\x20\x20\x69\x70\x66\x73\x20\x63\x6f\x6e\x66\x69\x67\x20\x41\x64\x64\x72\x65\x73\x73\x65\x73\x2e\x47\x61\x74\x65\x77\x61\x79\x20\x2f\x69\x70\x34\x2f\x30\x2e\x30\x2e\x30\x2e\x30\x2f\x74\x63\x70\x2f\x38\x30\x38\x30\x0a\x66\x69\x0a\x0a\x23\x20\x73\x65\x74\x20\x64\x61\x74\x61\x73\x74\x6f\x72\x65\x20\x71\x75\x6f\x74\x61\x0a\x69\x70\x66\x73\x20\x63\x6f\x6e\x66\x69\x67\x20\x44\x61\x74\x61\x73\x74\x6f\x72\x65\x2e\x53\x74\x6f\x72\x61\x67\x65\x4d\x61\x78\x20\x24\x44\x49\x53\x4b\x5f\x4d\x41\x58\x0a\x0a\x23\x20\x72\x65\x6c\x65\x61\x73\x65\x20\x6c\x6f\x63\x6b\x73\x0a\x69\x70\x66\x73\x20\x72\x65\x70\x6f\x20\x66\x73\x63\x6b\x0a\x0a\x23\x20\x69\x66\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x20\x69\x73\x20\x64\x61\x65\x6d\x6f\x6e\x0a\x69\x66\x20\x5b\x20\x22\x24\x31\x22\x20\x3d\x20\x22\x64\x61\x65\x6d\x6f\x6e\x22\x20\x5d\x3b\x20\x74\x68\x65\x6e\x0a\x20\x20\x23\x20\x66\x69\x6c\x74\x65\x72\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x20\x75\x6e\x74\x69\x6c\x0a\x20\x20\x23\x20\x68\x74\x74\x70\x73\x3a\x2f\x2f\x67\x69\x74\x68\x75\x62\x2e\x63\x6f\x6d\x2f\x69\x70\x66\x73\x2f\x67\x6f\x2d\x69\x70\x66\x73\x2f\x70\x75\x6c\x6c\x2f\x33\x35\x37\x33\x0a\x20\x20\x23\x20\x68\x61\x73\x20\x62\x65\x65\x6e\x20\x72\x65\x73\x6f\x6c\x76\x65\x64\x0a\x20\x20\x73\x68\x69\x66\x74\x0a\x65\x6c\x73\x65\x0a\x20\x20\x23\x20\x70\x72\x69\x6e\x74\x20\x64\x65\x70\x72\x65\x63\x61\x74\x69\x6f\x6e\x20\x77\x61\x72\x6e\x69\x6e\x67\x0a\x20\x20\x23\x20\x67\x6f\x2d\x69\x70\x66\x73\x20\x75\x73\x65\x64\x20\x74\x6f\x20\x68\x61\x72\x64\x63\x6f\x64\x65\x20\x22\x69\x70\x66\x73\x20\x64\x61\x65\x6d\x6f\x6e\x22\x20\x69\x6e\x20\x69\x74\x27\x73\x20\x65\x6e\x74\x72\x79\x70\x6f\x69\x6e\x74\x0a\x20\x20\x23\x20\x74\x68\x69\x73\x20\x77\x6f\x72\x6b\x61\x72\x6f\x75\x6e\x64\x20\x73\x75\x70\x70\x6f\x72\x74\x73\x20\x74\x68\x65\x20\x6e\x65\x77\x20\x73\x79\x6e\x74\x61\x78\x20\x73\x6f\x20\x70\x65\x6f\x70\x6c\x65\x20\x73\x74\x61\x72\x74\x20\x73\x65\x74\x74\x69\x6e\x67\x20\x64\x61\x65\x6d\x6f\x6e\x20\x65\x78\x70\x6c\x69\x63\x69\x74\x6c\x79\x0a\x20\x20\x23\x20\x77\x68\x65\x6e\x20\x6f\x76\x65\x72\x77\x72\x69\x74\x69\x6e\x67\x20\x43\x4d\x44\x0a\x20\x20\x65\x63\x68\x6f\x20\x22\x44\x45\x50\x52\x45\x43\x41\x54\x45\x44\x3a\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x20\x68\x61\x76\x65\x20\x62\x65\x65\x6e\x20\x73\x65\x74\x20\x62\x75\x74\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x20\x69\x73\x6e\x27\x74\x20\x27\x64\x61\x65\x6d\x6f\x6e\x27\x22\x20\x3e\x26\x32\x0a\x66\x69\x0a\x0a\x65\x78\x65\x63\x20\x69\x70\x66\x73\x20\x64\x61\x65\x6d\x6f\x6e\x20\x22\x24\x40\x22\x0a") func init() { err := CTX.Err() diff --git a/ipfs/node.go b/ipfs/node.go index 2a0937c..8015d96 100644 --- a/ipfs/node.go +++ b/ipfs/node.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "strconv" + + "github.com/docker/docker/api/types" ) const ( @@ -137,3 +139,28 @@ func (n *NodeInfo) labels(peers []string, dataDir string) map[string]string { keyResourcesMemory: strconv.Itoa(n.Resources.MemoryGB), } } + +func (n *NodeInfo) updateFromContainerDetails(c *types.Container) { + if c == nil { + return + } + + // check container ID + n.DockerID = c.ID + + // check ports + if len(c.Ports) > 0 { + for _, p := range c.Ports { + var public = strconv.Itoa(int(p.PublicPort)) + var private = strconv.Itoa(int(p.PrivatePort)) + switch private { + case containerSwarmPort: + n.Ports.Swarm = public + case containerAPIPort: + n.Ports.API = public + case containerGatewayPort: + n.Ports.Gateway = public + } + } + } +} diff --git a/ipfs/node_test.go b/ipfs/node_test.go index fd950f7..46c1323 100644 --- a/ipfs/node_test.go +++ b/ipfs/node_test.go @@ -4,6 +4,8 @@ import ( "encoding/json" "reflect" "testing" + + "github.com/docker/docker/api/types" ) func Test_newNode(t *testing.T) { @@ -58,3 +60,40 @@ func Test_newNode(t *testing.T) { }) } } + +func TestNodeInfo_updateFromContainerDetails(t *testing.T) { + type args struct { + c *types.Container + } + tests := []struct { + name string + args args + want NodeInfo + }{ + {"nil container", args{nil}, NodeInfo{}}, + {"with container", args{&types.Container{ + ID: "abcde", + Ports: []types.Port{ + {PrivatePort: 4001, PublicPort: 3456}, + {PrivatePort: 5001, PublicPort: 2345}, + {PrivatePort: 8080, PublicPort: 1234}, + }, + }}, NodeInfo{ + DockerID: "abcde", + Ports: NodePorts{ + Swarm: "3456", + API: "2345", + Gateway: "1234", + }, + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var n = NodeInfo{} + n.updateFromContainerDetails(tt.args.c) + if !reflect.DeepEqual(n, tt.want) { + t.Errorf("updateFromContainerDetails() = %v, want %v", n, tt.want) + } + }) + } +} diff --git a/ipfs/scripts.go b/ipfs/scripts.go deleted file mode 100644 index 464dc3b..0000000 --- a/ipfs/scripts.go +++ /dev/null @@ -1,17 +0,0 @@ -package ipfs - -import ( - "fmt" - - internal "github.com/RTradeLtd/Nexus/ipfs/internal" -) - -func newNodeStartScript(diskMax int) (string, error) { - f, err := internal.ReadFile("ipfs/internal/ipfs_start.sh") - if err != nil { - return "", fmt.Errorf("failed to generate startup script: %s", err.Error()) - } - return fmt.Sprintf(string(f), - diskMax, - ), nil -} diff --git a/ipfs/scripts_test.go b/ipfs/scripts_test.go deleted file mode 100644 index d02d126..0000000 --- a/ipfs/scripts_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package ipfs - -import ( - "fmt" - "io/ioutil" - "testing" -) - -func Test_newNodeStartScript(t *testing.T) { - var ( - disk = 10 - ) - - f, _ := ioutil.ReadFile("./internal/ipfs_start.sh") - expected := fmt.Sprintf(string(f), disk) - - got, err := newNodeStartScript(disk) - if err != nil { - t.Error("unexpected err:", err.Error()) - return - } - if got != expected { - t.Errorf("expected '%s', got '%s'", expected, got) - } -} diff --git a/log/middleware.go b/log/middleware.go new file mode 100644 index 0000000..882fdf5 --- /dev/null +++ b/log/middleware.go @@ -0,0 +1,49 @@ +package log + +import ( + "time" + + "net/http" + + "github.com/go-chi/chi/middleware" + "go.uber.org/zap" +) + +type loggerMiddleware struct { + l *zap.Logger +} + +// NewMiddleware instantiates a middleware function that logs all requests +// using the provided logger +func NewMiddleware(l *zap.SugaredLogger) func(next http.Handler) http.Handler { + return loggerMiddleware{l.Desugar()}.Handler +} + +func (z loggerMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + next.ServeHTTP(ww, r) + latency := time.Since(start) + + var requestID string + if reqID := r.Context().Value(middleware.RequestIDKey); reqID != nil { + requestID = reqID.(string) + } + z.l.Info("request completed", + // request metadata + zap.String("path", r.URL.Path), + zap.String("query", r.URL.RawQuery), + zap.String("method", r.Method), + zap.String("user-agent", r.UserAgent()), + + // response metadata + zap.Int("status", ww.Status()), + zap.Duration("took", latency), + + // additional metadata + zap.String("real-ip", r.RemoteAddr), + zap.String("request-id", requestID)) + }, + ) +} diff --git a/log/middleware_test.go b/log/middleware_test.go new file mode 100644 index 0000000..9004ff3 --- /dev/null +++ b/log/middleware_test.go @@ -0,0 +1,69 @@ +package log + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/render" +) + +func Test_loggerMiddleware(t *testing.T) { + type args struct { + method string + path string + body io.Reader + middlewares []func(http.Handler) http.Handler + } + tests := []struct { + name string + args args + want []string + }{ + { + "GET with requestID", + args{"GET", "/", nil, []func(http.Handler) http.Handler{middleware.RequestID}}, + []string{"path", "request-id"}, + }, + { + "GET with realIP", + args{"GET", "/", nil, []func(http.Handler) http.Handler{middleware.RealIP}}, + []string{"path", "real-ip"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create bootstrapped logger and middleware + var l, out = NewTestLogger() + var handler = NewMiddleware(l) + + // set up mock router + m := chi.NewRouter() + m.Use(tt.args.middlewares...) + m.Use(handler) + m.Get("/", func(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, map[string]string{"hi": "bye"}) + }) + + // create a mock request to use + req := httptest.NewRequest(tt.args.method, "http://testing"+tt.args.path, + tt.args.body) + + // serve request + m.ServeHTTP(httptest.NewRecorder(), req) + + // check for desired log fields + for _, e := range out.All() { + for _, f := range tt.want { + // find field, cast as string, and check if empty + if val, _ := e.ContextMap()[f].(string); val == "" { + t.Errorf("field %s unexpectedly empty", f) + } + } + } + }) + } +} diff --git a/network/ports.go b/network/ports.go index 440de64..11804a6 100644 --- a/network/ports.go +++ b/network/ports.go @@ -8,6 +8,14 @@ import ( "go.uber.org/zap" ) +const ( + // Private denotes localhost + Private = "127.0.0.1" + + // Public denotes 0.0.0.0 + Public = "0.0.0.0" +) + // Registry manages host network usage type Registry struct { l *zap.SugaredLogger diff --git a/orchestrator/database_test.go b/orchestrator/database_test.go index e47a379..3360412 100644 --- a/orchestrator/database_test.go +++ b/orchestrator/database_test.go @@ -7,6 +7,7 @@ import ( "github.com/RTradeLtd/Nexus/ipfs" tcfg "github.com/RTradeLtd/config" + "github.com/RTradeLtd/database" "github.com/RTradeLtd/database/models" ) @@ -18,6 +19,15 @@ var dbDefaults = tcfg.Database{ Password: "password123", } +func newTestDB() (*database.Manager, error) { + return database.Initialize(&tcfg.TemporalConfig{ + Database: dbDefaults, + }, database.Options{ + SSLModeDisable: true, + RunMigrations: true, + }) +} + func Test_getOptionsFromDatabaseEntry(t *testing.T) { type args struct { network *models.HostedIPFSPrivateNetwork diff --git a/orchestrator/orchestrator.go b/orchestrator/orchestrator.go index 2f319df..666fc01 100644 --- a/orchestrator/orchestrator.go +++ b/orchestrator/orchestrator.go @@ -6,11 +6,9 @@ import ( "fmt" "time" - "go.uber.org/zap" + "github.com/RTradeLtd/Nexus/temporal" - tcfg "github.com/RTradeLtd/config" - "github.com/RTradeLtd/database" - "github.com/RTradeLtd/database/models" + "go.uber.org/zap" "github.com/RTradeLtd/Nexus/config" "github.com/RTradeLtd/Nexus/ipfs" @@ -21,53 +19,41 @@ import ( // Orchestrator contains most primary application logic and manages node // availability type Orchestrator struct { + Registry *registry.NodeRegistry + l *zap.SugaredLogger - nm *models.IPFSNetworkManager + nm temporal.PrivateNetworks client ipfs.NodeClient - reg *registry.NodeRegistry address string } // New instantiates and bootstraps a new Orchestrator -func New(logger *zap.SugaredLogger, address string, c ipfs.NodeClient, - ports config.Ports, pg tcfg.Database, dev bool) (*Orchestrator, error) { +func New(logger *zap.SugaredLogger, address string, ports config.Ports, dev bool, + c ipfs.NodeClient, networks temporal.PrivateNetworks) (*Orchestrator, error) { var l = logger.Named("orchestrator") if address == "" { l.Warn("host address not set") } // bootstrap registry - l.Info("bootstrapping existing nodes") + l.Info("checking for existing nodes") nodes, err := c.Nodes(context.Background()) if err != nil { + l.Errorw("failed to fetch nodes", "error", err) return nil, fmt.Errorf("unable to fetch nodes: %s", err.Error()) } - reg := registry.New(l, ports, nodes...) - - // set up database connection - l.Infow("intializing database connection", - "db.host", pg.URL, - "db.port", pg.Port, - "db.name", pg.Name, - "db.with_ssl", !dev, - "db.with_migrations", dev) - dbm, err := database.Initialize(&tcfg.TemporalConfig{ - Database: pg, - }, database.Options{ - SSLModeDisable: dev, - RunMigrations: dev, - }) - if err != nil { - return nil, fmt.Errorf("unable to connect to database: %s", err.Error()) + if len(nodes) > 0 { + l.Infow("bootstrapping with found nodes", "nodes", nodes) } - l.Info("successfully connected to database") + reg := registry.New(l, ports, nodes...) return &Orchestrator{ + Registry: reg, + l: l, - nm: models.NewHostedIPFSNetworkManager(dbm.DB), + nm: networks, client: c, - reg: reg, address: address, }, nil } @@ -82,13 +68,7 @@ func (o *Orchestrator) Run(ctx context.Context) error { o.l.Info("releasing orchestrator resources") // close registry - o.reg.Close() - - // close database - if err := o.nm.DB.Close(); err != nil { - o.l.Warnw("error occurred closing database connection", - "error", err) - } + o.Registry.Close() } }() return nil @@ -96,8 +76,10 @@ func (o *Orchestrator) Run(ctx context.Context) error { // NetworkDetails provides information about an instantiated network type NetworkDetails struct { - API string - SwarmKey string + NetworkID string + PeerID string + SwarmPort string + SwarmKey string } // NetworkUp intializes a node for given network @@ -133,7 +115,7 @@ func (o *Orchestrator) NetworkUp(ctx context.Context, network string) (NetworkDe // register node for network newNode := getNodeFromDatabaseEntry(jobID, n) - if err := o.reg.Register(newNode); err != nil { + if err := o.Registry.Register(newNode); err != nil { l.Errorw("no available ports", "error", err) return NetworkDetails{}, fmt.Errorf("failed to allocate resources for network '%s': %s", network, err) @@ -143,29 +125,42 @@ func (o *Orchestrator) NetworkUp(ctx context.Context, network string) (NetworkDe l = l.With("node", newNode) l.Info("network registered, creating node") if err := o.client.CreateNode(ctx, newNode, opts); err != nil { - l.Errorw("unable to create node", + l.Errorw("unable to create node - deregistering", "error", err) + o.Registry.Deregister(newNode.NetworkID) return NetworkDetails{}, fmt.Errorf("failed to instantiate node for network '%s': %s", network, err) } l.Info("node created") + s, err := o.client.NodeStats(ctx, newNode) + if err != nil { + l.Errorw("failed to get node stats after node started up successfully", "error", err) + return NetworkDetails{NetworkID: network}, fmt.Errorf("failed to get stats about network '%s': %s", + network, err) + } + // update network in database - n.APIURL = o.address + ":" + newNode.Ports.API + n.PeerKey = s.PeerKey n.SwarmKey = string(opts.SwarmKey) + n.SwarmAddr = fmt.Sprintf("%s:%s", o.address, newNode.Ports.Swarm) n.Activated = time.Now() - if check := o.nm.DB.Save(n); check != nil && check.Error != nil { - l.Errorw("failed to update database", + if err := o.nm.SaveNetwork(n); err != nil { + l.Errorw("failed to update database - removing node", "error", err, "entry", n) - return NetworkDetails{}, fmt.Errorf("failed to update network '%s': %s", network, check.Error) + o.Registry.Deregister(newNode.NetworkID) + o.client.RemoveNode(ctx, newNode.NetworkID) + return NetworkDetails{NetworkID: network}, fmt.Errorf("failed to update network '%s': %s", network, err) } l.Infow("network up process completed", "network_up.duration", time.Since(start)) return NetworkDetails{ - API: n.APIURL, - SwarmKey: n.SwarmKey, + NetworkID: network, + PeerID: s.PeerID, + SwarmPort: newNode.Ports.Swarm, + SwarmKey: n.SwarmKey, }, nil } @@ -176,7 +171,7 @@ func (o *Orchestrator) NetworkUpdate(ctx context.Context, network string) error } // check node exists - node, err := o.reg.Get(network) + node, err := o.Registry.Get(network) if err != nil { return fmt.Errorf("failed to find node for network '%s': %s", network, err.Error()) } @@ -215,8 +210,8 @@ func (o *Orchestrator) NetworkUpdate(ctx context.Context, network string) error // update registry l.Info("updating registry") - o.reg.Deregister(network) - if err := o.reg.Register(new); err != nil { + o.Registry.Deregister(network) + if err := o.Registry.Register(new); err != nil { l.Errorw("failed to register updated network", "error", err) return fmt.Errorf("error updating registry: %s", err.Error()) } @@ -240,7 +235,7 @@ func (o *Orchestrator) NetworkDown(ctx context.Context, network string) error { l.Info("network up process started") // retrieve node from registry - node, err := o.reg.Get(network) + node, err := o.Registry.Get(network) if err != nil { l.Info("could not find node in registry") return fmt.Errorf("failed to get node for network %s from registry: %s", network, err.Error()) @@ -256,7 +251,7 @@ func (o *Orchestrator) NetworkDown(ctx context.Context, network string) error { l.Info("node stopped") // deregister node - if err := o.reg.Deregister(network); err != nil { + if err := o.Registry.Deregister(network); err != nil { l.Errorw("error occurred while deregistering node", "error", err) } @@ -264,8 +259,8 @@ func (o *Orchestrator) NetworkDown(ctx context.Context, network string) error { // update network in database to indicate it is no longer active var t time.Time if err := o.nm.UpdateNetworkByName(network, map[string]interface{}{ - "activated": t, - "api_url": "", + "activated": t, + "swarm_addr": "", }); err != nil { l.Errorw("failed to update database entry for network", "err", err) @@ -284,25 +279,24 @@ func (o *Orchestrator) NetworkRemove(ctx context.Context, network string) error return errors.New("invalid network name provided") } - if _, err := o.reg.Get(network); err == nil { + if _, err := o.Registry.Get(network); err == nil { return errors.New("network is still online and in registry - must be offline for removal") } return o.client.RemoveNode(ctx, network) } -// NetworkStatus denotes details about requested network +// NetworkStatus denotes high-level details about requested network, intended +// for consumer use type NetworkStatus struct { - Network string - API string + NetworkDetails Uptime time.Duration DiskUsage int64 - Stats interface{} } // NetworkStatus retrieves the status of the node for the given status func (o *Orchestrator) NetworkStatus(ctx context.Context, network string) (NetworkStatus, error) { - n, err := o.reg.Get(network) + n, err := o.Registry.Get(network) if err != nil { return NetworkStatus{}, fmt.Errorf("failed to retrieve network details: %s", err.Error()) } @@ -316,10 +310,41 @@ func (o *Orchestrator) NetworkStatus(ctx context.Context, network string) (Netwo } return NetworkStatus{ - Network: network, - API: o.address + ":" + n.Ports.API, + NetworkDetails: NetworkDetails{ + NetworkID: network, + PeerID: n.NetworkID, + SwarmPort: n.Ports.Swarm, + SwarmKey: "", + }, Uptime: stats.Uptime, DiskUsage: stats.DiskUsage, - Stats: stats.Stats, + }, nil +} + +// NetworkDiagnostics describe detailed statistics and information about a node +type NetworkDiagnostics struct { + ipfs.NodeInfo + ipfs.NodeStats +} + +// NetworkDiagnostics retrieves detailed statistics and information about a node +func (o *Orchestrator) NetworkDiagnostics(ctx context.Context, network string) (NetworkDiagnostics, error) { + o.l.Info("diagnostics requested for network", "network.id", network) + n, err := o.Registry.Get(network) + if err != nil { + return NetworkDiagnostics{}, fmt.Errorf("failed to retrieve network details: %s", err.Error()) + } + + // attempt to retrieve live network stats, return what's possible + stats, err := o.client.NodeStats(ctx, &n) + if err != nil { + o.l.Errorw("error occurred while attempting to acess registered node", + "error", err, + "node", n) + } + + return NetworkDiagnostics{ + NodeInfo: n, + NodeStats: stats, }, nil } diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go index c3ace91..47172c9 100644 --- a/orchestrator/orchestrator_test.go +++ b/orchestrator/orchestrator_test.go @@ -19,7 +19,6 @@ import ( func TestNew(t *testing.T) { type args struct { - pgOpts tcfg.Database } tests := []struct { name string @@ -27,9 +26,8 @@ func TestNew(t *testing.T) { wantClientErr bool wantErr bool }{ - {"node client err", args{dbDefaults}, true, true}, - {"invalid db options", args{tcfg.Database{}}, false, true}, - {"all good", args{dbDefaults}, false, false}, + {"node client err", args{}, true, true}, + {"all good", args{}, false, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -42,7 +40,12 @@ func TestNew(t *testing.T) { client.NodesReturns([]*ipfs.NodeInfo{}, nil) } - _, err := New(l, "", client, config.Ports{}, tt.args.pgOpts, true) + dbm, err := newTestDB() + if err != nil { + t.Fatalf("failed to reach database: %s\n", err.Error()) + } + + _, err = New(l, "", config.Ports{}, true, client, models.NewHostedIPFSNetworkManager(dbm.DB)) if (err != nil) != tt.wantErr { t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) return @@ -54,8 +57,11 @@ func TestNew(t *testing.T) { func TestOrchestrator_Run(t *testing.T) { l, _ := log.NewTestLogger() client := &mock.FakeNodeClient{} - - o, err := New(l, "", client, config.Ports{}, dbDefaults, true) + dbm, err := newTestDB() + if err != nil { + t.Fatalf("failed to reach database: %s\n", err.Error()) + } + o, err := New(l, "", config.Ports{}, true, client, models.NewHostedIPFSNetworkManager(dbm.DB)) if err != nil { t.Error(err) return @@ -112,11 +118,11 @@ func TestOrchestrator_NetworkUp(t *testing.T) { l, _ := log.NewTestLogger() client := &mock.FakeNodeClient{} o := &Orchestrator{ - l: l, - nm: nm, - client: client, - reg: registry.New(l, tt.fields.regPorts), - address: "127.0.0.1", + Registry: registry.New(l, tt.fields.regPorts), + l: l, + nm: nm, + client: client, + address: "127.0.0.1", } if tt.createErr { @@ -177,11 +183,11 @@ func TestOrchestrator_NetworkDown(t *testing.T) { l, _ := log.NewTestLogger() client := &mock.FakeNodeClient{} o := &Orchestrator{ - l: l, - nm: nm, - client: client, - reg: registry.New(l, config.New().Ports, &tt.fields.node), - address: "127.0.0.1", + Registry: registry.New(l, config.New().Ports, &tt.fields.node), + l: l, + nm: nm, + client: client, + address: "127.0.0.1", } if tt.createErr { @@ -219,10 +225,10 @@ func TestOrchestrator_NetworkRemove(t *testing.T) { l, _ := log.NewTestLogger() client := &mock.FakeNodeClient{} o := &Orchestrator{ - l: l, - client: client, - reg: registry.New(l, config.New().Ports, &tt.fields.node), - address: "127.0.0.1", + Registry: registry.New(l, config.New().Ports, &tt.fields.node), + l: l, + client: client, + address: "127.0.0.1", } if tt.createErr { @@ -236,6 +242,74 @@ func TestOrchestrator_NetworkRemove(t *testing.T) { } } +func TestOrchestrator_NetworkUpdate(t *testing.T) { + // pre-test database setup + dbm, err := database.Initialize(&tcfg.TemporalConfig{ + Database: dbDefaults, + }, database.Options{ + RunMigrations: true, + SSLModeDisable: true, + }) + if err != nil { + t.Fatalf("failed to connect to dev database: %s", err.Error()) + } + nm := models.NewHostedIPFSNetworkManager(dbm.DB) + testNetwork := &models.HostedIPFSPrivateNetwork{ + Name: "test-network-2", + } + if check := nm.DB.Create(testNetwork); check.Error != nil { + t.Log(check.Error.Error()) + } + defer nm.DB.Delete(testNetwork) + + // tests + type fields struct { + node ipfs.NodeInfo + } + type args struct { + network string + } + tests := []struct { + name string + fields fields + args args + createErr bool + wantErr bool + }{ + {"invalid network name", + fields{ipfs.NodeInfo{}}, args{""}, false, true}, + {"node doesn't exist", + fields{ipfs.NodeInfo{}}, args{"asdf"}, false, true}, + {"node exists but not in db", + fields{ipfs.NodeInfo{NetworkID: "asdf"}}, args{"asdf"}, false, true}, + {"client fail", + fields{}, args{"asdf"}, true, true}, + {"client succeed", + fields{ipfs.NodeInfo{NetworkID: "test-network-2"}}, args{"test-network-2"}, false, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l, _ := log.NewTestLogger() + client := &mock.FakeNodeClient{} + o := &Orchestrator{ + Registry: registry.New(l, config.New().Ports, &tt.fields.node), + l: l, + client: client, + nm: nm, + address: "127.0.0.1", + } + + if tt.createErr { + client.UpdateNodeReturns(errors.New("oh no")) + } + + if err := o.NetworkUpdate(context.Background(), tt.args.network); (err != nil) != tt.wantErr { + t.Errorf("Orchestrator.NetworkUpdate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + func TestOrchestrator_NetworkStatus(t *testing.T) { type fields struct { node ipfs.NodeInfo @@ -260,10 +334,10 @@ func TestOrchestrator_NetworkStatus(t *testing.T) { l, _ := log.NewTestLogger() client := &mock.FakeNodeClient{} o := &Orchestrator{ - l: l, - client: client, - reg: registry.New(l, config.New().Ports, &tt.fields.node), - address: "127.0.0.1", + Registry: registry.New(l, config.New().Ports, &tt.fields.node), + l: l, + client: client, + address: "127.0.0.1", } if tt.createErr { @@ -277,27 +351,7 @@ func TestOrchestrator_NetworkStatus(t *testing.T) { } } -func TestOrchestrator_NetworkUpdate(t *testing.T) { - // pre-test database setup - dbm, err := database.Initialize(&tcfg.TemporalConfig{ - Database: dbDefaults, - }, database.Options{ - RunMigrations: true, - SSLModeDisable: true, - }) - if err != nil { - t.Fatalf("failed to connect to dev database: %s", err.Error()) - } - nm := models.NewHostedIPFSNetworkManager(dbm.DB) - testNetwork := &models.HostedIPFSPrivateNetwork{ - Name: "test-network-2", - } - if check := nm.DB.Create(testNetwork); check.Error != nil { - t.Log(check.Error.Error()) - } - defer nm.DB.Delete(testNetwork) - - // tests +func TestOrchestrator_NetworkDiagnostics(t *testing.T) { type fields struct { node ipfs.NodeInfo } @@ -311,35 +365,28 @@ func TestOrchestrator_NetworkUpdate(t *testing.T) { createErr bool wantErr bool }{ - {"invalid network name", - fields{ipfs.NodeInfo{}}, args{""}, false, true}, - {"node doesn't exist", - fields{ipfs.NodeInfo{}}, args{"asdf"}, false, true}, - {"node exists but not in db", - fields{ipfs.NodeInfo{NetworkID: "asdf"}}, args{"asdf"}, false, true}, - {"client fail", - fields{}, args{"asdf"}, true, true}, - {"client succeed", - fields{ipfs.NodeInfo{NetworkID: "test-network-2"}}, args{"test-network-2"}, false, false}, + {"invalid network name", fields{ipfs.NodeInfo{}}, args{""}, false, true}, + {"unable to find node", fields{ipfs.NodeInfo{}}, args{"asdf"}, false, true}, + {"client fail should still return", fields{ipfs.NodeInfo{NetworkID: "asdf"}}, args{"asdf"}, true, false}, + {"client succeed", fields{ipfs.NodeInfo{NetworkID: "asdf"}}, args{"asdf"}, false, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l, _ := log.NewTestLogger() client := &mock.FakeNodeClient{} o := &Orchestrator{ - l: l, - client: client, - nm: nm, - reg: registry.New(l, config.New().Ports, &tt.fields.node), - address: "127.0.0.1", + Registry: registry.New(l, config.New().Ports, &tt.fields.node), + l: l, + client: client, + address: "127.0.0.1", } if tt.createErr { - client.UpdateNodeReturns(errors.New("oh no")) + client.NodeStatsReturns(ipfs.NodeStats{}, errors.New("oh no")) } - if err := o.NetworkUpdate(context.Background(), tt.args.network); (err != nil) != tt.wantErr { - t.Errorf("Orchestrator.NetworkUpdate() error = %v, wantErr %v", err, tt.wantErr) + if _, err := o.NetworkDiagnostics(context.Background(), tt.args.network); (err != nil) != tt.wantErr { + t.Errorf("Orchestrator.NetworkDiagnostics() error = %v, wantErr %v", err, tt.wantErr) } }) } diff --git a/registry/registry.go b/registry/registry.go index c4c8bfa..2d86583 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -49,9 +49,11 @@ func New(logger *zap.SugaredLogger, ports config.Ports, nodes ...*ipfs.NodeInfo) l: logger.Named("registry"), nodes: m, - swarmPorts: network.NewRegistry(logger, "0.0.0.0", ports.Swarm), - apiPorts: network.NewRegistry(logger, "0.0.0.0", ports.API), - gatewayPorts: network.NewRegistry(logger, "127.0.0.1", ports.Gateway), + // See documentation regarding public/private-ness of IPFS ports in package + // ipfs + swarmPorts: network.NewRegistry(logger, network.Public, ports.Swarm), + apiPorts: network.NewRegistry(logger, network.Private, ports.API), + gatewayPorts: network.NewRegistry(logger, network.Private, ports.Gateway), } } @@ -68,19 +70,22 @@ func (r *NodeRegistry) Register(node *ipfs.NodeInfo) error { return errors.New(ErrNetworkExists) } - // assign ports to this node - var err error - var swarm, api, gateway string - if swarm, err = r.swarmPorts.AssignPort(); err != nil { - return fmt.Errorf("failed to register node: %s", err.Error()) - } - if api, err = r.apiPorts.AssignPort(); err != nil { - return fmt.Errorf("failed to register node: %s", err.Error()) - } - if gateway, err = r.gatewayPorts.AssignPort(); err != nil { - return fmt.Errorf("failed to register node: %s", err.Error()) + // assign ports to this node - do not assign new ones if ports are already + // provided in node.Ports + if node.Ports.Swarm == "" || node.Ports.Gateway == "" || node.Ports.API == "" { + var err error + var swarm, api, gateway string + if swarm, err = r.swarmPorts.AssignPort(); err != nil { + return fmt.Errorf("failed to register node: %s", err.Error()) + } + if api, err = r.apiPorts.AssignPort(); err != nil { + return fmt.Errorf("failed to register node: %s", err.Error()) + } + if gateway, err = r.gatewayPorts.AssignPort(); err != nil { + return fmt.Errorf("failed to register node: %s", err.Error()) + } + node.Ports = ipfs.NodePorts{Swarm: swarm, API: api, Gateway: gateway} } - node.Ports = ipfs.NodePorts{Swarm: swarm, API: api, Gateway: gateway} r.nodes[node.NetworkID] = node diff --git a/temporal/database.go b/temporal/database.go new file mode 100644 index 0000000..8dbd2dd --- /dev/null +++ b/temporal/database.go @@ -0,0 +1,11 @@ +package temporal + +import "github.com/RTradeLtd/database/models" + +// PrivateNetworks is an interface to wrap the Temporal IPFSNetworkManager +// database class +type PrivateNetworks interface { + GetNetworkByName(name string) (*models.HostedIPFSPrivateNetwork, error) + UpdateNetworkByName(name string, attrs map[string]interface{}) error + SaveNetwork(n *models.HostedIPFSPrivateNetwork) error +} diff --git a/temporal/doc.go b/temporal/doc.go new file mode 100644 index 0000000..6916032 --- /dev/null +++ b/temporal/doc.go @@ -0,0 +1,2 @@ +// Package temporal provides wrappers for imported Temporal classes +package temporal diff --git a/temporal/mock/networks.mock.go b/temporal/mock/networks.mock.go new file mode 100644 index 0000000..ebdffce --- /dev/null +++ b/temporal/mock/networks.mock.go @@ -0,0 +1,264 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mock + +import ( + "sync" + + "github.com/RTradeLtd/Nexus/temporal" + "github.com/RTradeLtd/database/models" +) + +type FakePrivateNetworks struct { + GetNetworkByNameStub func(string) (*models.HostedIPFSPrivateNetwork, error) + getNetworkByNameMutex sync.RWMutex + getNetworkByNameArgsForCall []struct { + arg1 string + } + getNetworkByNameReturns struct { + result1 *models.HostedIPFSPrivateNetwork + result2 error + } + getNetworkByNameReturnsOnCall map[int]struct { + result1 *models.HostedIPFSPrivateNetwork + result2 error + } + SaveNetworkStub func(*models.HostedIPFSPrivateNetwork) error + saveNetworkMutex sync.RWMutex + saveNetworkArgsForCall []struct { + arg1 *models.HostedIPFSPrivateNetwork + } + saveNetworkReturns struct { + result1 error + } + saveNetworkReturnsOnCall map[int]struct { + result1 error + } + UpdateNetworkByNameStub func(string, map[string]interface{}) error + updateNetworkByNameMutex sync.RWMutex + updateNetworkByNameArgsForCall []struct { + arg1 string + arg2 map[string]interface{} + } + updateNetworkByNameReturns struct { + result1 error + } + updateNetworkByNameReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakePrivateNetworks) GetNetworkByName(arg1 string) (*models.HostedIPFSPrivateNetwork, error) { + fake.getNetworkByNameMutex.Lock() + ret, specificReturn := fake.getNetworkByNameReturnsOnCall[len(fake.getNetworkByNameArgsForCall)] + fake.getNetworkByNameArgsForCall = append(fake.getNetworkByNameArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("GetNetworkByName", []interface{}{arg1}) + fake.getNetworkByNameMutex.Unlock() + if fake.GetNetworkByNameStub != nil { + return fake.GetNetworkByNameStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getNetworkByNameReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakePrivateNetworks) GetNetworkByNameCallCount() int { + fake.getNetworkByNameMutex.RLock() + defer fake.getNetworkByNameMutex.RUnlock() + return len(fake.getNetworkByNameArgsForCall) +} + +func (fake *FakePrivateNetworks) GetNetworkByNameCalls(stub func(string) (*models.HostedIPFSPrivateNetwork, error)) { + fake.getNetworkByNameMutex.Lock() + defer fake.getNetworkByNameMutex.Unlock() + fake.GetNetworkByNameStub = stub +} + +func (fake *FakePrivateNetworks) GetNetworkByNameArgsForCall(i int) string { + fake.getNetworkByNameMutex.RLock() + defer fake.getNetworkByNameMutex.RUnlock() + argsForCall := fake.getNetworkByNameArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePrivateNetworks) GetNetworkByNameReturns(result1 *models.HostedIPFSPrivateNetwork, result2 error) { + fake.getNetworkByNameMutex.Lock() + defer fake.getNetworkByNameMutex.Unlock() + fake.GetNetworkByNameStub = nil + fake.getNetworkByNameReturns = struct { + result1 *models.HostedIPFSPrivateNetwork + result2 error + }{result1, result2} +} + +func (fake *FakePrivateNetworks) GetNetworkByNameReturnsOnCall(i int, result1 *models.HostedIPFSPrivateNetwork, result2 error) { + fake.getNetworkByNameMutex.Lock() + defer fake.getNetworkByNameMutex.Unlock() + fake.GetNetworkByNameStub = nil + if fake.getNetworkByNameReturnsOnCall == nil { + fake.getNetworkByNameReturnsOnCall = make(map[int]struct { + result1 *models.HostedIPFSPrivateNetwork + result2 error + }) + } + fake.getNetworkByNameReturnsOnCall[i] = struct { + result1 *models.HostedIPFSPrivateNetwork + result2 error + }{result1, result2} +} + +func (fake *FakePrivateNetworks) SaveNetwork(arg1 *models.HostedIPFSPrivateNetwork) error { + fake.saveNetworkMutex.Lock() + ret, specificReturn := fake.saveNetworkReturnsOnCall[len(fake.saveNetworkArgsForCall)] + fake.saveNetworkArgsForCall = append(fake.saveNetworkArgsForCall, struct { + arg1 *models.HostedIPFSPrivateNetwork + }{arg1}) + fake.recordInvocation("SaveNetwork", []interface{}{arg1}) + fake.saveNetworkMutex.Unlock() + if fake.SaveNetworkStub != nil { + return fake.SaveNetworkStub(arg1) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.saveNetworkReturns + return fakeReturns.result1 +} + +func (fake *FakePrivateNetworks) SaveNetworkCallCount() int { + fake.saveNetworkMutex.RLock() + defer fake.saveNetworkMutex.RUnlock() + return len(fake.saveNetworkArgsForCall) +} + +func (fake *FakePrivateNetworks) SaveNetworkCalls(stub func(*models.HostedIPFSPrivateNetwork) error) { + fake.saveNetworkMutex.Lock() + defer fake.saveNetworkMutex.Unlock() + fake.SaveNetworkStub = stub +} + +func (fake *FakePrivateNetworks) SaveNetworkArgsForCall(i int) *models.HostedIPFSPrivateNetwork { + fake.saveNetworkMutex.RLock() + defer fake.saveNetworkMutex.RUnlock() + argsForCall := fake.saveNetworkArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePrivateNetworks) SaveNetworkReturns(result1 error) { + fake.saveNetworkMutex.Lock() + defer fake.saveNetworkMutex.Unlock() + fake.SaveNetworkStub = nil + fake.saveNetworkReturns = struct { + result1 error + }{result1} +} + +func (fake *FakePrivateNetworks) SaveNetworkReturnsOnCall(i int, result1 error) { + fake.saveNetworkMutex.Lock() + defer fake.saveNetworkMutex.Unlock() + fake.SaveNetworkStub = nil + if fake.saveNetworkReturnsOnCall == nil { + fake.saveNetworkReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.saveNetworkReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakePrivateNetworks) UpdateNetworkByName(arg1 string, arg2 map[string]interface{}) error { + fake.updateNetworkByNameMutex.Lock() + ret, specificReturn := fake.updateNetworkByNameReturnsOnCall[len(fake.updateNetworkByNameArgsForCall)] + fake.updateNetworkByNameArgsForCall = append(fake.updateNetworkByNameArgsForCall, struct { + arg1 string + arg2 map[string]interface{} + }{arg1, arg2}) + fake.recordInvocation("UpdateNetworkByName", []interface{}{arg1, arg2}) + fake.updateNetworkByNameMutex.Unlock() + if fake.UpdateNetworkByNameStub != nil { + return fake.UpdateNetworkByNameStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.updateNetworkByNameReturns + return fakeReturns.result1 +} + +func (fake *FakePrivateNetworks) UpdateNetworkByNameCallCount() int { + fake.updateNetworkByNameMutex.RLock() + defer fake.updateNetworkByNameMutex.RUnlock() + return len(fake.updateNetworkByNameArgsForCall) +} + +func (fake *FakePrivateNetworks) UpdateNetworkByNameCalls(stub func(string, map[string]interface{}) error) { + fake.updateNetworkByNameMutex.Lock() + defer fake.updateNetworkByNameMutex.Unlock() + fake.UpdateNetworkByNameStub = stub +} + +func (fake *FakePrivateNetworks) UpdateNetworkByNameArgsForCall(i int) (string, map[string]interface{}) { + fake.updateNetworkByNameMutex.RLock() + defer fake.updateNetworkByNameMutex.RUnlock() + argsForCall := fake.updateNetworkByNameArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakePrivateNetworks) UpdateNetworkByNameReturns(result1 error) { + fake.updateNetworkByNameMutex.Lock() + defer fake.updateNetworkByNameMutex.Unlock() + fake.UpdateNetworkByNameStub = nil + fake.updateNetworkByNameReturns = struct { + result1 error + }{result1} +} + +func (fake *FakePrivateNetworks) UpdateNetworkByNameReturnsOnCall(i int, result1 error) { + fake.updateNetworkByNameMutex.Lock() + defer fake.updateNetworkByNameMutex.Unlock() + fake.UpdateNetworkByNameStub = nil + if fake.updateNetworkByNameReturnsOnCall == nil { + fake.updateNetworkByNameReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateNetworkByNameReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakePrivateNetworks) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getNetworkByNameMutex.RLock() + defer fake.getNetworkByNameMutex.RUnlock() + fake.saveNetworkMutex.RLock() + defer fake.saveNetworkMutex.RUnlock() + fake.updateNetworkByNameMutex.RLock() + defer fake.updateNetworkByNameMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakePrivateNetworks) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ temporal.PrivateNetworks = new(FakePrivateNetworks) diff --git a/temporal/mock/token.mock.go b/temporal/mock/token.mock.go new file mode 100644 index 0000000..e368e5b --- /dev/null +++ b/temporal/mock/token.mock.go @@ -0,0 +1,12 @@ +package mock + +import jwt "github.com/dgrijalva/jwt-go" + +// FakeToken generates a fake JWT token +func FakeToken(user, key string) string { + token := jwt.New(jwt.SigningMethodHS256) + claims := token.Claims.(jwt.MapClaims) + claims["id"] = user + tokenString, _ := token.SignedString([]byte(key)) + return tokenString +}