Skip to content

Commit

Permalink
testutil/compose: add lock command (#570)
Browse files Browse the repository at this point in the history
Adds the first version of the lock command that creates a docker-compose file to run `charon create cluster` to generate keys and cluster lock file.

category: feature
ticket: #568
  • Loading branch information
corverroos committed May 21, 2022
1 parent f31efa6 commit ac67cd8
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 29 deletions.
22 changes: 19 additions & 3 deletions testutil/compose/compose/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
// using docker-compose.
//
// It consists of three steps:
// - compose define: Creates compose.yml (and p2pkeys) that defines a desired cluster including keygen.
// - compose define: Creates charon-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
Expand All @@ -41,17 +41,33 @@ func newRootCmd() *cobra.Command {
}

root.AddCommand(newDefineCmd())
root.AddCommand(newLockCmd())

return root
}

func newLockCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "lock",
Short: "Create a docker-compose.yml from charon-compose.yml for generating keys and a cluster lock file.",
}

dir := cmd.Flags().String("compose-dir", ".", "Directory to use for compose artifacts")

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

return cmd
}

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

dir := cmd.Flags().String("compose-dir", "", "Directory to use for compose artifacts")
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")

Expand Down
18 changes: 12 additions & 6 deletions testutil/compose/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.

//nolint:deadcode,varcheck // Busy implementing
package compose

import (
Expand All @@ -21,13 +22,18 @@ import (

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

containerBinary = "/usr/local/bin/charon"
cmdRun = "run"
cmdDKG = "dkg"
cmdCreateCluster = "[create,cluster]"
)

// vcType defines a validator client type.
Expand All @@ -44,8 +50,8 @@ type keyGen string

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

// config defines a local compose cluster; including both keygen and running a cluster.
Expand All @@ -69,8 +75,8 @@ type config struct {
Def cluster.Definition `json:"definition"`
}

// newDefaultConfig returns a new default config excluding cluster definition.
func newDefaultConfig() config {
// newBaseConfig returns a new base config excluding cluster definition.
func newBaseConfig() config {
return config{
Version: version,
ImageTag: defaultImageTag,
Expand Down
49 changes: 33 additions & 16 deletions testutil/compose/define.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package compose

import (
"context"
"crypto/ecdsa"
"encoding/json"
"fmt"
"os"
Expand Down Expand Up @@ -50,12 +51,35 @@ func Define(ctx context.Context, dir string, clean bool, seed int) error {
}
}

conf, p2pkeys := newDefaultConfig(seed)

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

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

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")
}
}

if err := writeConfig(dir, conf); err != nil {
return err
}

log.Info(ctx, "Created charon-compose.yml and node*/p2pkey")

return nil
}

func newDefaultConfig(seed int) (config, []*ecdsa.PrivateKey) {
lock, p2pkeys, _ := cluster.NewForT(&testing.T{}, defaultNumVals, defaultThreshold, defaultNumNodes, seed)
conf := newDefaultConfig()

conf := newBaseConfig()
conf.Def = lock.Definition
conf.Def.Name = "compose"
conf.Def.FeeRecipientAddress = ""
Expand All @@ -64,16 +88,16 @@ func Define(ctx context.Context, dir string, clean bool, seed int) error {
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)
return conf, p2pkeys
}

err := crypto.SaveECDSA(nodeFile(dir, i, "p2pkey"), key)
if err != nil {
return errors.Wrap(err, "save p2pkey")
}
}
// 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)
}

// writeConfig writes the config as yaml to disk.
func writeConfig(dir string, conf config) error {
b, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return errors.Wrap(err, "marshal config")
Expand All @@ -89,12 +113,5 @@ func Define(ctx context.Context, dir string, clean bool, seed int) error {
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)
}
4 changes: 1 addition & 3 deletions testutil/compose/define_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,14 @@ import (
"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"))
conf, err := os.ReadFile(path.Join(dir, "charon-compose.yml"))
require.NoError(t, err)

testutil.RequireGoldenBytes(t, conf)
Expand Down
77 changes: 77 additions & 0 deletions testutil/compose/docker-compose.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
version: "3.8"

