Skip to content

Commit

Permalink
testutil/compose: implement v0 define command (#569)
Browse files Browse the repository at this point in the history
Implements the first iteration of the `compose define` command. It just uses default configuration for now and outputs the `compose.yml` file. Adding webui can be done later.

category: feature 
ticket: #568
  • Loading branch information
corverroos committed May 20, 2022
1 parent 2464f0a commit 0bfa40b
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 0 deletions.
63 changes: 63 additions & 0 deletions testutil/compose/compose/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 <http://www.gnu.org/licenses/>.

// Command compose provides a tool to run, test, debug local charon clusters
// using docker-compose.
//
// It consists of three steps:
// - compose define: Creates compose.yml (and p2pkeys) that defines a desired cluster including keygen.
// - compose lock: Creates docker-compose.yml to generates keys and cluster lock file.
// - compose run: Creates docker-compose.yml that runs the cluster.
package main

import (
"time"

"github.com/spf13/cobra"

"github.com/obolnetwork/charon/testutil/compose"
)

func main() {
cobra.CheckErr(newRootCmd().Execute())
}

func newRootCmd() *cobra.Command {
root := &cobra.Command{
Use: "compose",
Short: "Charon Compose - Run, test, and debug a developer-focussed insecure local charon cluster using docker-compose",
}

root.AddCommand(newDefineCmd())

return root
}

func newDefineCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "define",
Short: "Define a cluster; including both keygen and running definitions",
}

dir := cmd.Flags().String("compose-dir", "", "Directory to use for compose artifacts")
clean := cmd.Flags().Bool("clean", true, "Clean compose dir before defining a new cluster")
seed := cmd.Flags().Int("seed", int(time.Now().UnixNano()), "Randomness seed")

cmd.RunE = func(cmd *cobra.Command, _ []string) error {
return compose.Define(cmd.Context(), *dir, *clean, *seed)
}

return cmd
}
81 changes: 81 additions & 0 deletions testutil/compose/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 <http://www.gnu.org/licenses/>.

package compose

import (
"github.com/obolnetwork/charon/cluster"
)

const (
version = "obol/charon/compose/1.0.0"
composeFile = "compose.yml"
defaultImageTag = "latest"
defaultBeaconNode = "mock"
defaultKeyGen = keyGenDKG
defaultNumVals = 1
defaultNumNodes = 4
defaultThreshold = 3
)

// vcType defines a validator client type.
type vcType string

const (
vcMock vcType = "mock"
vcTeku vcType = "teku"
vcLighthouse vcType = "lighthouse"
)

// keyGen defines a key generation process.
type keyGen string

const (
keyGenDKG keyGen = "dkg"
keyGenCreate keyGen = "create" //nolint:deadcode,varcheck
keyGenSplit keyGen = "split" //nolint:deadcode,varcheck
)

// config defines a local compose cluster; including both keygen and running a cluster.
type config struct {
// Version defines the compose config version.
Version string `json:"version"`

// ImageTag defines the charon docker image tag: ghcr.io/obolnetwork/charon:{ImageTag}.
ImageTag string `json:"image_tag"`

// VCs define the types of validator clients to use.
VCs []vcType `json:"validator_clients"`

// keyGen defines the key generation process.
KeyGen keyGen `json:"key_gen"`

// BeaconNode url endpoint or "mock" for simnet.
BeaconNode string `json:"beacon_node"`

// Def is the cluster definition.
Def cluster.Definition `json:"definition"`
}

// newDefaultConfig returns a new default config excluding cluster definition.
func newDefaultConfig() config {
return config{
Version: version,
ImageTag: defaultImageTag,
VCs: []vcType{vcTeku, vcLighthouse, vcMock},
KeyGen: defaultKeyGen,
BeaconNode: defaultBeaconNode,
}
}
100 changes: 100 additions & 0 deletions testutil/compose/define.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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 <http://www.gnu.org/licenses/>.

package compose

import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"testing"

"github.com/ethereum/go-ethereum/crypto"
"github.com/goccy/go-yaml"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/cluster"
)

