Files
nigiri/cli/cmd/start.go
Pietralberto Mazza b7a61e9db1 Fix hardcoded ports & logs (#27)
* disable liquid faucet for next release

* use vars instead of hardcoding services external ports

* add .env to gitignore

* change paths

* add --ports flag to set services external ports

* remove roadmap

* fix change paths
2019-05-14 18:54:21 +02:00

315 lines
7.5 KiB
Go

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"
)
const listAll = true
var StartCmd = &cobra.Command{
Use: "start",
Short: "Build and start Nigiri",
RunE: start,
PreRunE: startChecks,
}
func startChecks(cmd *cobra.Command, args []string) error {
network, _ := cmd.Flags().GetString("network")
datadir, _ := cmd.Flags().GetString("datadir")
ports, _ := cmd.Flags().GetString("ports")
// 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()
if err != nil {
return err
}
if isRunning {
return fmt.Errorf("Nigiri is already running, please stop it first")
}
// scratch datadir if not exists
if err := os.MkdirAll(datadir, 0755); err != nil {
return err
}
// 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 {
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 {
return err
}
if !exists {
filedir := filepath.Join(datadir, "nigiri.config.json")
if err := config.WriteConfig(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 = filepath.Join(".", ".env")
if err := writeComposeEnvFile(filedir, ports); err != nil {
return err
}
}
if err := config.ReadFromFile(datadir); err != nil {
return err
}
return nil
}
func start(cmd *cobra.Command, args []string) error {
bashCmd, err := getStartBashCmd()
if err != nil {
return err
}
err = bashCmd.Run()
if err != nil {
return err
}
path := filepath.Join(".", ".env")
ports, err := readComposeEnvFile(path)
if err != nil {
return err
}
for chain, services := range ports {
fmt.Printf("%s services:\n", chain)
for name, port := range services {
fmt.Printf("\t%s: localhost:%d\n", name, port)
}
}
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 getComposePath() string {
viper := config.Viper()
datadir := viper.GetString("datadir")
network := viper.GetString("network")
attachLiquid := viper.GetBool("attachLiquid")
if attachLiquid {
network += "-liquid"
}
return filepath.Join(datadir, "resources", fmt.Sprintf("docker-compose-%s.yml", network))
}
func nigiriIsRunning() (bool, error) {
listOnlyRunningContainers := !listAll
return nigiriExists(listOnlyRunningContainers)
}
func nigiriExistsAndNotRunning() (bool, error) {
return nigiriExists(listAll)
}
func getStartBashCmd() (*exec.Cmd, error) {
composePath := getComposePath()
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
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
}