x-node-base: &node-base
image: ghcr.io/obolnetwork/charon:{{.CharonImageTag}}
entrypoint: {{.CharonEntrypoint}}
command: {{.CharonCommand}}
networks: [compose]
volumes: [{{.ComposeDir}}:/compose]
{{if not .NodeOnly }}depends_on: [bootnode] {{end}}

services:
{{ range $i, $node := .Nodes}}
node{{$i}}:
<<: *node-base
environment:
{{- range $node.EnvVars}}
CHARON_{{.KeyUpper}}: {{.Value}}
{{- end}}
{{if .Ports}}
ports:
{{- range $node.Ports}}
- "{{.External}}:{{.Internal}}"
{{- end}}
{{- end}}
{{ end -}}

{{if not .NodeOnly }}
bootnode:
<<: *node-base
command: bootnode
depends_on: []
environment:
CHARON_BOOTNODE_HTTP_ADDRESS: 0.0.0.0:16000
CHARON_DATA_DIR: /compose/bootnode
CHARON_P2P_BOOTNODES: ""
CHARON_P2P_EXTERNAL_HOSTNAME: bootnode

{{- range $i, $vc := .VCs}}
vc{{$i}}-{{$vc.Label}}:
{{if $vc.Build}} build: {{$vc.Build}} {{end}}
{{if $vc.Image}} image: {{$vc.Image}} {{end}}
{{if $vc.Command}} command: {{$vc.Command}} {{end}}
networks: [compose]
depends_on: [node{{$i}}]
environment:
NODE: node{{$i}}
volumes:
- .:/compose
{{end}}

prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
networks: [compose]
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml

grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
networks: [compose]
volumes:
- ./grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
- ./grafana/dashboards.yml:/etc/grafana/provisioning/dashboards/datasource.yml
- ./grafana/grafana.ini:/etc/grafana/grafana.ini:ro
- ./grafana/simnet_dash.json:/etc/dashboards/simnet_dash.json

jaeger:
image: jaegertracing/all-in-one:latest
networks: [compose]
ports:
- "16686:16686"
{{end}}
networks:
compose:
93 changes: 93 additions & 0 deletions testutil/compose/lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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"
"fmt"
"os"
"path"

"github.com/goccy/go-yaml"

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

func Lock(ctx context.Context, dir string) error {
ctx = log.WithTopic(ctx, "lock")

conf, err := loadConfig(dir)
if err != nil {
return err
}

if conf.KeyGen != keyGenCreate {
return errors.New("only keygen create supported")
}

// Only single node to call charon create cluster generate keys
n := node{EnvVars: []kv{
{"threshold", fmt.Sprint(conf.Def.Threshold)},
{"nodes", fmt.Sprint(len(conf.Def.Operators))},
{"cluster_dir", "/compose"},
}}

data := tmplData{
NodeOnly: true,
ComposeDir: dir,
CharonImageTag: conf.ImageTag,
CharonEntrypoint: containerBinary,
CharonCommand: cmdCreateCluster,
Nodes: []node{n},
}

log.Info(ctx, "Created docker-compose.yml")
log.Info(ctx, "Create keys and cluster lock with: docker-compose up")

return writeDockerCompose(dir, data)
}

//nolint:deadcode // Busy implementing.
func newNodeEnvs(mockValidator bool) []kv {
return []kv{
{"jaeger_address", "jaeger:6831"},
{"definition_file", "/compose/cluster-definition.json"},
{"lock_file", "/compose/cluster-lock.json"},
{"monitoring_address", "0.0.0.0:16001"},
{"validator_api_address", "0.0.0.0:16002"},
{"p2p_tcp_address", "0.0.0.0:16003"},
{"p2p_udp_address", "0.0.0.0:16004"},
{"p2p_bootnodes", "http://bootnode:16000/enr"},
{"simnet_validator_mock", fmt.Sprint(mockValidator)},
{"log_level", "info"},
}
}

// loadConfig returns the config loaded from disk.
func loadConfig(dir string) (config, error) {
b, err := os.ReadFile(path.Join(dir, composeFile))
if err != nil {
return config{}, errors.Wrap(err, "load config")
}

var resp config
if err := yaml.Unmarshal(b, &resp); err != nil {
return config{}, errors.Wrap(err, "unmarshal config")
}

return resp, nil
}
Loading

0 comments on commit ac67cd8

Please sign in to comment.