Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(world-cli): Add commands for running evm and da layer #17

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions cmd/world/cardinal/cardinal.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package cardinal

import (
"errors"

"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/dependency"
"pkg.world.dev/world-cli/tea/style"
)

func init() {
// Register subcommands - `world cardinal [subcommand]`
BaseCmd.AddCommand(createCmd, startCmd, devCmd, restartCmd, purgeCmd, stopCmd)
BaseCmd.Flags().String("config", "", "a toml encoded config file")
}

// BaseCmd is the base command for the cardinal subcommand
Expand All @@ -38,20 +34,3 @@ var BaseCmd = &cobra.Command{
}
},
}

func getConfig(cmd *cobra.Command) (cfg config.Config, err error) {
if !cmd.Flags().Changed("config") {
// The config flag was not set. Attempt to find the config via environment variables or in the local directory
return config.LoadConfig("")
}
// The config flag was explicitly set
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return cfg, err
}
if configFile == "" {
return cfg, errors.New("config cannot be empty")
}
return config.LoadConfig(configFile)

}
3 changes: 2 additions & 1 deletion cmd/world/cardinal/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cardinal

import (
"github.com/spf13/cobra"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/tea_cmd"
)

Expand All @@ -20,7 +21,7 @@ This will restart the following Docker services:
- Cardinal (Core game logic)
- Nakama (Relay)`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := getConfig(cmd)
cfg, err := config.GetConfig(cmd)
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/world/cardinal/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/spf13/cobra"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/tea_cmd"
)

Expand Down Expand Up @@ -35,7 +36,7 @@ This will start the following Docker services and its dependencies:
- Nakama (Relay)
- Redis (Cardinal dependency)`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := getConfig(cmd)
cfg, err := config.GetConfig(cmd)
if err != nil {
return err
}
Expand Down
217 changes: 217 additions & 0 deletions cmd/world/evm/evm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package evm

import (
"bytes"
"errors"
"fmt"
"os/exec"
"strings"
"time"

"github.com/spf13/cobra"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/tea_cmd"
)

func EVMCmds() *cobra.Command {
evmRootCmd := &cobra.Command{
Use: "evm",
Short: "EVM base shard commands.",
Long: "Commands for provisioning the EVM Base Shard.",
}
evmRootCmd.AddGroup(&cobra.Group{
ID: "EVM",
Title: "EVM Base Shard Commands",
})
evmRootCmd.AddCommand(
StartEVM(),
StopAll(),
)
return evmRootCmd
}

const (
FlagUseDevDA = "dev"
FlagDAAuthToken = "da-auth-token"
EnvDAAuthToken = "DA_AUTH_TOKEN"
EnvDABaseURL = "DA_BASE_URL"
EnvDANamespaceID = "DA_NAMESPACE_ID"

daService = tea_cmd.DockerServiceDA
)

var (
// Docker compose seems to replace the hyphen with an underscore. This could be properly fixed by removing the hyphen
// from celesta-devnet, or by investigating the docker compose documentation.
daContainer = strings.ReplaceAll(string(daService), "-", "_")
)

func services(s ...tea_cmd.DockerService) []tea_cmd.DockerService {
return s
}

// validateDevDALayer starts a locally running version of the DA layer, and replaces the DA_AUTH_TOKEN configuration
// variable with the token from the locally running container.
func validateDevDALayer(cfg config.Config) error {
cfg.Build = true
cfg.Debug = false
cfg.Detach = true
cfg.Timeout = -1
fmt.Println("starting DA docker service for dev mode...")
if err := tea_cmd.DockerStart(cfg, services(daService)); err != nil {
return fmt.Errorf("error starting %s docker container: %w", daService, err)
}

if err := blockUntilContainerIsRunning(daContainer, 10*time.Second); err != nil {
return err
}
fmt.Println("started DA service...")

daToken, err := getDAToken()
if err != nil {
return err
}
envOverrides := map[string]string{
EnvDAAuthToken: daToken,
EnvDABaseURL: fmt.Sprintf("http://%s:26658", daService),
EnvDANamespaceID: "67480c4a88c4d12935d4",
}
for key, value := range envOverrides {
fmt.Printf("overriding config value %q to %q\n", key, value)
cfg.DockerEnv[key] = value
}
return nil
}

// validateProdDALayer assumes the DA layer is running somewhere else and validates the required world.toml variables are
// non-empty.
func validateProdDALayer(cfg config.Config) error {
requiredEnvVariables := []string{
EnvDAAuthToken,
EnvDABaseURL,
EnvDANamespaceID,
}
var errs []error
for _, env := range requiredEnvVariables {
if len(cfg.DockerEnv[env]) > 0 {
continue
}
errs = append(errs, fmt.Errorf("missing %q", env))
}
if len(errs) > 0 {
// Prepend an error describing the overall problem
errs = append([]error{
fmt.Errorf("the [evm] section of your config is missing some required variables"),
}, errs...)
return errors.Join(errs...)
}
return nil
}

func validateDALayer(cmd *cobra.Command, cfg config.Config) error {
devDA, err := cmd.Flags().GetBool(FlagUseDevDA)
if err != nil {
return err
}
if devDA {
return validateDevDALayer(cfg)
}
return validateProdDALayer(cfg)
}

