diff --git a/testutil/compose/README.md b/testutil/compose/README.md
index 3874a176d..434ef2e87 100644
--- a/testutil/compose/README.md
+++ b/testutil/compose/README.md
@@ -7,22 +7,23 @@ Compose is a tool that generates `docker-compose.yml` files such that different
The aim is for developers to be able to debug features and check functionality of clusters on their local machines.
The `compose` command should be executed in sequential steps:
- 1. `compose clean`: Cleans the compose directory of existing artifacts.
- 2. `compose define`: Defines the target cluster and how keys are to be created.
- 1. It outputs `config.json` which is the compose config
- 1. It also creates `docker-compose.yml` in order to create `cluster-definition.json` if `keygen==dkg`.
- 1. `compose lock`: Creates `docker-compose.yml` to create threshold key shares and the `cluster-lock.json` file.
- 1. `compose run`: Creates `docker-compose.yml` to run the cluster.
+ 1. `compose new`: Creates a new config.json that defines what will be composed.
+ 2. `compose define`: Creates a docker-compose.yml that executes `charon create dkg` if keygen==dkg.
+ 3. `compose lock`: Creates a docker-compose.yml that executes `charon create cluster` or `charon dkg`.
+ 4. `compose run`: Creates a docker-compose.yml that executes `charon run`.
+
+The `compose` command also includes some convenience functions.
+- `compose clean`: Cleans the compose directory of existing files.
+- `compose auto`: Runs `compose define && compose lock && compose run`.
Note that compose automatically runs `docker-compose up` at the end of each command. This can be disabled via `--up=false`.
-The `compose define` step configures the target cluster and key generation process. It supports the following flags:
+The `compose new` step configures the target cluster and key generation process. It supports the following flags:
- `--keygen`: Key generation process: `create` or `dkg`.
- create` creates keys locally via `charon create cluster`
- `dkg` creates keys via `charon create dkg` followed by `charon dkg`.
- `--split-keys-dir`: Path to a folder containing keys to split. Only applicable to `--keygen=create`.
- `--build-local`: Build a local charon binary from source. Note this requires the `CHARON_REPO` path env var. Devs are encouraged to put this in the bash profile.
- - `--seed`: Randomness seed, can be used to produce deterministic p2pkeys for dkg.
## Usage
Install the `compose` binary:
@@ -45,7 +46,7 @@ cd charon-compose
```
Create the default cluster:
```
-compose clean && compose define && compose lock && compose run
+compose clean && compose new && compose define && compose lock && compose run
```
Monitor the cluster via `grafana` and `jaeger`:
```
@@ -54,8 +55,6 @@ open http://localhost:16686 # Open Jaeger dashboard
```
Creating a DKG based cluster that uses locally built binary:
```
-compose clean
-compose define --keygen=dkg --build-local
-compose lock
-compose run
+compose new --keygen=dkg --build-local
+compose auto
```
diff --git a/testutil/compose/compose/main.go b/testutil/compose/compose/main.go
index 38a4dd826..5063c8339 100644
--- a/testutil/compose/compose/main.go
+++ b/testutil/compose/compose/main.go
@@ -16,23 +16,24 @@
// Command compose provides a tool to run, test, debug local charon clusters
// using docker-compose.
//
-// It consists of three steps:
-// - compose define: Creates config.json (and p2pkeys) and a docker-compose.yml to create a cluster definition file.
-// - compose lock: Creates docker-compose.yml to generates keys and cluster lock file.
-// - compose run: Creates docker-compose.yml that runs the cluster.
+// It consists of multiple steps:
+// - compose new: Creates a new config.json that defines what will be composed.
+// - compose define: Creates a docker-compose.yml that executes `charon create dkg` if keygen==dkg.
+// - compose lock: Creates a docker-compose.yml that executes `charon create cluster` or `charon dkg`.
+// - compose run: Creates a docker-compose.yml that executes `charon run`.
package main
import (
"context"
"os"
"os/exec"
- "time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
+ "github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/testutil/compose"
)
@@ -46,54 +47,87 @@ func newRootCmd() *cobra.Command {
Short: "Charon Compose - Run, test, and debug a developer-focussed insecure local charon cluster using docker-compose",
}
+ root.AddCommand(newNewCmd())
root.AddCommand(newCleanCmd())
- root.AddCommand(newDefineCmd())
- root.AddCommand(newLockCmd())
- root.AddCommand(newRunCmd())
+ root.AddCommand(newAutoCmd())
+ root.AddCommand(newDockerCmd(
+ "define",
+ "Creates a docker-compose.yml that executes `charon create dkg` if keygen==dkg",
+ compose.Define,
+ ))
+ root.AddCommand(newDockerCmd(
+ "lock",
+ "Creates a docker-compose.yml that executes `charon create cluster` or `charon dkg`",
+ compose.Lock,
+ ))
+ root.AddCommand(newDockerCmd(
+ "run",
+ "Creates a docker-compose.yml that executes `charon run`",
+ compose.Run,
+ ))
return root
}
-func newRunCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "run",
- Short: "Creates docker-compose.yml that runs the cluster.",
- }
+// newDockerRunFunc returns a cobra run function that generates docker-compose.yml files and executes it.
+func newDockerRunFunc(topic string, dir *string, up *bool, runFunc func(context.Context, string) error) func(cmd *cobra.Command, _ []string) error {
+ return func(cmd *cobra.Command, _ []string) (err error) {
+ ctx := log.WithTopic(cmd.Context(), topic)
+ defer func() {
+ if err != nil {
+ log.Error(ctx, "Fatal error", err)
+ }
+ }()
- up := addUpFlag(cmd.Flags())
- dir := addDirFlag(cmd.Flags())
+ log.Info(ctx, "Running compose command", z.Str("command", topic))
- cmd.RunE = func(cmd *cobra.Command, _ []string) error {
- if err := compose.Run(cmd.Context(), *dir); err != nil {
+ if err := runFunc(ctx, *dir); err != nil {
return err
}
if *up {
- return execUp(cmd.Context(), *dir)
+ return execUp(ctx, *dir)
}
return nil
}
+}
+
+// newDockerCmd returns a cobra command that generates docker-compose.yml files and executes it.
+func newDockerCmd(use string, short string, run func(context.Context, string) error) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: use,
+ Short: short,
+ }
+
+ up := addUpFlag(cmd.Flags())
+ dir := addDirFlag(cmd.Flags())
+ cmd.RunE = newDockerRunFunc(use, dir, up, run)
return cmd
}
-func newLockCmd() *cobra.Command {
+func newAutoCmd() *cobra.Command {
cmd := &cobra.Command{
- Use: "lock",
- Short: "Creates docker-compose.yml to generates keys and cluster lock file.",
+ Use: "auto",
+ Short: "Convenience function that runs `compose define && compose lock && compose run`",
}
- up := addUpFlag(cmd.Flags())
dir := addDirFlag(cmd.Flags())
+ up := true
- cmd.RunE = func(cmd *cobra.Command, _ []string) error {
- if err := compose.Lock(cmd.Context(), *dir); err != nil {
- return err
- }
+ runFuncs := []func(cmd *cobra.Command, _ []string) (err error){
+ newDockerRunFunc("define", dir, &up, compose.Define),
+ newDockerRunFunc("lock", dir, &up, compose.Lock),
+ newDockerRunFunc("run", dir, &up, compose.Run),
+ }
- if *up {
- return execUp(cmd.Context(), *dir)
+ cmd.RunE = func(cmd *cobra.Command, _ []string) (err error) {
+ for _, runFunc := range runFuncs {
+ err := runFunc(cmd, nil)
+ if err != nil {
+ return err
+ }
}
return nil
@@ -102,17 +136,15 @@ func newLockCmd() *cobra.Command {
return cmd
}
-func newDefineCmd() *cobra.Command {
+func newNewCmd() *cobra.Command {
cmd := &cobra.Command{
- Use: "define",
- Short: "Creates config.json (and p2pkeys) and a docker-compose.yml to create a cluster definition file",
+ Use: "new",
+ Short: "Creates a new config.json that defines what will be composed",
}
conf := compose.NewDefaultConfig()
- up := addUpFlag(cmd.Flags())
dir := addDirFlag(cmd.Flags())
- seed := cmd.Flags().Int("seed", int(time.Now().UnixNano()), "Randomness seed")
keygen := cmd.Flags().String("keygen", string(conf.KeyGen), "Key generation process: create, split, dkg")
buildLocal := cmd.Flags().Bool("build-local", conf.BuildLocal, "Enables building a local charon binary from source. Note this requires the CHARON_REPO env var.")
@@ -120,14 +152,12 @@ func newDefineCmd() *cobra.Command {
conf.KeyGen = compose.KeyGen(*keygen)
conf.BuildLocal = *buildLocal
- if err := compose.Define(cmd.Context(), *dir, *seed, conf); err != nil {
+ ctx := log.WithTopic(cmd.Context(), "new")
+ if err := compose.New(ctx, *dir, conf); err != nil {
+ log.Error(ctx, "Fatal error", err)
return err
}
- if *up {
- return execUp(cmd.Context(), *dir)
- }
-
return nil
}
@@ -137,7 +167,7 @@ func newDefineCmd() *cobra.Command {
func newCleanCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "clean",
- Short: "Cleans compose files and artifacts",
+ Short: "Convenience function that cleans the compose directory",
}
dir := addDirFlag(cmd.Flags())
@@ -159,7 +189,6 @@ func addUpFlag(flags *pflag.FlagSet) *bool {
// execUp executes `docker-compose up`.
func execUp(ctx context.Context, dir string) error {
- ctx = log.WithTopic(ctx, "cmd")
log.Info(ctx, "Executing docker-compose up")
cmd := exec.CommandContext(ctx, "docker-compose", "up", "--remove-orphans", "--build")
diff --git a/testutil/compose/config.go b/testutil/compose/config.go
index 19e802afe..7aff478db 100644
--- a/testutil/compose/config.go
+++ b/testutil/compose/config.go
@@ -56,6 +56,7 @@ const (
type step string
const (
+ stepNew step = "new"
stepDefined step = "defined"
stepLocked step = "locked"
)
diff --git a/testutil/compose/define.go b/testutil/compose/define.go
index 1579dfd38..8375f23ec 100644
--- a/testutil/compose/define.go
+++ b/testutil/compose/define.go
@@ -20,12 +20,14 @@ import (
"crypto/ecdsa"
"encoding/json"
"fmt"
+ "io/fs"
"math/rand"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
+ "time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enode"
@@ -61,11 +63,14 @@ func Clean(ctx context.Context, dir string) error {
}
// Define defines a compose cluster; including both keygen and running definitions.
-func Define(ctx context.Context, dir string, seed int, conf Config) error {
- ctx = log.WithTopic(ctx, "define")
-
- if _, err := loadConfig(dir); err == nil {
- return errors.New("compose config already defined; maybe try `compose clean` or `compose lock`")
+func Define(ctx context.Context, dir string) error {
+ conf, err := loadConfig(dir)
+ if errors.Is(err, fs.ErrNotExist) {
+ return errors.New("compose config not found; maybe try `compose new` first")
+ } else if err != nil {
+ return err
+ } else if conf.Step != stepNew {
+ return errors.New("compose config not new, so can't be defined", z.Any("step", conf.Step))
}
if conf.BuildLocal {
@@ -79,7 +84,7 @@ func Define(ctx context.Context, dir string, seed int, conf Config) error {
log.Info(ctx, "Creating node*/p2pkey for ENRs required for charon create dkg")
// charon create dkg requires operator ENRs, so we need to create p2pkeys now.
- p2pkeys, err := newP2PKeys(conf.NumNodes, seed)
+ p2pkeys, err := newP2PKeys(conf.NumNodes)
if err != nil {
return err
}
@@ -233,9 +238,12 @@ func keyToENR(key *ecdsa.PrivateKey) (string, error) {
return p2p.EncodeENR(r)
}
+// p2pSeed can be overridden in tests for deterministic p2pkeys.
+var p2pSeed = time.Now().UnixNano()
+
// newP2PKeys returns a slice of newly generated ecdsa private keys.
-func newP2PKeys(n, seed int) ([]*ecdsa.PrivateKey, error) {
- random := rand.New(rand.NewSource(int64(seed))) //nolint:gosec // Weak random is fine for testing.
+func newP2PKeys(n int) ([]*ecdsa.PrivateKey, error) {
+ random := rand.New(rand.NewSource(p2pSeed)) //nolint:gosec // Weak random is fine for testing.
var resp []*ecdsa.PrivateKey
for i := 0; i < n; i++ {
key, err := ecdsa.GenerateKey(crypto.S256(), random)
diff --git a/testutil/compose/define_test.go b/testutil/compose/define_internal_test.go
similarity index 71%
rename from testutil/compose/define_test.go
rename to testutil/compose/define_internal_test.go
index 4c84a26a4..477eb7a5c 100644
--- a/testutil/compose/define_test.go
+++ b/testutil/compose/define_internal_test.go
@@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License along with
// this program. If not, see .
-package compose_test
+package compose
import (
"bytes"
@@ -25,30 +25,19 @@ import (
"github.com/stretchr/testify/require"
"github.com/obolnetwork/charon/testutil"
- "github.com/obolnetwork/charon/testutil/compose"
)
-func TestDefineCompose(t *testing.T) {
- dir, err := os.MkdirTemp("", "")
- require.NoError(t, err)
-
- err = compose.Define(context.Background(), dir, 1, compose.NewDefaultConfig())
- require.NoError(t, err)
-
- conf, err := os.ReadFile(path.Join(dir, "config.json"))
- require.NoError(t, err)
-
- testutil.RequireGoldenBytes(t, conf)
-}
-
func TestDefineDKG(t *testing.T) {
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
- conf := compose.NewDefaultConfig()
+ conf := NewDefaultConfig()
conf.KeyGen = "dkg"
+ conf.Step = stepNew
+ p2pSeed = 1
+ require.NoError(t, writeConfig(dir, conf))
- err = compose.Define(context.Background(), dir, 1, conf)
+ err = Define(context.Background(), dir)
require.NoError(t, err)
dc, err := os.ReadFile(path.Join(dir, "docker-compose.yml"))
@@ -62,10 +51,12 @@ func TestDefineCreate(t *testing.T) {
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
- conf := compose.NewDefaultConfig()
+ conf := NewDefaultConfig()
conf.KeyGen = "create"
+ conf.Step = stepNew
+ require.NoError(t, writeConfig(dir, conf))
- err = compose.Define(context.Background(), dir, 1, conf)
+ err = Define(context.Background(), dir)
require.NoError(t, err)
dc, err := os.ReadFile(path.Join(dir, "docker-compose.yml"))
diff --git a/testutil/compose/lock.go b/testutil/compose/lock.go
index 493ca4287..c023f25fe 100644
--- a/testutil/compose/lock.go
+++ b/testutil/compose/lock.go
@@ -25,6 +25,7 @@ import (
"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
+ "github.com/obolnetwork/charon/app/z"
)
// Lock creates a docker-compose.yml from a charon-compose.yml for generating keys and a cluster lock file.
@@ -33,11 +34,11 @@ func Lock(ctx context.Context, dir string) error {
conf, err := loadConfig(dir)
if errors.Is(err, fs.ErrNotExist) {
- return errors.New("compose config not found; maybe try `compose define` first")
+ return errors.New("compose config not found; maybe try `compose new` first")
} else if err != nil {
return err
- } else if conf.Step == stepLocked {
- return errors.New("compose config already locked; maybe try `compose clean` or `compose run`")
+ } else if conf.Step != stepDefined {
+ return errors.New("compose config not defined, so can't be locked", z.Any("step", conf.Step))
}
var data tmplData
diff --git a/testutil/compose/new.go b/testutil/compose/new.go
new file mode 100644
index 000000000..7e9443266
--- /dev/null
+++ b/testutil/compose/new.go
@@ -0,0 +1,36 @@
+// Copyright © 2022 Obol Labs Inc.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see .
+
+package compose
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/obolnetwork/charon/app/log"
+ "github.com/obolnetwork/charon/app/z"
+)
+
+// New creates a new compose config file from flags.
+func New(ctx context.Context, dir string, conf Config) error {
+ conf.Step = stepNew
+
+ log.Info(ctx, "Writing config to compose dir",
+ z.Str("dir", dir),
+ z.Str("config", fmt.Sprintf("%#v", conf)),
+ )
+
+ return writeConfig(dir, conf)
+}
diff --git a/testutil/compose/new_test.go b/testutil/compose/new_test.go
new file mode 100644
index 000000000..6e639e341
--- /dev/null
+++ b/testutil/compose/new_test.go
@@ -0,0 +1,41 @@
+// Copyright © 2022 Obol Labs Inc.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see .
+
+package compose_test
+
+import (
+ "context"
+ "os"
+ "path"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/obolnetwork/charon/testutil"
+ "github.com/obolnetwork/charon/testutil/compose"
+)
+
+func TestNewDefaultConfig(t *testing.T) {
+ dir, err := os.MkdirTemp("", "")
+ require.NoError(t, err)
+
+ err = compose.New(context.Background(), dir, compose.NewDefaultConfig())
+ require.NoError(t, err)
+
+ conf, err := os.ReadFile(path.Join(dir, "config.json"))
+ require.NoError(t, err)
+
+ testutil.RequireGoldenBytes(t, conf)
+}
diff --git a/testutil/compose/run.go b/testutil/compose/run.go
index 0cb442e6c..ab36d4e86 100644
--- a/testutil/compose/run.go
+++ b/testutil/compose/run.go
@@ -21,6 +21,7 @@ import (
"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
+ "github.com/obolnetwork/charon/app/z"
)
// Run creates a docker-compose.yml from config.json to run the cluster.
@@ -29,11 +30,11 @@ func Run(ctx context.Context, dir string) error {
conf, err := loadConfig(dir)
if errors.Is(err, fs.ErrNotExist) {
- return errors.New("compose config not found; maybe try `compose define` first")
+ return errors.New("compose config not found; maybe try `compose new` first")
} else if err != nil {
return err
} else if conf.Step != stepLocked {
- return errors.New("compose config not locked yet, maybe try `compose lock` first")
+ return errors.New("compose config not locked, so can't be run", z.Any("step", conf.Step))
}
var (
diff --git a/testutil/compose/testdata/TestDefineCompose.golden b/testutil/compose/testdata/TestNewDefaultConfig.golden
similarity index 92%
rename from testutil/compose/testdata/TestDefineCompose.golden
rename to testutil/compose/testdata/TestNewDefaultConfig.golden
index ec1acea25..27f4d3c84 100644
--- a/testutil/compose/testdata/TestDefineCompose.golden
+++ b/testutil/compose/testdata/TestNewDefaultConfig.golden
@@ -1,6 +1,6 @@
{
"version": "obol/charon/compose/1.0.0",
- "step": "defined",
+ "step": "new",
"num_nodes": 4,
"threshold": 3,
"num_validators": 1,