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:
Pietralberto Mazza
2019-12-09 14:58:32 +00:00
committed by Marco Argentieri
parent d0b3676c14
commit e02c2fdd0d
15 changed files with 909 additions and 458 deletions

View 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
View 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
View 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
View 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
}

View 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
{},
}