func StartEVM() *cobra.Command {
cmd := &cobra.Command{
Use: "start",
Short: "Start the EVM base shard. Use --da-auth-token to pass in an auth token directly.",
Long: "Start the EVM base shard. Requires connection to celestia DA.",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.GetConfig(cmd)
if err != nil {
return err
}

if err = validateDALayer(cmd, cfg); err != nil {
return err
}

daToken, err := cmd.Flags().GetString(FlagDAAuthToken)
if err != nil {
return err
}
if daToken != "" {
cfg.DockerEnv[EnvDAAuthToken] = daToken
}

cfg.Build = true
cfg.Debug = false
cfg.Detach = false
cfg.Timeout = 0

err = tea_cmd.DockerStart(cfg, services(tea_cmd.DockerServiceEVM))
if err != nil {
fmt.Errorf("error starting %s docker container: %w", tea_cmd.DockerServiceEVM, err)
}
return nil
},
}
cmd.Flags().String(FlagDAAuthToken, "", "DA Auth Token that allows the rollup to communicate with the Celestia client.")
cmd.Flags().Bool(FlagUseDevDA, false, "Use a locally running DA layer")
return cmd
}

func StopAll() *cobra.Command {
cmd := &cobra.Command{
Use: "stop",
Short: "Stop the EVM base shard and DA layer client.",
Long: "Stop the EVM base shard and data availability layer client if they are running.",
RunE: func(cmd *cobra.Command, args []string) error {
return tea_cmd.DockerStop(services(tea_cmd.DockerServiceEVM, tea_cmd.DockerServiceDA))
},
}
return cmd
}

func getDAToken() (token string, err error) {
// Create a new command
maxRetries := 10
cmdString := fmt.Sprintf("docker exec %s celestia bridge --node.store /home/celestia/bridge/ auth admin", daContainer)
cmdParts := strings.Split(cmdString, " ")
for retry := 0; retry < maxRetries; retry++ {
fmt.Println("attempting to get DA token...")

cmd := exec.Command(cmdParts[0], cmdParts[1:]...)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("failed to get da token")
fmt.Printf("%d/%d retrying...\n", retry+1, maxRetries)
time.Sleep(2 * time.Second)
continue
}

if bytes.Contains(output, []byte("\n")) {
return "", fmt.Errorf("da token should be a single line. got %v", string(output))
}
if len(output) == 0 {
return "", fmt.Errorf("got empty DA token")
}
return string(output), nil
}
return "", fmt.Errorf("timed out while getting DA token")

}

func blockUntilContainerIsRunning(targetContainer string, timeout time.Duration) error {
timeoutAt := time.Now().Add(timeout)
cmdString := "docker container inspect -f '{{.State.Running}}' " + targetContainer
// This string will be returned by the above command when the container is running
runningOutput := "'true'\n"
cmdParts := strings.Split(cmdString, " ")
for time.Now().Before(timeoutAt) {
output, err := exec.Command(cmdParts[0], cmdParts[1:]...).CombinedOutput()
if err == nil && string(output) == runningOutput {
return nil
}
time.Sleep(250 * time.Millisecond)
}
return fmt.Errorf("timeout while waiting for %q to start", targetContainer)
}
9 changes: 8 additions & 1 deletion cmd/world/root/root.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package root

import (
"os"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"os"
"pkg.world.dev/world-cli/cmd/world/cardinal"
"pkg.world.dev/world-cli/cmd/world/evm"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/tea/style"
)

Expand All @@ -21,6 +24,10 @@ func init() {

// Register subcommands
rootCmd.AddCommand(cardinal.BaseCmd)

rootCmd.AddCommand(evm.EVMCmds())

config.AddConfigFlag(rootCmd)
}

// rootCmd represents the base command
Expand Down
25 changes: 24 additions & 1 deletion common/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@

"github.com/pelletier/go-toml"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

const (
WorldCLIConfigFileEnvVariable = "WORLD_CLI_CONFIG_FILE"
WorldCLIConfigFilename = "world.toml"

flagForConfigFile = "config"
)

var (
Expand All @@ -32,7 +35,27 @@
DockerEnv map[string]string
}

func LoadConfig(filename string) (Config, error) {
func AddConfigFlag(cmd *cobra.Command) {
cmd.Flags().String(flagForConfigFile, "", "a toml encoded config file")
}

func GetConfig(cmd *cobra.Command) (cfg Config, err error) {
if !cmd.Flags().Changed(flagForConfigFile) {
// The config flag was not set. Attempt to find the config via environment variables or in the local directory
return loadConfig("")
}
// The config flag was explicitly set
configFile, err := cmd.Flags().GetString(flagForConfigFile)
if err != nil {
return cfg, err
}

Check warning on line 51 in common/config/config.go

View check run for this annotation

Codecov / codecov/patch

common/config/config.go#L50-L51

Added lines #L50 - L51 were not covered by tests
if configFile == "" {
return cfg, errors.New("config cannot be empty")
}
return loadConfig(configFile)
}

func loadConfig(filename string) (Config, error) {
if filename != "" {
return loadConfigFromFile(filename)
}
Expand Down
Loading