// Define defines a compose cluster; including both keygen and running definitions.
func Define(ctx context.Context, dir string, clean bool, seed int) error {
ctx = log.WithTopic(ctx, "define")

if clean {
files, err := filepath.Glob(path.Join(dir, "*"))
if err != nil {
return errors.Wrap(err, "glob dir")
}
log.Info(ctx, "Cleaning compose dir", z.Int("files", len(files)))
for _, file := range files {
if err := os.RemoveAll(file); err != nil {
return errors.Wrap(err, "remove file")
}
}
}

// TODO(corver): Serve a web UI to allow configuration of default values.

log.Info(ctx, "Using default config")

lock, p2pkeys, _ := cluster.NewForT(&testing.T{}, defaultNumVals, defaultThreshold, defaultNumNodes, seed)
conf := newDefaultConfig()
conf.Def = lock.Definition
conf.Def.Name = "compose"
conf.Def.FeeRecipientAddress = ""
conf.Def.WithdrawalAddress = ""
for i := 0; i < len(conf.Def.Operators); i++ {
conf.Def.Operators[i].Address = ""
}

for i, key := range p2pkeys {
// Best effort creation of folder, rather fail when saving p2pkey file next.
_ = os.MkdirAll(nodeFile(dir, i, ""), 0o755)

err := crypto.SaveECDSA(nodeFile(dir, i, "p2pkey"), key)
if err != nil {
return errors.Wrap(err, "save p2pkey")
}
}

b, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return errors.Wrap(err, "marshal config")
}

b, err = yaml.JSONToYAML(b)
if err != nil {
return errors.Wrap(err, "yaml config")
}

err = os.WriteFile(path.Join(dir, composeFile), b, 0o755) //nolint:gosec
if err != nil {
return errors.Wrap(err, "write config")
}

log.Info(ctx, "Created config.yml and p2pkeys")

return nil
}

// nodeFile returns the path to a file in a node folder.
func nodeFile(dir string, i int, file string) string {
return path.Join(dir, fmt.Sprintf("node%d", i), file)
}
43 changes: 43 additions & 0 deletions testutil/compose/define_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 <http://www.gnu.org/licenses/>.

package compose_test

import (
"context"
"os"
"path"
"testing"

"github.com/stretchr/testify/require"

"github.com/obolnetwork/charon/testutil"
"github.com/obolnetwork/charon/testutil/compose"
)

//go:generate go test . -update -clean

func TestDefine(t *testing.T) {
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)

err = compose.Define(context.Background(), dir, false, 1)
require.NoError(t, err)

conf, err := os.ReadFile(path.Join(dir, "compose.yml"))
require.NoError(t, err)

testutil.RequireGoldenBytes(t, conf)
}
35 changes: 35 additions & 0 deletions testutil/compose/testdata/TestDefine.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: obol/charon/compose/1.0.0
image_tag: latest
validator_clients:
- teku
- lighthouse
- mock
key_gen: dkg
beacon_node: mock
definition:
name: compose
operators:
- address: ""
enr: enr:-Ie4QO_2Hr_cSEDspVO3eY2kE6GObcL-F7ouWhwH8PIalvjrZqwTnGWRampMnWFQrE4-LE5EtVdiVn40fq8zdkacQLiAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQI9dJg9StMlwLpNijInaqLqMzVJKmBt7S2NOKZC-uPj5oN0Y3ABg3VkcAI=
nonce: 0
enr_signature: null
- address: ""
enr: enr:-Ie4QND0b1o5R2-iUhEcRkip-QY8l-xF67L1IC4sn3oRJEpYQYYNjDKCEEwUqBE9XwbUjNsI3rh_IoaFHYtf9UGDoKaAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQLOVPSAl78GlDurrLEzUEw-z669q19TmiWQ5wlmjJEoI4N0Y3ADg3VkcAQ=
nonce: 0
enr_signature: null
- address: ""
enr: enr:-Ie4QD3pENsSBFa2mNy9lntGWbyaB3PZqxQg6qfcRsibv2SGVZaUSIM0nH7Ger2Y5u4M5iITz48WUdfN4w8NGkTItSmAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMIF0e-yqxgr5e_leu0Qql06t0gv1WRDg4S5Iaxe_loSIN0Y3AFg3VkcAY=
nonce: 0
enr_signature: null
- address: ""
enr: enr:-Ie4QChc9X-TUNf5Teo1M0ar00cYhSGcbR5ddMxW8z7Tnah4CbDUcjkdgp4JiB3n60UONJ4KNOXuo9StqebRql9yAhaAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMwtBj_k3MB-Yjgz6Qexa5dPzPbVTD-39k3MhHXnQhc5YN0Y3AHg3VkcAg=
nonce: 0
enr_signature: null
uuid: 3BEA6F5B-3AF6-DE03-7436-6C4719E43A1B
version: v1.0.0
num_validators: 1
threshold: 3
dkg_algorithm: default
fork_version: "0x0000000"
definition_hash: kHuiCBpi3oeTEMUQIf4lubbyH7d3BBcCsGjFuh4kaJM=
operator_signatures: null

0 comments on commit 0bfa40b

Please sign in to comment.