Skip to content

Commit

Permalink
feat: change docker compose using docker api sdk (#72)
Browse files Browse the repository at this point in the history
Closes: WORLD-1173

## Overview

Changing docker compose command to docker api sdk for golang.

## Brief Changelog

- Create `Start, Stop, Restart, Purge` func in common package `docker`
- Modify all docker usage using new func

## Testing and Verifying

- Added new unit test for `docker` common package
- Some function still covered by existing unit test
- Manually tested using world cardinal start, stop, restart, and purge
<!-- ELLIPSIS_HIDDEN -->

----

| 🚀 | This description was created by [Ellipsis](https://www.ellipsis.dev) for commit e4ae56e  |
|--------|--------|

### Summary:
Replaced Docker Compose with Docker API SDK in Go, adding new management functions, updating commands, and improving testing, error handling, and service management.

**Key points**:
- Closes: WORLD-1173
- Replaced Docker Compose with Docker API SDK in Go.
- Added `Start`, `Stop`, `Restart`, `Purge` functions in `common/docker`.
- Updated `cmd/world/cardinal` commands to use new Docker functions.
- Added unit tests for `common/docker` package.
- Manual testing for start, stop, restart, and purge commands.
- Improved Docker service management and dynamic database password generation.
- Enhanced configuration handling and error handling during service startup.
- Added support for BuildKit in configuration management.
- Enhanced error handling in the lint installation script.
- Comprehensive service management for Redis, Nakama, Celestia, and EVM within Docker.
- Improved error visibility and handling in logging functionality.
- Updated command descriptions for ongoing development status.

----
Generated with ❤️ by [ellipsis.dev](https://www.ellipsis.dev)

----

<!-- ELLIPSIS_HIDDEN -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Improved Docker service management with a new `docker` package for starting, stopping, restarting, and purging services.
	- Dynamic database password generation during service startup for enhanced security.
	- Enhanced configuration handling for Docker commands, allowing for more tailored operations.
	- Added support for BuildKit in configuration management.
	- Introduced comprehensive service management for Redis, Nakama, Celestia, and EVM within the Docker environment.
	- Enhanced logging functionality with a new public `VerboseMode` variable for better management of logging levels.
	- Implemented a spinner component for terminal applications to provide real-time feedback during long-running operations.

- **Bug Fixes**
	- Enhanced error handling during service startup and configuration retrieval.
	- Improved error visibility and handling in the logging functionality.

- **Documentation**
	- Updated command descriptions to reflect ongoing development status, particularly for the login feature.

- **Tests**
	- Introduced comprehensive unit tests for Docker container operations to ensure reliability and correctness.

- **Chores**
	- Enhanced error handling in the lint installation script for better stability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
zulkhair committed Sep 23, 2024
1 parent 69e78c9 commit 6eee015
Show file tree
Hide file tree
Showing 31 changed files with 2,079 additions and 386 deletions.
14 changes: 11 additions & 3 deletions cmd/world/cardinal/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import (

"pkg.world.dev/world-cli/common"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/docker"
"pkg.world.dev/world-cli/common/docker/service"
"pkg.world.dev/world-cli/common/logger"
"pkg.world.dev/world-cli/common/teacmd"
"pkg.world.dev/world-cli/tea/style"
)

Expand Down Expand Up @@ -228,10 +229,17 @@ func startRedis(ctx context.Context, cfg *config.Config) error {
// Create an error group for managing redis lifecycle
group := new(errgroup.Group)

// Create docker client
dockerClient, err := docker.NewClient(cfg)
if err != nil {
return err

Check warning on line 235 in cmd/world/cardinal/dev.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/cardinal/dev.go#L235

Added line #L235 was not covered by tests
}
defer dockerClient.Close()

// Start Redis container
group.Go(func() error {
cfg.Detach = true
if err := teacmd.DockerStart(cfg, []teacmd.DockerService{teacmd.DockerServiceRedis}); err != nil {
if err := dockerClient.Start(ctx, cfg, service.Redis); err != nil {
return eris.Wrap(err, "Encountered an error with Redis")
}
return nil
Expand All @@ -243,7 +251,7 @@ func startRedis(ctx context.Context, cfg *config.Config) error {
// 2) The parent context is canceled for whatever reason.
group.Go(func() error {
<-ctx.Done()
if err := teacmd.DockerStop([]teacmd.DockerService{teacmd.DockerServiceRedis}); err != nil {
if err := dockerClient.Stop(cfg, service.Redis); err != nil {
return err
}
return nil
Expand Down
20 changes: 17 additions & 3 deletions cmd/world/cardinal/purge.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (

"github.com/spf13/cobra"

"pkg.world.dev/world-cli/common/teacmd"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/docker"
"pkg.world.dev/world-cli/common/docker/service"
)

/////////////////
Expand All @@ -19,8 +21,20 @@ var purgeCmd = &cobra.Command{
Short: "Stop and reset the state of your Cardinal game shard",
Long: `Stop and reset the state of your Cardinal game shard.
This command stop all Docker services and remove all Docker volumes.`,
RunE: func(_ *cobra.Command, _ []string) error {
err := teacmd.DockerPurge()
RunE: func(cmd *cobra.Command, _ []string) error {
cfg, err := config.GetConfig(cmd)
if err != nil {
return err

Check warning on line 27 in cmd/world/cardinal/purge.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/cardinal/purge.go#L27

Added line #L27 was not covered by tests
}

// Create a new Docker client
dockerClient, err := docker.NewClient(cfg)
if err != nil {
return err

Check warning on line 33 in cmd/world/cardinal/purge.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/cardinal/purge.go#L33

Added line #L33 was not covered by tests
}
defer dockerClient.Close()

err = dockerClient.Purge(cfg, service.Nakama, service.Cardinal, service.NakamaDB, service.Redis)
if err != nil {
return err
}
Expand Down
19 changes: 8 additions & 11 deletions cmd/world/cardinal/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import (
"github.com/spf13/cobra"

"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/teacmd"
"pkg.world.dev/world-cli/common/docker"
"pkg.world.dev/world-cli/common/docker/service"
)

// restartCmd restarts your Cardinal game shard stack
Expand All @@ -31,18 +32,14 @@ This will restart the following Docker services:
return err
}

if cfg.Debug {
err = teacmd.DockerRestart(cfg, []teacmd.DockerService{
teacmd.DockerServiceCardinalDebug,
teacmd.DockerServiceNakama,
})
} else {
err = teacmd.DockerRestart(cfg, []teacmd.DockerService{
teacmd.DockerServiceCardinal,
teacmd.DockerServiceNakama,
})
// Create docker client
dockerClient, err := docker.NewClient(cfg)
if err != nil {
return err

Check warning on line 38 in cmd/world/cardinal/restart.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/cardinal/restart.go#L38

Added line #L38 was not covered by tests
}
defer dockerClient.Close()

err = dockerClient.Restart(cmd.Context(), cfg, service.Cardinal, service.Nakama)
if err != nil {
return err
}
Expand Down
13 changes: 11 additions & 2 deletions cmd/world/cardinal/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (

"pkg.world.dev/world-cli/common"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/teacmd"
"pkg.world.dev/world-cli/common/docker"
"pkg.world.dev/world-cli/common/docker/service"
)

/////////////////
Expand Down Expand Up @@ -110,9 +111,17 @@ This will start the following Docker services and its dependencies:

group, ctx := errgroup.WithContext(cmd.Context())

// Create docker client
dockerClient, err := docker.NewClient(cfg)
if err != nil {
return err

Check warning on line 117 in cmd/world/cardinal/start.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/cardinal/start.go#L117

Added line #L117 was not covered by tests
}
defer dockerClient.Close()

// Start the World Engine stack
group.Go(func() error {
if err := teacmd.DockerStartAll(cfg); err != nil {
if err := dockerClient.Start(ctx, cfg, service.NakamaDB,
service.Redis, service.Cardinal, service.Nakama); err != nil {
return eris.Wrap(err, "Encountered an error with Docker")
}
return eris.Wrap(ErrGracefulExit, "Stack terminated")
Expand Down
20 changes: 17 additions & 3 deletions cmd/world/cardinal/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (

"github.com/spf13/cobra"

"pkg.world.dev/world-cli/common/teacmd"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/docker"
"pkg.world.dev/world-cli/common/docker/service"
)

/////////////////
Expand All @@ -23,8 +25,20 @@ This will stop the following Docker services:
- Cardinal (Game shard)
- Nakama (Relay) + DB
- Redis (Cardinal dependency)`,
RunE: func(_ *cobra.Command, _ []string) error {
err := teacmd.DockerStopAll()
RunE: func(cmd *cobra.Command, _ []string) error {
cfg, err := config.GetConfig(cmd)
if err != nil {
return err

Check warning on line 31 in cmd/world/cardinal/stop.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/cardinal/stop.go#L31

Added line #L31 was not covered by tests
}

// Create docker client
dockerClient, err := docker.NewClient(cfg)
if err != nil {
return err

Check warning on line 37 in cmd/world/cardinal/stop.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/cardinal/stop.go#L37

Added line #L37 was not covered by tests
}
defer dockerClient.Close()

err = dockerClient.Stop(cfg, service.Nakama, service.Cardinal, service.NakamaDB, service.Redis)
if err != nil {
return err
}
Expand Down
116 changes: 56 additions & 60 deletions cmd/world/evm/start.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
package evm

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

"github.com/rotisserie/eris"
"github.com/spf13/cobra"

"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/docker"
"pkg.world.dev/world-cli/common/docker/service"
"pkg.world.dev/world-cli/common/logger"
"pkg.world.dev/world-cli/common/teacmd"
)

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

var startCmd = &cobra.Command{
Use: "start",
Short: "Start the EVM base shard. Use --da-auth-token to pass in an auth token directly.",
Expand All @@ -32,7 +26,14 @@ var startCmd = &cobra.Command{
return err
}

if err = validateDALayer(cmd, cfg); err != nil {
// Create docker client
dockerClient, err := docker.NewClient(cfg)
if err != nil {
return err

Check warning on line 32 in cmd/world/evm/start.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/evm/start.go#L32

Added line #L32 was not covered by tests
}
defer dockerClient.Close()

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

Expand All @@ -49,10 +50,18 @@ var startCmd = &cobra.Command{
cfg.Detach = false
cfg.Timeout = 0

err = teacmd.DockerStart(cfg, services(teacmd.DockerServiceEVM))
err = dockerClient.Start(cmd.Context(), cfg, service.EVM)
if err != nil {
return fmt.Errorf("error starting %s docker container: %w", teacmd.DockerServiceEVM, err)
}

// Stop the DA service if it was started in dev mode
if cfg.DevDA {
err = dockerClient.Stop(cfg, service.CelestiaDevNet)
if err != nil {
return eris.Wrap(err, "Failed to stop DA service")

Check warning on line 62 in cmd/world/evm/start.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/evm/start.go#L62

Added line #L62 was not covered by tests
}
}
return nil
},
}
Expand All @@ -65,22 +74,18 @@ func init() {

// 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 {
func validateDevDALayer(ctx context.Context, cfg *config.Config, dockerClient *docker.Client) error {
cfg.Build = true
cfg.Debug = false
cfg.Detach = true
cfg.Timeout = -1
logger.Println("starting DA docker service for dev mode...")
if err := teacmd.DockerStart(cfg, services(daService)); err != nil {
if err := dockerClient.Start(ctx, cfg, service.CelestiaDevNet); err != nil {
return fmt.Errorf("error starting %s docker container: %w", daService, err)
}

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

daToken, err := getDAToken()
daToken, err := getDAToken(ctx, cfg, dockerClient)
if err != nil {
return err
}
Expand Down Expand Up @@ -121,58 +126,49 @@ func validateProdDALayer(cfg *config.Config) error {
return nil
}

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

func getDAToken() (string, 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++ {
logger.Println("attempting to get DA token...")

cmd := exec.Command(cmdParts[0], cmdParts[1:]...) //nolint:gosec // not applicable
output, err := cmd.CombinedOutput()
if err != nil {
logger.Println("failed to get da token")
logger.Printf("%d/%d retrying...\n", retry+1, maxRetries)
time.Sleep(2 * time.Second) //nolint:gomnd
continue
}
func getDAToken(ctx context.Context, cfg *config.Config, dockerClient *docker.Client) (string, error) {
fmt.Println("Getting DA token")

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 "", errors.New("got empty DA token")
}
return string(output), nil
containerName := service.CelestiaDevNet(cfg)

_, err := dockerClient.Exec(ctx, containerName.Name,
[]string{
"mkdir",
"-p",
"/home/celestia/bridge/keys",
})
if err != nil {
return "", eris.Wrap(err, "Failed to create keys directory")

Check warning on line 153 in cmd/world/evm/start.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/evm/start.go#L153

Added line #L153 was not covered by tests
}
return "", errors.New("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() //nolint:gosec // not applicable
if err == nil && string(output) == runningOutput {
return nil
}
time.Sleep(250 * time.Millisecond) //nolint:gomnd
token, err := dockerClient.Exec(ctx, containerName.Name,
[]string{
"celestia",
"bridge",
"--node.store",
"/home/celestia/bridge/",
"auth",
"admin",
})

if err != nil {
return "", eris.Wrapf(err, "Failed to get DA token")

Check warning on line 167 in cmd/world/evm/start.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/evm/start.go#L167

Added line #L167 was not covered by tests
}

if token == "" {
return "", errors.New("got empty DA token")

Check warning on line 171 in cmd/world/evm/start.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/evm/start.go#L171

Added line #L171 was not covered by tests
}
return fmt.Errorf("timeout while waiting for %q to start", targetContainer)
return token, nil
}
20 changes: 17 additions & 3 deletions cmd/world/evm/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,29 @@ import (

"github.com/spf13/cobra"

"pkg.world.dev/world-cli/common/teacmd"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/docker"
"pkg.world.dev/world-cli/common/docker/service"
)

var stopCmd = &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(_ *cobra.Command, _ []string) error {
err := teacmd.DockerStop(services(teacmd.DockerServiceEVM, teacmd.DockerServiceDA))
RunE: func(cmd *cobra.Command, _ []string) error {
cfg, err := config.GetConfig(cmd)
if err != nil {
return err

Check warning on line 20 in cmd/world/evm/stop.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/evm/stop.go#L17-L20

Added lines #L17 - L20 were not covered by tests
}

// Create docker client
dockerClient, err := docker.NewClient(cfg)
if err != nil {
return err

Check warning on line 26 in cmd/world/evm/stop.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/evm/stop.go#L24-L26

Added lines #L24 - L26 were not covered by tests
}
defer dockerClient.Close()

Check warning on line 28 in cmd/world/evm/stop.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/evm/stop.go#L28

Added line #L28 was not covered by tests

err = dockerClient.Stop(cfg, service.EVM, service.CelestiaDevNet)

Check warning on line 30 in cmd/world/evm/stop.go

View check run for this annotation

Codecov / codecov/patch

cmd/world/evm/stop.go#L30

Added line #L30 was not covered by tests
if err != nil {
return err
}
Expand Down
7 changes: 0 additions & 7 deletions cmd/world/evm/util.go

This file was deleted.

Loading

0 comments on commit 6eee015

Please # to comment.