mirror of
https://github.com/aljazceru/nigiri.git
synced 2026-02-07 07:44:35 +01:00
Changes for new version of esplora image (#62)
* changes for new version of esplora image * add electrs port and esplora url env vars in compose yaml files * wrap viper methods into Config type and use constants package * add controller to interact with nigiri resources: * .env for docker-compose * docker daemon * json config file * add use of constants and config packages and change start flag from --port to --env * add package for global constants and variables * add use of controller and constants packages instead of local methods and vars * bump version * use contants in logs command tests
This commit is contained in:
committed by
Marco Argentieri
parent
d0b3676c14
commit
e02c2fdd0d
@@ -3,12 +3,11 @@ package cmd
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/vulpemventures/nigiri/cli/config"
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -18,38 +17,25 @@ var (
|
||||
flagAttachLiquid bool
|
||||
flagLiquidService bool
|
||||
flagEnv string
|
||||
|
||||
defaultPorts = map[string]map[string]int{
|
||||
"bitcoin": {
|
||||
"node": 18443,
|
||||
"electrs_rpc": 60401,
|
||||
"chopsticks": 3000,
|
||||
"esplora": 5000,
|
||||
},
|
||||
"liquid": {
|
||||
"node": 7041,
|
||||
"electrs_rpc": 51401,
|
||||
"chopsticks": 3001,
|
||||
"esplora": 5001,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "nigiri",
|
||||
Short: "Nigiri lets you manage a full dockerized bitcoin environment",
|
||||
Long: "Nigiri lets you create your dockerized environment with a bitcoin and optionally a liquid node + block explorer powered by an electrum server for every network",
|
||||
Version: "0.0.3",
|
||||
Version: "0.0.4",
|
||||
}
|
||||
|
||||
func init() {
|
||||
defaultDir := getDefaultDir()
|
||||
ports, _ := json.Marshal(defaultPorts)
|
||||
c := &config.Config{}
|
||||
viper := c.Viper()
|
||||
defaultDir := c.GetPath()
|
||||
defaultJSON, _ := json.Marshal(constants.DefaultEnv)
|
||||
|
||||
RootCmd.PersistentFlags().StringVar(&flagDatadir, "datadir", defaultDir, "Set nigiri default directory")
|
||||
StartCmd.PersistentFlags().StringVar(&flagNetwork, "network", "regtest", "Set bitcoin network - regtest only for now")
|
||||
StartCmd.PersistentFlags().BoolVar(&flagAttachLiquid, "liquid", false, "Enable liquid sidechain")
|
||||
StartCmd.PersistentFlags().StringVar(&flagEnv, "ports", string(ports), "Set services ports in JSON format")
|
||||
StartCmd.PersistentFlags().StringVar(&flagEnv, "env", string(defaultJSON), "Set compose env in JSON format")
|
||||
StopCmd.PersistentFlags().BoolVar(&flagDelete, "delete", false, "Stop and delete nigiri")
|
||||
LogsCmd.PersistentFlags().BoolVar(&flagLiquidService, "liquid", false, "Set to see logs of a liquid service")
|
||||
|
||||
@@ -57,18 +43,12 @@ func init() {
|
||||
RootCmd.AddCommand(StopCmd)
|
||||
RootCmd.AddCommand(LogsCmd)
|
||||
|
||||
viper := config.Viper()
|
||||
viper.BindPFlag(config.Datadir, RootCmd.PersistentFlags().Lookup("datadir"))
|
||||
viper.BindPFlag(config.Network, StartCmd.PersistentFlags().Lookup("network"))
|
||||
viper.BindPFlag(config.AttachLiquid, StartCmd.PersistentFlags().Lookup("liquid"))
|
||||
viper.BindPFlag(constants.Datadir, RootCmd.PersistentFlags().Lookup("datadir"))
|
||||
viper.BindPFlag(constants.Network, StartCmd.PersistentFlags().Lookup("network"))
|
||||
viper.BindPFlag(constants.AttachLiquid, StartCmd.PersistentFlags().Lookup("liquid"))
|
||||
|
||||
cobra.OnInitialize(func() {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetLevel(log.InfoLevel)
|
||||
})
|
||||
}
|
||||
|
||||
func getDefaultDir() string {
|
||||
home, _ := homedir.Expand("~")
|
||||
return filepath.Join(home, ".nigiri")
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
|
||||
"github.com/vulpemventures/nigiri/cli/config"
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
"github.com/vulpemventures/nigiri/cli/controller"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -18,42 +17,39 @@ var LogsCmd = &cobra.Command{
|
||||
PreRunE: logsChecks,
|
||||
}
|
||||
|
||||
var services = map[string]bool{
|
||||
"node": true,
|
||||
"electrs": true,
|
||||
"esplora": true,
|
||||
"chopsticks": true,
|
||||
}
|
||||
|
||||
func logsChecks(cmd *cobra.Command, args []string) error {
|
||||
datadir, _ := cmd.Flags().GetString("datadir")
|
||||
isLiquidService, _ := cmd.Flags().GetBool("liquid")
|
||||
|
||||
if !isDatadirOk(datadir) {
|
||||
return fmt.Errorf("Invalid datadir, it must be an absolute path: %s", datadir)
|
||||
}
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("Invalid number of args, expected 1, got: %d", len(args))
|
||||
}
|
||||
|
||||
service := args[0]
|
||||
if !services[service] {
|
||||
return fmt.Errorf("Invalid service, must be one of %s. Got: %s", reflect.ValueOf(services).MapKeys(), service)
|
||||
}
|
||||
isRunning, err := nigiriIsRunning()
|
||||
ctl, err := controller.NewController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isRunning {
|
||||
return fmt.Errorf("Nigiri is not running")
|
||||
|
||||
if err := ctl.ParseDatadir(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(args) != 1 {
|
||||
return constants.ErrInvalidArgs
|
||||
}
|
||||
|
||||
if err := config.ReadFromFile(datadir); err != nil {
|
||||
service := args[0]
|
||||
if err := ctl.ParseServiceName(service); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isLiquidService && isLiquidService != config.GetBool(config.AttachLiquid) {
|
||||
return fmt.Errorf("Nigiri has been started with no Liquid sidechain.\nPlease stop and restart it using the --liquid flag")
|
||||
if isRunning, err := ctl.IsNigiriRunning(); err != nil {
|
||||
return err
|
||||
} else if !isRunning {
|
||||
return constants.ErrNigiriNotRunning
|
||||
}
|
||||
|
||||
if err := ctl.ReadConfigFile(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isLiquidService && isLiquidService != ctl.GetConfigBoolField(constants.AttachLiquid) {
|
||||
return constants.ErrNigiriLiquidNotEnabled
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -64,10 +60,15 @@ func logs(cmd *cobra.Command, args []string) error {
|
||||
datadir, _ := cmd.Flags().GetString("datadir")
|
||||
isLiquidService, _ := cmd.Flags().GetBool("liquid")
|
||||
|
||||
serviceName := getServiceName(service, isLiquidService)
|
||||
composePath := getPath(datadir, "compose")
|
||||
envPath := getPath(datadir, "env")
|
||||
env := loadEnv(envPath)
|
||||
ctl, err := controller.NewController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceName := ctl.GetServiceName(service, isLiquidService)
|
||||
composePath := ctl.GetResourcePath(datadir, "compose")
|
||||
envPath := ctl.GetResourcePath(datadir, "env")
|
||||
env := ctl.LoadComposeEnvironment(envPath)
|
||||
|
||||
bashCmd := exec.Command("docker-compose", "-f", composePath, "logs", serviceName)
|
||||
bashCmd.Stdout = os.Stdout
|
||||
@@ -80,19 +81,3 @@ func logs(cmd *cobra.Command, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServiceName(name string, liquid bool) string {
|
||||
service := name
|
||||
if service == "node" {
|
||||
service = "bitcoin"
|
||||
}
|
||||
if liquid {
|
||||
if service == "bitcoin" {
|
||||
service = "liquid"
|
||||
} else {
|
||||
service = fmt.Sprintf("%s-liquid", service)
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -41,7 +43,7 @@ func TestLogLiquidServices(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLogShouldFail(t *testing.T) {
|
||||
expectedError := "Nigiri is not running"
|
||||
expectedError := constants.ErrNigiriNotRunning.Error()
|
||||
|
||||
err := testCommand("logs", serviceList[0], bitcoin)
|
||||
if err == nil {
|
||||
@@ -65,7 +67,7 @@ func TestStartBitcoinAndLogNigiriServicesShouldFail(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedError := "Nigiri has been started with no Liquid sidechain.\nPlease stop and restart it using the --liquid flag"
|
||||
expectedError := constants.ErrNigiriLiquidNotEnabled.Error()
|
||||
|
||||
err := testCommand("logs", serviceList[0], liquid)
|
||||
if err == nil {
|
||||
|
||||
327
cli/cmd/start.go
327
cli/cmd/start.go
@@ -1,25 +1,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/vulpemventures/nigiri/cli/config"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
"github.com/vulpemventures/nigiri/cli/controller"
|
||||
)
|
||||
|
||||
const listAll = true
|
||||
|
||||
var StartCmd = &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Build and start Nigiri",
|
||||
@@ -30,27 +21,30 @@ var StartCmd = &cobra.Command{
|
||||
func startChecks(cmd *cobra.Command, args []string) error {
|
||||
network, _ := cmd.Flags().GetString("network")
|
||||
datadir, _ := cmd.Flags().GetString("datadir")
|
||||
ports, _ := cmd.Flags().GetString("ports")
|
||||
env, _ := cmd.Flags().GetString("env")
|
||||
|
||||
// check flags
|
||||
if !isNetworkOk(network) {
|
||||
return fmt.Errorf("Invalid network: %s", network)
|
||||
}
|
||||
|
||||
if !isDatadirOk(datadir) {
|
||||
return fmt.Errorf("Invalid datadir, it must be an absolute path: %s", datadir)
|
||||
}
|
||||
if !isEnvOk(ports) {
|
||||
return fmt.Errorf("Invalid env JSON, it must contain a \"bitcoin\" object with at least one service specified. It can optionally contain a \"liquid\" object with at least one service specified.\nGot: %s", ports)
|
||||
}
|
||||
|
||||
// if nigiri is already running return error
|
||||
isRunning, err := nigiriIsRunning()
|
||||
ctl, err := controller.NewController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isRunning {
|
||||
return fmt.Errorf("Nigiri is already running, please stop it first")
|
||||
|
||||
if err := ctl.ParseNetwork(network); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ctl.ParseDatadir(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
composeEnv, err := ctl.ParseEnv(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if nigiri is already running return error
|
||||
if isRunning, err := ctl.IsNigiriRunning(); err != nil {
|
||||
return err
|
||||
} else if isRunning {
|
||||
return constants.ErrNigiriAlreadyRunning
|
||||
}
|
||||
|
||||
// scratch datadir if not exists
|
||||
@@ -60,65 +54,77 @@ func startChecks(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// if datadir is set we must copy the resources directory from ~/.nigiri
|
||||
// to the new one
|
||||
if datadir != getDefaultDir() {
|
||||
if err := copyResources(datadir); err != nil {
|
||||
if datadir != ctl.GetDefaultDatadir() {
|
||||
if err := ctl.NewDatadirFromDefault(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if nigiri not exists, we need to write the configuration file and then
|
||||
// read from it to get viper updated, otherwise we just read from it.
|
||||
exists, err := nigiriExistsAndNotRunning()
|
||||
if err != nil {
|
||||
if isStopped, err := ctl.IsNigiriStopped(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
filedir := getPath(datadir, "config")
|
||||
if err := config.WriteConfig(filedir); err != nil {
|
||||
} else if isStopped {
|
||||
if err := ctl.ReadConfigFile(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
filedir := ctl.GetResourcePath(datadir, "config")
|
||||
if err := ctl.WriteConfigFile(filedir); err != nil {
|
||||
return err
|
||||
}
|
||||
// .env must be in the directory where docker-compose is run from, not where YAML files are placed
|
||||
// https://docs.docker.com/compose/env-file/
|
||||
filedir = getPath(datadir, "env")
|
||||
if err := writeComposeEnvFile(filedir, ports); err != nil {
|
||||
filedir = ctl.GetResourcePath(datadir, "env")
|
||||
if err := ctl.WriteComposeEnvironment(filedir, composeEnv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := config.ReadFromFile(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func start(cmd *cobra.Command, args []string) error {
|
||||
datadir, _ := cmd.Flags().GetString("datadir")
|
||||
liquidEnabled, _ := cmd.Flags().GetBool("liquid")
|
||||
|
||||
bashCmd, err := getStartBashCmd(datadir)
|
||||
ctl, err := controller.NewController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datadir, _ := cmd.Flags().GetString("datadir")
|
||||
liquidEnabled := ctl.GetConfigBoolField(constants.AttachLiquid)
|
||||
|
||||
envPath := ctl.GetResourcePath(datadir, "env")
|
||||
composePath := ctl.GetResourcePath(datadir, "compose")
|
||||
|
||||
bashCmd := exec.Command("docker-compose", "-f", composePath, "up", "-d")
|
||||
if isStopped, err := ctl.IsNigiriStopped(); err != nil {
|
||||
return err
|
||||
} else if isStopped {
|
||||
bashCmd = exec.Command("docker-compose", "-f", composePath, "start")
|
||||
}
|
||||
bashCmd.Stdout = os.Stdout
|
||||
bashCmd.Stderr = os.Stderr
|
||||
bashCmd.Env = ctl.LoadComposeEnvironment(envPath)
|
||||
|
||||
if err := bashCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := getPath(datadir, "env")
|
||||
ports, err := readComposeEnvFile(path)
|
||||
path := ctl.GetResourcePath(datadir, "env")
|
||||
env, err := ctl.ReadComposeEnvironment(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prettyPrintServices := func(chain string, services map[string]int) {
|
||||
fmt.Printf("%s services:\n", chain)
|
||||
fmt.Printf("%s services:\n", strings.Title(chain))
|
||||
for name, port := range services {
|
||||
formatName := fmt.Sprintf("%s:", name)
|
||||
fmt.Printf(" %-14s localhost:%d\n", formatName, port)
|
||||
}
|
||||
}
|
||||
|
||||
for chain, services := range ports {
|
||||
for chain, services := range env["ports"].(map[string]map[string]int) {
|
||||
if chain == "bitcoin" {
|
||||
prettyPrintServices(chain, services)
|
||||
} else if liquidEnabled {
|
||||
@@ -128,226 +134,3 @@ func start(cmd *cobra.Command, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var images = map[string]bool{
|
||||
"vulpemventures/bitcoin:latest": true,
|
||||
"vulpemventures/liquid:latest": true,
|
||||
"vulpemventures/electrs:latest": true,
|
||||
"vulpemventures/electrs-liquid:latest": true,
|
||||
"vulpemventures/esplora:latest": true,
|
||||
"vulpemventures/esplora-liquid:latest": true,
|
||||
"vulpemventures/nigiri-chopsticks:latest": true,
|
||||
}
|
||||
|
||||
func copyResources(datadir string) error {
|
||||
defaultDatadir := getDefaultDir()
|
||||
cmd := exec.Command("cp", "-R", filepath.Join(defaultDatadir, "resources"), datadir)
|
||||
return cmd.Run()
|
||||
|
||||
}
|
||||
|
||||
func nigiriExists(listAll bool) (bool, error) {
|
||||
cli, err := client.NewEnvClient()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: listAll})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
if images[container.Image] {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func isNetworkOk(network string) bool {
|
||||
var ok bool
|
||||
for _, n := range []string{"regtest"} {
|
||||
if network == n {
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func isDatadirOk(datadir string) bool {
|
||||
return filepath.IsAbs(datadir)
|
||||
}
|
||||
|
||||
func isEnvOk(stringifiedJSON string) bool {
|
||||
var parsedJSON map[string]map[string]int
|
||||
err := json.Unmarshal([]byte(stringifiedJSON), &parsedJSON)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(parsedJSON) <= 0 {
|
||||
return false
|
||||
}
|
||||
if len(parsedJSON["bitcoin"]) <= 0 {
|
||||
return false
|
||||
}
|
||||
if parsedJSON["bitcoin"]["node"] <= 0 &&
|
||||
parsedJSON["bitcoin"]["electrs"] <= 0 &&
|
||||
parsedJSON["bitcoin"]["esplora"] <= 0 &&
|
||||
parsedJSON["bitcoin"]["chopsticks"] <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(parsedJSON["liquid"]) > 0 &&
|
||||
parsedJSON["liquid"]["node"] <= 0 &&
|
||||
parsedJSON["liquid"]["electrs"] <= 0 &&
|
||||
parsedJSON["liquid"]["esplora"] <= 0 &&
|
||||
parsedJSON["liquid"]["chopsticks"] <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func getPath(datadir, t string) string {
|
||||
viper := config.Viper()
|
||||
|
||||
if t == "compose" {
|
||||
network := viper.GetString("network")
|
||||
attachLiquid := viper.GetBool("attachLiquid")
|
||||
if attachLiquid {
|
||||
network += "-liquid"
|
||||
}
|
||||
return filepath.Join(datadir, "resources", fmt.Sprintf("docker-compose-%s.yml", network))
|
||||
}
|
||||
|
||||
if t == "env" {
|
||||
return filepath.Join(datadir, ".env")
|
||||
}
|
||||
|
||||
if t == "config" {
|
||||
return filepath.Join(datadir, "nigiri.config.json")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func nigiriIsRunning() (bool, error) {
|
||||
listOnlyRunningContainers := !listAll
|
||||
return nigiriExists(listOnlyRunningContainers)
|
||||
}
|
||||
|
||||
func nigiriExistsAndNotRunning() (bool, error) {
|
||||
return nigiriExists(listAll)
|
||||
}
|
||||
|
||||
func getStartBashCmd(datadir string) (*exec.Cmd, error) {
|
||||
composePath := getPath(datadir, "compose")
|
||||
envPath := getPath(datadir, "env")
|
||||
env := loadEnv(envPath)
|
||||
|
||||
bashCmd := exec.Command("docker-compose", "-f", composePath, "up", "-d")
|
||||
|
||||
isStopped, err := nigiriExistsAndNotRunning()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isStopped {
|
||||
bashCmd = exec.Command("docker-compose", "-f", composePath, "start")
|
||||
}
|
||||
bashCmd.Stdout = os.Stdout
|
||||
bashCmd.Stderr = os.Stderr
|
||||
bashCmd.Env = env
|
||||
|
||||
return bashCmd, nil
|
||||
}
|
||||
|
||||
func writeComposeEnvFile(path string, stringifiedJSON string) error {
|
||||
defaultJSON, _ := json.Marshal(defaultPorts)
|
||||
env := map[string]map[string]int{}
|
||||
json.Unmarshal([]byte(stringifiedJSON), &env)
|
||||
|
||||
if stringifiedJSON != string(defaultJSON) {
|
||||
env = mergeComposeEnvFiles([]byte(stringifiedJSON))
|
||||
}
|
||||
|
||||
fileContent := ""
|
||||
for chain, services := range env {
|
||||
for k, v := range services {
|
||||
fileContent += fmt.Sprintf("%s_%s_PORT=%d\n", strings.ToUpper(chain), strings.ToUpper(k), v)
|
||||
}
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, []byte(fileContent), os.ModePerm)
|
||||
}
|
||||
|
||||
func readComposeEnvFile(path string) (map[string]map[string]int, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
ports := map[string]map[string]int{
|
||||
"bitcoin": map[string]int{},
|
||||
"liquid": map[string]int{},
|
||||
}
|
||||
// Each line is in the format PREFIX_SERVICE_NAME_SUFFIX=value
|
||||
// PREFIX is either 'BITCOIN' or 'LIQUID', while SUFFIX is always 'PORT'
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
splitLine := strings.Split(line, "=")
|
||||
key := splitLine[0]
|
||||
value, _ := strconv.Atoi(splitLine[1])
|
||||
chain := "bitcoin"
|
||||
if strings.HasPrefix(key, strings.ToUpper("liquid")) {
|
||||
chain = "liquid"
|
||||
}
|
||||
|
||||
suffix := "_PORT"
|
||||
prefix := strings.ToUpper(fmt.Sprintf("%s_", chain))
|
||||
trimmedKey := strings.ToLower(strings.TrimSuffix(strings.TrimPrefix(key, prefix), suffix))
|
||||
ports[chain][trimmedKey] = value
|
||||
}
|
||||
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
func mergeComposeEnvFiles(rawJSON []byte) map[string]map[string]int {
|
||||
newPorts := map[string]map[string]int{}
|
||||
json.Unmarshal(rawJSON, &newPorts)
|
||||
|
||||
mergedPorts := map[string]map[string]int{}
|
||||
for chain, services := range defaultPorts {
|
||||
mergedPorts[chain] = make(map[string]int)
|
||||
for name, port := range services {
|
||||
newPort := newPorts[chain][name]
|
||||
if newPort > 0 && newPort != port {
|
||||
mergedPorts[chain][name] = newPort
|
||||
} else {
|
||||
mergedPorts[chain][name] = port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergedPorts
|
||||
}
|
||||
|
||||
func loadEnv(path string) []string {
|
||||
content, _ := ioutil.ReadFile(path)
|
||||
lines := strings.Split(string(content), "\n")
|
||||
env := os.Environ()
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
env = append(env, line)
|
||||
}
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
"github.com/vulpemventures/nigiri/cli/controller"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,7 +40,7 @@ func TestStartStopBitcoin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStopBeforeStartShouldFail(t *testing.T) {
|
||||
expectedError := "Nigiri is neither running nor stopped, please create it first"
|
||||
expectedError := constants.ErrNigiriNotRunning.Error()
|
||||
|
||||
err := testCommand("stop", "", !delete)
|
||||
if err == nil {
|
||||
@@ -47,6 +50,7 @@ func TestStopBeforeStartShouldFail(t *testing.T) {
|
||||
t.Fatalf("Expected error: %s, got: %s", expectedError, err)
|
||||
}
|
||||
|
||||
expectedError = constants.ErrNigiriNotExisting.Error()
|
||||
err = testCommand("stop", "", delete)
|
||||
if err == nil {
|
||||
t.Fatal("Should return error when trying to delete before starting")
|
||||
@@ -57,7 +61,7 @@ func TestStopBeforeStartShouldFail(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStartAfterStartShouldFail(t *testing.T) {
|
||||
expectedError := "Nigiri is already running, please stop it first"
|
||||
expectedError := constants.ErrNigiriAlreadyRunning.Error()
|
||||
|
||||
if err := testCommand("start", "", bitcoin); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -85,30 +89,48 @@ func TestStartAfterStartShouldFail(t *testing.T) {
|
||||
}
|
||||
|
||||
func testStart(t *testing.T, flag bool) {
|
||||
ctl, err := controller.NewController()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testCommand("start", "", flag); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if isRunning, _ := nigiriIsRunning(); !isRunning {
|
||||
t.Fatal("Nigiri should be started but services have not been found among running containers")
|
||||
if isRunning, err := ctl.IsNigiriRunning(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !isRunning {
|
||||
t.Fatal("Nigiri should have been started but services have not been found among running containers")
|
||||
}
|
||||
}
|
||||
|
||||
func testStop(t *testing.T) {
|
||||
fmt.Println(!delete)
|
||||
ctl, err := controller.NewController()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testCommand("stop", "", !delete); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if isStopped, _ := nigiriExistsAndNotRunning(); !isStopped {
|
||||
t.Fatal("Nigiri should be stopped but services have not been found among stopped containers")
|
||||
if isStopped, err := ctl.IsNigiriStopped(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !isStopped {
|
||||
t.Fatal("Nigiri should have been stopped but services have not been found among stopped containers")
|
||||
}
|
||||
}
|
||||
|
||||
func testDelete(t *testing.T) {
|
||||
ctl, err := controller.NewController()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := testCommand("stop", "", delete); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if isStopped, _ := nigiriExistsAndNotRunning(); isStopped {
|
||||
t.Fatal("Nigiri should be terminated at this point but services have found among stopped containers")
|
||||
if isStopped, err := ctl.IsNigiriStopped(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if isStopped {
|
||||
t.Fatal("Nigiri should have been terminated at this point but services have been found among stopped containers")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
119
cli/cmd/stop.go
119
cli/cmd/stop.go
@@ -2,13 +2,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/vulpemventures/nigiri/cli/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
"github.com/vulpemventures/nigiri/cli/controller"
|
||||
)
|
||||
|
||||
var StopCmd = &cobra.Command{
|
||||
@@ -20,24 +19,36 @@ var StopCmd = &cobra.Command{
|
||||
|
||||
func stopChecks(cmd *cobra.Command, args []string) error {
|
||||
datadir, _ := cmd.Flags().GetString("datadir")
|
||||
delete, _ := cmd.Flags().GetBool("delete")
|
||||
|
||||
if !isDatadirOk(datadir) {
|
||||
return fmt.Errorf("Invalid datadir, it must be an absolute path: %s", datadir)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(datadir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("Datadir do not exists: %s", datadir)
|
||||
}
|
||||
|
||||
nigiriExists, err := nigiriExistsAndNotRunning()
|
||||
ctl, err := controller.NewController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !nigiriExists {
|
||||
return fmt.Errorf("Nigiri is neither running nor stopped, please create it first")
|
||||
|
||||
if err := ctl.ParseDatadir(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := config.ReadFromFile(datadir); err != nil {
|
||||
if _, err := os.Stat(datadir); os.IsNotExist(err) {
|
||||
return constants.ErrDatadirNotExisting
|
||||
}
|
||||
|
||||
if isRunning, err := ctl.IsNigiriRunning(); err != nil {
|
||||
return err
|
||||
} else if !isRunning {
|
||||
if delete {
|
||||
if isStopped, err := ctl.IsNigiriStopped(); err != nil {
|
||||
return err
|
||||
} else if !isStopped {
|
||||
return constants.ErrNigiriNotExisting
|
||||
}
|
||||
} else {
|
||||
return constants.ErrNigiriNotRunning
|
||||
}
|
||||
}
|
||||
|
||||
if err := ctl.ReadConfigFile(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -47,40 +58,15 @@ func stop(cmd *cobra.Command, args []string) error {
|
||||
delete, _ := cmd.Flags().GetBool("delete")
|
||||
datadir, _ := cmd.Flags().GetString("datadir")
|
||||
|
||||
bashCmd := getStopBashCmd(datadir, delete)
|
||||
if err := bashCmd.Run(); err != nil {
|
||||
ctl, err := controller.NewController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if delete {
|
||||
fmt.Println("Removing data from volumes...")
|
||||
if err := cleanVolumes(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configFile := getPath(datadir, "config")
|
||||
envFile := getPath(datadir, "env")
|
||||
|
||||
fmt.Println("Removing configuration file...")
|
||||
if err := os.Remove(configFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Removing environmet file...")
|
||||
if err := os.Remove(envFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Nigiri has been cleaned up successfully.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStopBashCmd(datadir string, delete bool) *exec.Cmd {
|
||||
composePath := getPath(datadir, "compose")
|
||||
envPath := getPath(datadir, "env")
|
||||
env := loadEnv(envPath)
|
||||
composePath := ctl.GetResourcePath(datadir, "compose")
|
||||
configPath := ctl.GetResourcePath(datadir, "config")
|
||||
envPath := ctl.GetResourcePath(datadir, "env")
|
||||
env := ctl.LoadComposeEnvironment(envPath)
|
||||
|
||||
bashCmd := exec.Command("docker-compose", "-f", composePath, "stop")
|
||||
if delete {
|
||||
@@ -90,34 +76,27 @@ func getStopBashCmd(datadir string, delete bool) *exec.Cmd {
|
||||
bashCmd.Stderr = os.Stderr
|
||||
bashCmd.Env = env
|
||||
|
||||
return bashCmd
|
||||
}
|
||||
|
||||
// cleanVolumes navigates into <datadir>/resources/volumes/<network>
|
||||
// and deletes all files and directories but the *.conf config files.
|
||||
func cleanVolumes(datadir string) error {
|
||||
network := config.GetString(config.Network)
|
||||
attachLiquid := config.GetBool(config.AttachLiquid)
|
||||
if attachLiquid {
|
||||
network = fmt.Sprintf("liquid%s", network)
|
||||
}
|
||||
volumedir := filepath.Join(datadir, "resources", "volumes", network)
|
||||
|
||||
subdirs, err := ioutil.ReadDir(volumedir)
|
||||
if err != nil {
|
||||
if err := bashCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, d := range subdirs {
|
||||
volumedir := filepath.Join(volumedir, d.Name())
|
||||
subsubdirs, _ := ioutil.ReadDir(volumedir)
|
||||
for _, sd := range subsubdirs {
|
||||
if sd.IsDir() {
|
||||
if err := os.RemoveAll(filepath.Join(volumedir, sd.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if delete {
|
||||
fmt.Println("Removing data from volumes...")
|
||||
if err := ctl.CleanResourceVolumes(datadir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Removing configuration file...")
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Removing environmet file...")
|
||||
if err := os.Remove(envPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Nigiri has been cleaned up successfully.")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -5,14 +5,7 @@ import (
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
Datadir = "datadir"
|
||||
Network = "network"
|
||||
Filename = "nigiri.config.json"
|
||||
AttachLiquid = "attachLiquid"
|
||||
Version = "version"
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
)
|
||||
|
||||
var vip *viper.Viper
|
||||
@@ -25,37 +18,43 @@ func init() {
|
||||
setConfigFromDefaults(vip, defaults)
|
||||
}
|
||||
|
||||
func Viper() *viper.Viper {
|
||||
type Config struct{}
|
||||
|
||||
func (c *Config) Viper() *viper.Viper {
|
||||
return vip
|
||||
}
|
||||
|
||||
func ReadFromFile(path string) error {
|
||||
vip.SetConfigFile(filepath.Join(path, Filename))
|
||||
func (c *Config) ReadFromFile(path string) error {
|
||||
vip.SetConfigFile(filepath.Join(path, constants.Filename))
|
||||
return vip.ReadInConfig()
|
||||
}
|
||||
|
||||
func WriteConfig(path string) error {
|
||||
func (c *Config) WriteConfig(path string) error {
|
||||
vip.SetConfigFile(path)
|
||||
return vip.WriteConfig()
|
||||
}
|
||||
|
||||
func GetString(str string) string {
|
||||
func (c *Config) GetString(str string) string {
|
||||
return vip.GetString(str)
|
||||
}
|
||||
|
||||
func GetBool(str string) bool {
|
||||
func (c *Config) GetBool(str string) bool {
|
||||
return vip.GetBool(str)
|
||||
}
|
||||
|
||||
func GetPath() string {
|
||||
func (c *Config) GetPath() string {
|
||||
return getPath()
|
||||
}
|
||||
|
||||
func getPath() string {
|
||||
home, _ := homedir.Expand("~")
|
||||
return filepath.Join(home, ".nigiri")
|
||||
}
|
||||
|
||||
func newDefaultConfig(v *viper.Viper) {
|
||||
v.SetDefault(Datadir, GetPath())
|
||||
v.SetDefault(Network, "regtest")
|
||||
v.SetDefault(AttachLiquid, false)
|
||||
v.SetDefault(constants.Datadir, getPath())
|
||||
v.SetDefault(constants.Network, "regtest")
|
||||
v.SetDefault(constants.AttachLiquid, false)
|
||||
}
|
||||
|
||||
func setConfigFromDefaults(v *viper.Viper, d *viper.Viper) {
|
||||
|
||||
66
cli/constants/constants.go
Normal file
66
cli/constants/constants.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// Datadir key in config json
|
||||
Datadir = "datadir"
|
||||
// Network key in config json
|
||||
Network = "network"
|
||||
// Filename key in config json
|
||||
Filename = "nigiri.config.json"
|
||||
// AttachLiquid key in config json
|
||||
AttachLiquid = "attachLiquid"
|
||||
// Version key in config json
|
||||
Version = "version"
|
||||
)
|
||||
|
||||
var (
|
||||
AvaliableNetworks = []string{"regtest"}
|
||||
NigiriImages = []string{
|
||||
"vulpemventures/bitcoin:latest",
|
||||
"vulpemventures/electrs:latest",
|
||||
"vulpemventures/esplora:latest",
|
||||
"vulpemventures/nigiri-chopsticks:latest",
|
||||
"vulpemventures/liquid:latest",
|
||||
"vulpemventures/electrs-liquid:latest",
|
||||
}
|
||||
DefaultEnv = map[string]interface{}{
|
||||
"ports": map[string]map[string]int{
|
||||
"bitcoin": map[string]int{
|
||||
"node": 18433,
|
||||
"esplora": 5000,
|
||||
"electrs": 3002,
|
||||
"electrs_rpc": 51401,
|
||||
"chopsticks": 3000,
|
||||
},
|
||||
"liquid": map[string]int{
|
||||
"node": 7041,
|
||||
"esplora": 5001,
|
||||
"electrs": 3012,
|
||||
"electrs_rpc": 60401,
|
||||
"chopsticks": 3001,
|
||||
},
|
||||
},
|
||||
"urls": map[string]string{
|
||||
"bitcoin_esplora": "http://localhost:3000",
|
||||
"liquid_esplora": "http://localhost:3001",
|
||||
},
|
||||
}
|
||||
|
||||
ErrInvalidNetwork = errors.New("Network provided is not valid")
|
||||
ErrInvalidDatadir = errors.New("Datadir provided is not valid: it must be an absolute path")
|
||||
ErrInvalidServiceName = errors.New("Service provided is not valid")
|
||||
ErrInvalidArgs = errors.New("Invalid number of args")
|
||||
ErrInvalidJSON = errors.New("JSON environment provided is not valid: missing required fields")
|
||||
ErrMalformedJSON = errors.New("Failed to parse malformed JSON environment")
|
||||
ErrEmptyJSON = errors.New("JSON environment provided is not valid: it must not be empty")
|
||||
ErrDatadirNotExisting = errors.New("Datadir provided is not valid: it must be an existing path")
|
||||
ErrNigiriNotRunning = errors.New("Nigiri is not running")
|
||||
ErrNigiriNotExisting = errors.New("Nigiri does not exists, cannot delete")
|
||||
ErrNigiriAlreadyRunning = errors.New("Nigiri is already running, please stop it first")
|
||||
ErrNigiriLiquidNotEnabled = errors.New("Nigiri has been started with no Liquid sidechain.\nPlease stop and restart it using the --liquid flag")
|
||||
ErrDockerNotRunning = errors.New("Nigiri requires the Docker daemon to be running, but it not seems to be started")
|
||||
)
|
||||
187
cli/controller/controller.go
Normal file
187
cli/controller/controller.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/vulpemventures/nigiri/cli/config"
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
)
|
||||
|
||||
var services = map[string]bool{
|
||||
"node": true,
|
||||
"esplora": true,
|
||||
"electrs": true,
|
||||
"chopsticks": true,
|
||||
}
|
||||
|
||||
// Controller implements useful functions to securely parse flags provided at run-time
|
||||
// and to interact with the resources used by Nigiri:
|
||||
// * docker
|
||||
// * .env for docker-compose
|
||||
// * nigiri.config.json config file
|
||||
type Controller struct {
|
||||
config *config.Config
|
||||
parser *Parser
|
||||
docker *Docker
|
||||
env *Env
|
||||
}
|
||||
|
||||
// NewController returns a new Controller instance or error
|
||||
func NewController() (*Controller, error) {
|
||||
c := &Controller{}
|
||||
|
||||
dockerClient := &Docker{}
|
||||
if err := dockerClient.New(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.env = &Env{}
|
||||
c.parser = newParser(services)
|
||||
c.docker = dockerClient
|
||||
c.config = &config.Config{}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ParseNetwork checks if a valid network has been provided
|
||||
func (c *Controller) ParseNetwork(network string) error {
|
||||
return c.parser.parseNetwork(network)
|
||||
}
|
||||
|
||||
// ParseDatadir checks if a valid datadir has been provided
|
||||
func (c *Controller) ParseDatadir(path string) error {
|
||||
return c.parser.parseDatadir(path)
|
||||
}
|
||||
|
||||
// ParseEnv checks if a valid JSON format for docker compose has been provided
|
||||
func (c *Controller) ParseEnv(env string) (string, error) {
|
||||
return c.parser.parseEnvJSON(env)
|
||||
}
|
||||
|
||||
// ParseServiceName checks if a valid service has been provided
|
||||
func (c *Controller) ParseServiceName(name string) error {
|
||||
return c.parser.parseServiceName(name)
|
||||
}
|
||||
|
||||
// IsNigiriRunning checks if nigiri is running by looking if the bitcoin
|
||||
// services are in the list of docker running containers
|
||||
func (c *Controller) IsNigiriRunning() (bool, error) {
|
||||
if !c.docker.isDockerRunning() {
|
||||
return false, constants.ErrDockerNotRunning
|
||||
}
|
||||
return c.docker.isNigiriRunning(), nil
|
||||
}
|
||||
|
||||
// IsNigiriStopped checks that nigiri is not actually running and that
|
||||
// the bitcoin services appear in the list of non running containers
|
||||
func (c *Controller) IsNigiriStopped() (bool, error) {
|
||||
if !c.docker.isDockerRunning() {
|
||||
return false, constants.ErrDockerNotRunning
|
||||
}
|
||||
return c.docker.isNigiriStopped(), nil
|
||||
}
|
||||
|
||||
// WriteComposeEnvironment creates a .env in datadir used by
|
||||
// the docker-compose YAML file resource
|
||||
func (c *Controller) WriteComposeEnvironment(datadir, env string) error {
|
||||
return c.env.writeEnvForCompose(datadir, env)
|
||||
}
|
||||
|
||||
// ReadComposeEnvironment reads from .env and returns it as a useful type
|
||||
func (c *Controller) ReadComposeEnvironment(datadir string) (map[string]interface{}, error) {
|
||||
return c.env.readEnvForCompose(datadir)
|
||||
}
|
||||
|
||||
// LoadComposeEnvironment returns an os.Environ created from datadir/.env resource
|
||||
func (c *Controller) LoadComposeEnvironment(datadir string) []string {
|
||||
return c.env.load(datadir)
|
||||
}
|
||||
|
||||
// WriteConfigFile writes the configuration handled by the underlying viper
|
||||
// into the file at filedir path
|
||||
func (c *Controller) WriteConfigFile(filedir string) error {
|
||||
return c.config.WriteConfig(filedir)
|
||||
}
|
||||
|
||||
// ReadConfigFile reads the configuration of the file at filedir path
|
||||
func (c *Controller) ReadConfigFile(filedir string) error {
|
||||
return c.config.ReadFromFile(filedir)
|
||||
}
|
||||
|
||||
// GetConfigBoolField returns a bool field of the config file
|
||||
func (c *Controller) GetConfigBoolField(field string) bool {
|
||||
return c.config.GetBool(field)
|
||||
}
|
||||
|
||||
// GetConfigStringField returns a string field of the config file
|
||||
func (c *Controller) GetConfigStringField(field string) string {
|
||||
return c.config.GetString(field)
|
||||
}
|
||||
|
||||
// NewDatadirFromDefault copies the default ~/.nigiri at the desidered path
|
||||
// and cleans the docker volumes to make a fresh Nigiri instance
|
||||
func (c *Controller) NewDatadirFromDefault(datadir string) error {
|
||||
defaultDatadir := c.config.GetPath()
|
||||
cmd := exec.Command("cp", "-R", filepath.Join(defaultDatadir, "resources"), datadir)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.CleanResourceVolumes(datadir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetResourcePath returns the absolute path of the requested resource
|
||||
func (c *Controller) GetResourcePath(datadir, resource string) string {
|
||||
if resource == "compose" {
|
||||
network := c.config.GetString(constants.Network)
|
||||
if c.config.GetBool(constants.AttachLiquid) {
|
||||
network += "-liquid"
|
||||
}
|
||||
return filepath.Join(datadir, "resources", fmt.Sprintf("docker-compose-%s.yml", network))
|
||||
}
|
||||
if resource == "env" {
|
||||
return filepath.Join(datadir, ".env")
|
||||
}
|
||||
if resource == "config" {
|
||||
return filepath.Join(datadir, "nigiri.config.json")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CleanResourceVolumes recursively deletes the content of the
|
||||
// docker volumes in the resource path
|
||||
func (c *Controller) CleanResourceVolumes(datadir string) error {
|
||||
network := c.config.GetString(constants.Network)
|
||||
attachLiquid := c.config.GetBool(constants.AttachLiquid)
|
||||
if attachLiquid {
|
||||
network = fmt.Sprintf("liquid%s", network)
|
||||
}
|
||||
volumedir := filepath.Join(datadir, "resources", "volumes", network)
|
||||
|
||||
return c.docker.cleanVolumes(volumedir)
|
||||
}
|
||||
|
||||
// GetDefaultDatadir returns the absolute path of Nigiri default directory
|
||||
func (c *Controller) GetDefaultDatadir() string {
|
||||
return c.config.GetPath()
|
||||
}
|
||||
|
||||
// GetServiceName returns the right name of the requested service
|
||||
// If requesting a name for a Liquid service, then the suffix -liquid
|
||||
// is appended to the canonical name except for "node" that is mapped
|
||||
// to either "bitcoin" or "liquid"
|
||||
func (c *Controller) GetServiceName(name string, liquid bool) string {
|
||||
service := name
|
||||
if service == "node" {
|
||||
service = "bitcoin"
|
||||
}
|
||||
if liquid {
|
||||
if service == "bitcoin" {
|
||||
service = "liquid"
|
||||
} else {
|
||||
service = fmt.Sprintf("%s-liquid", service)
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
101
cli/controller/docker.go
Normal file
101
cli/controller/docker.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
)
|
||||
|
||||
// Docker type handles interfaction with containers via docker and docker-compose
|
||||
type Docker struct {
|
||||
cli *client.Client
|
||||
}
|
||||
|
||||
// New initialize a new Docker handler
|
||||
func (d *Docker) New() error {
|
||||
cli, err := client.NewEnvClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.cli = cli
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Docker) isDockerRunning() bool {
|
||||
_, err := d.cli.ContainerList(context.Background(), types.ContainerListOptions{All: false})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Docker) findNigiriContainers(listAllContainers bool) bool {
|
||||
containers, _ := d.cli.ContainerList(context.Background(), types.ContainerListOptions{All: listAllContainers})
|
||||
|
||||
if len(containers) <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
images := []string{}
|
||||
for _, c := range containers {
|
||||
images = append(images, c.Image)
|
||||
}
|
||||
|
||||
for _, nigiriImage := range constants.NigiriImages {
|
||||
// just check if services for bitcoin chain are up and running
|
||||
if !strings.Contains(nigiriImage, "liquid") {
|
||||
if !contains(images, nigiriImage) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Docker) isNigiriRunning() bool {
|
||||
return d.findNigiriContainers(false)
|
||||
}
|
||||
|
||||
func (d *Docker) isNigiriStopped() bool {
|
||||
isRunning := d.isNigiriRunning()
|
||||
if !isRunning {
|
||||
return d.findNigiriContainers(true)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *Docker) cleanVolumes(path string) error {
|
||||
subdirs, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, d := range subdirs {
|
||||
path := filepath.Join(path, d.Name())
|
||||
subsubdirs, _ := ioutil.ReadDir(path)
|
||||
for _, sd := range subsubdirs {
|
||||
if sd.IsDir() {
|
||||
if err := os.RemoveAll(filepath.Join(path, sd.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(list []string, elem string) bool {
|
||||
for _, l := range list {
|
||||
if l == elem {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
124
cli/controller/env.go
Normal file
124
cli/controller/env.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
)
|
||||
|
||||
// Env implements functions for interacting with the environment file used by
|
||||
// docker-compose to dynamically set service ports and variables
|
||||
type Env struct{}
|
||||
|
||||
func (e *Env) writeEnvForCompose(path, strJSON string) error {
|
||||
var env map[string]interface{}
|
||||
err := json.Unmarshal([]byte(strJSON), &env)
|
||||
if err != nil {
|
||||
return constants.ErrMalformedJSON
|
||||
}
|
||||
|
||||
fileContent := ""
|
||||
for chain, services := range env["ports"].(map[string]interface{}) {
|
||||
for k, v := range services.(map[string]interface{}) {
|
||||
fileContent += fmt.Sprintf("%s_%s_PORT=%d\n", strings.ToUpper(chain), strings.ToUpper(k), int(v.(float64)))
|
||||
}
|
||||
}
|
||||
for hostname, url := range env["urls"].(map[string]interface{}) {
|
||||
fileContent += fmt.Sprintf("%s_URL=%s\n", strings.ToUpper(hostname), url.(string))
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, []byte(fileContent), os.ModePerm)
|
||||
}
|
||||
|
||||
func (e *Env) readEnvForCompose(path string) (map[string]interface{}, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
ports := map[string]map[string]int{
|
||||
"bitcoin": map[string]int{},
|
||||
"liquid": map[string]int{},
|
||||
}
|
||||
urls := map[string]string{}
|
||||
// Each line is in the format PREFIX_SERVICE_NAME_SUFFIX=value
|
||||
// PREFIX is either 'BITCOIN' or 'LIQUID', while SUFFIX is either 'PORT' or 'URL'
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
splitLine := strings.Split(line, "=")
|
||||
key := splitLine[0]
|
||||
|
||||
if strings.Contains(key, "PORT") {
|
||||
value, _ := strconv.Atoi(splitLine[1])
|
||||
chain := "bitcoin"
|
||||
if strings.HasPrefix(key, strings.ToUpper("liquid")) {
|
||||
chain = "liquid"
|
||||
}
|
||||
prefix := strings.ToUpper(fmt.Sprintf("%s_", chain))
|
||||
suffix := "_PORT"
|
||||
trimmedKey := strings.ToLower(
|
||||
strings.TrimSuffix(strings.TrimPrefix(key, prefix), suffix),
|
||||
)
|
||||
ports[chain][trimmedKey] = value
|
||||
} else {
|
||||
// Here the prefix is not trimmed
|
||||
value := splitLine[1]
|
||||
suffix := "_URL"
|
||||
trimmedKey := strings.ToLower(strings.TrimSuffix(key, suffix))
|
||||
urls[trimmedKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{"ports": ports, "urls": urls}, nil
|
||||
}
|
||||
|
||||
func (e *Env) load(path string) []string {
|
||||
content, _ := ioutil.ReadFile(path)
|
||||
lines := strings.Split(string(content), "\n")
|
||||
env := os.Environ()
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
env = append(env, line)
|
||||
}
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
type envPortsData struct {
|
||||
Node int `json:"node,omitempty"`
|
||||
Esplora int `json:"esplora,omitempty"`
|
||||
Electrs int `json:"electrs,omitempty"`
|
||||
ElectrsRPC int `json:"electrs_rpc,omitempty"`
|
||||
Chopsticks int `json:"chopsticks,omitempty"`
|
||||
}
|
||||
type envPorts struct {
|
||||
Bitcoin *envPortsData `json:"bitcoin,omitempty"`
|
||||
Liquid *envPortsData `json:"liquid,omitempty"`
|
||||
}
|
||||
type envUrls struct {
|
||||
BitcoinEsplora string `json:"bitcoin_esplora,omitempty"`
|
||||
LiquidEsplora string `json:"liquid_esplora,omitempty"`
|
||||
}
|
||||
type envJSON struct {
|
||||
Ports *envPorts `json:"ports,omitempty"`
|
||||
Urls *envUrls `json:"urls,omitempty"`
|
||||
}
|
||||
|
||||
func (e envJSON) copy() envJSON {
|
||||
var v envJSON
|
||||
bytes, _ := json.Marshal(e)
|
||||
json.Unmarshal(bytes, &v)
|
||||
|
||||
return v
|
||||
}
|
||||
58
cli/controller/parser.go
Normal file
58
cli/controller/parser.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
)
|
||||
|
||||
// Parser implements functions for parsing flags, JSON files
|
||||
// and system directories passed to the CLI commands
|
||||
type Parser struct {
|
||||
services map[string]bool
|
||||
}
|
||||
|
||||
func newParser(services map[string]bool) *Parser {
|
||||
p := &Parser{services}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Parser) parseNetwork(network string) error {
|
||||
for _, n := range constants.AvaliableNetworks {
|
||||
if network == n {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return constants.ErrInvalidNetwork
|
||||
}
|
||||
|
||||
func (p *Parser) parseDatadir(path string) error {
|
||||
if !filepath.IsAbs(path) {
|
||||
return constants.ErrInvalidDatadir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseEnvJSON(strJSON string) (string, error) {
|
||||
// merge default json and incoming json by parsing DefaultEnv to
|
||||
// envJSON type and then parsing the incoming json using the same variable
|
||||
var parsedJSON envJSON
|
||||
defaultJSON, _ := json.Marshal(constants.DefaultEnv)
|
||||
json.Unmarshal(defaultJSON, &parsedJSON)
|
||||
err := json.Unmarshal([]byte(strJSON), &parsedJSON)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", constants.ErrMalformedJSON
|
||||
}
|
||||
merged, _ := json.Marshal(parsedJSON)
|
||||
return string(merged), nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseServiceName(name string) error {
|
||||
if !p.services[name] {
|
||||
return constants.ErrInvalidServiceName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
159
cli/controller/parser_test.go
Normal file
159
cli/controller/parser_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/vulpemventures/nigiri/cli/constants"
|
||||
)
|
||||
|
||||
func TestParserParseNetwork(t *testing.T) {
|
||||
p := &Parser{}
|
||||
|
||||
validNetworks := []string{"regtest"}
|
||||
for _, n := range validNetworks {
|
||||
err := p.parseNetwork(n)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserParseDatadir(t *testing.T) {
|
||||
p := &Parser{}
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
validDatadirs := []string{currentDir}
|
||||
for _, n := range validDatadirs {
|
||||
err := p.parseDatadir(n)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserParseEnvJSON(t *testing.T) {
|
||||
p := &Parser{}
|
||||
|
||||
for _, e := range testJSONs {
|
||||
parsedJSON, _ := json.Marshal(e)
|
||||
mergedJSON, err := p.parseEnvJSON(string(parsedJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(mergedJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserParseNetworkShouldFail(t *testing.T) {
|
||||
p := &Parser{}
|
||||
|
||||
invalidNetworks := []string{"simnet", "testnet"}
|
||||
for _, n := range invalidNetworks {
|
||||
err := p.parseNetwork(n)
|
||||
if err == nil {
|
||||
t.Fatalf("Should have been failed before")
|
||||
}
|
||||
if err != constants.ErrInvalidNetwork {
|
||||
t.Fatalf("Got: %s, wanted: %s", err, constants.ErrInvalidNetwork)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserParseDatadirShouldFail(t *testing.T) {
|
||||
p := &Parser{}
|
||||
|
||||
invalidDatadirs := []string{"."}
|
||||
for _, d := range invalidDatadirs {
|
||||
err := p.parseDatadir(d)
|
||||
if err == nil {
|
||||
t.Fatalf("Should have been failed before")
|
||||
}
|
||||
if err != constants.ErrInvalidDatadir {
|
||||
t.Fatalf("Got: %s, wanted: %s", err, constants.ErrInvalidDatadir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testJSONs = []map[string]interface{}{
|
||||
// only btc services
|
||||
{
|
||||
"urls": map[string]string{
|
||||
"bitcoin_esplora": "https://blockstream.info/",
|
||||
},
|
||||
"ports": map[string]map[string]int{
|
||||
"bitcoin": map[string]int{
|
||||
"node": 1111,
|
||||
"esplora": 2222,
|
||||
"electrs": 3333,
|
||||
"chopsticks": 4444,
|
||||
},
|
||||
},
|
||||
},
|
||||
// btc and liquid services
|
||||
{
|
||||
"urls": map[string]string{
|
||||
"bitcoin_esplora": "https://blockstream.info/",
|
||||
"liquid_esplora": "http://blockstream.info/liquid",
|
||||
},
|
||||
"ports": map[string]map[string]int{
|
||||
"bitcoin": map[string]int{
|
||||
"node": 1111,
|
||||
"esplora": 2222,
|
||||
"electrs": 3333,
|
||||
"chopsticks": 4444,
|
||||
},
|
||||
"liquid": map[string]int{
|
||||
"node": 5555,
|
||||
"esplora": 6666,
|
||||
"electrs": 7777,
|
||||
"chopsticks": 8888,
|
||||
},
|
||||
},
|
||||
},
|
||||
// incomplete examples:
|
||||
// incomplete bitcoin services
|
||||
{
|
||||
"ports": map[string]map[string]int{
|
||||
"bitcoin": map[string]int{
|
||||
"esplora": 1111,
|
||||
"electrs": 2222,
|
||||
"chopsticks": 3333,
|
||||
},
|
||||
},
|
||||
"urls": map[string]string{
|
||||
"bitcoin_esplora": "http://test.com/api",
|
||||
},
|
||||
},
|
||||
// bitcoin services ports and liquid service url
|
||||
{
|
||||
"ports": map[string]map[string]int{
|
||||
"bitcoin": map[string]int{
|
||||
"node": 1111,
|
||||
"esplora": 2222,
|
||||
"electrs": 3333,
|
||||
"chopsticks": 4444,
|
||||
},
|
||||
},
|
||||
"urls": map[string]string{
|
||||
"liquid_esplora": "http://test.com/liquid/api",
|
||||
},
|
||||
},
|
||||
// liquid services ports and bitcoin service url
|
||||
{
|
||||
"ports": map[string]map[string]int{
|
||||
"liquid": map[string]int{
|
||||
"node": 1111,
|
||||
"esplora": 2222,
|
||||
"electrs": 3333,
|
||||
"chopsticks": 4444,
|
||||
},
|
||||
},
|
||||
"urls": map[string]string{
|
||||
"bitcoin_esplora": "http://test.com/api",
|
||||
},
|
||||
},
|
||||
// empty config
|
||||
{},
|
||||
}
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
- bitcoin
|
||||
ports:
|
||||
- ${BITCOIN_ELECTRS_RPC_PORT}:60401
|
||||
- 3002:3002
|
||||
- ${BITCOIN_ELECTRS_PORT}:3002
|
||||
volumes:
|
||||
- ./volumes/liquidregtest/config/:/config
|
||||
restart: unless-stopped
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
- liquid
|
||||
ports:
|
||||
- ${LIQUID_ELECTRS_RPC_PORT}:60401
|
||||
- 3022:3002
|
||||
- ${LIQUID_ELECTRS_PORT}:3002
|
||||
volumes:
|
||||
- ./volumes/liquidregtest/liquid-config/:/config
|
||||
restart: unless-stopped
|
||||
@@ -96,21 +96,25 @@ services:
|
||||
local:
|
||||
ipv4_address: 10.10.0.14
|
||||
links:
|
||||
- electrs
|
||||
- chopsticks
|
||||
depends_on:
|
||||
- electrs
|
||||
- chopsticks
|
||||
environment:
|
||||
API_URL: ${BITCOIN_ESPLORA_URL}
|
||||
ports:
|
||||
- ${BITCOIN_ESPLORA_PORT}:5000
|
||||
restart: unless-stopped
|
||||
esplora-liquid:
|
||||
image: vulpemventures/esplora-liquid:latest
|
||||
image: vulpemventures/esplora:latest
|
||||
networks:
|
||||
local:
|
||||
ipv4_address: 10.10.0.15
|
||||
links:
|
||||
- electrs-liquid
|
||||
- chopsticks-liquid
|
||||
depends_on:
|
||||
- electrs-liquid
|
||||
- chopsticks-liquid
|
||||
environment:
|
||||
API_URL: ${LIQUID_ESPLORA_URL}
|
||||
ports:
|
||||
- ${LIQUID_ESPLORA_PORT}:5000
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -41,7 +41,7 @@ services:
|
||||
- bitcoin
|
||||
ports:
|
||||
- ${BITCOIN_ELECTRS_RPC_PORT}:60401
|
||||
- 3002:3002
|
||||
- ${BITCOIN_ELECTRS_PORT}:3002
|
||||
volumes:
|
||||
- ./volumes/regtest/config/:/config
|
||||
restart: unless-stopped
|
||||
@@ -52,9 +52,11 @@ services:
|
||||
local:
|
||||
ipv4_address: 10.10.0.12
|
||||
links:
|
||||
- electrs
|
||||
- chopsticks
|
||||
depends_on:
|
||||
- electrs
|
||||
- chopsticks
|
||||
environment:
|
||||
API_URL: ${BITCOIN_ESPLORA_URL}
|
||||
ports:
|
||||
- ${BITCOIN_ESPLORA_PORT}:5000
|
||||
restart: unless-stopped
|
||||
|
||||
Reference in New Issue
Block a user