From f131fe1c451db33e18e0eb05e7db9230d920acd8 Mon Sep 17 00:00:00 2001 From: zulkhair Date: Tue, 6 Aug 2024 02:04:03 +0700 Subject: [PATCH] change docker compose using docker api sdk --- cmd/world/cardinal/dev.go | 19 +- cmd/world/cardinal/purge.go | 24 ++- cmd/world/cardinal/restart.go | 27 +-- cmd/world/cardinal/start.go | 23 ++- cmd/world/cardinal/stop.go | 26 ++- cmd/world/root/login.go | 2 +- cmd/world/root/root_test.go | 2 +- common/config/config.go | 15 +- common/docker/client.go | 138 +++++++++++++ common/docker/client_container.go | 150 ++++++++++++++ common/docker/client_image.go | 174 ++++++++++++++++ common/docker/client_network.go | 64 ++++++ common/docker/client_test.go | 233 ++++++++++++++++++++++ common/docker/client_util.go | 53 +++++ common/docker/client_volume.go | 66 ++++++ common/docker/service/cardinal.Dockerfile | 61 ++++++ common/docker/service/cardinal.go | 94 +++++++++ common/docker/service/celestia.go | 46 +++++ common/docker/service/evm.go | 79 ++++++++ common/docker/service/nakama.go | 76 +++++++ common/docker/service/nakamadb.go | 51 +++++ common/docker/service/redis.go | 43 ++++ common/docker/service/service.go | 28 +++ common/logger/init.go | 6 +- common/logger/logger.go | 7 +- go.mod | 25 ++- go.sum | 111 ++++++++++- makefiles/lint.mk | 4 +- 28 files changed, 1602 insertions(+), 45 deletions(-) create mode 100644 common/docker/client.go create mode 100644 common/docker/client_container.go create mode 100644 common/docker/client_image.go create mode 100644 common/docker/client_network.go create mode 100644 common/docker/client_test.go create mode 100644 common/docker/client_util.go create mode 100644 common/docker/client_volume.go create mode 100644 common/docker/service/cardinal.Dockerfile create mode 100644 common/docker/service/cardinal.go create mode 100644 common/docker/service/celestia.go create mode 100644 common/docker/service/evm.go create mode 100644 common/docker/service/nakama.go create mode 100644 common/docker/service/nakamadb.go create mode 100644 common/docker/service/redis.go create mode 100644 common/docker/service/service.go diff --git a/cmd/world/cardinal/dev.go b/cmd/world/cardinal/dev.go index 1e04da6..2266f82 100644 --- a/cmd/world/cardinal/dev.go +++ b/cmd/world/cardinal/dev.go @@ -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" ) @@ -228,10 +229,22 @@ func startRedis(ctx context.Context, cfg *config.Config) error { // Create an error group for managing redis lifecycle group := new(errgroup.Group) + // Create docker client + docker, err := docker.NewClient(cfg) + if err != nil { + return err + } + defer func() { + err := docker.Close() + if err != nil { + logger.Error("Failed to close docker client", err) + } + }() + // Start Redis container group.Go(func() error { cfg.Detach = true - if err := teacmd.DockerStart(cfg, []teacmd.DockerService{teacmd.DockerServiceRedis}); err != nil { + if err := docker.Start(ctx, cfg, service.Redis); err != nil { return eris.Wrap(err, "Encountered an error with Redis") } return nil @@ -243,7 +256,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 := docker.Stop(cfg, service.Redis); err != nil { return err } return nil diff --git a/cmd/world/cardinal/purge.go b/cmd/world/cardinal/purge.go index 46e2daf..e0092fe 100644 --- a/cmd/world/cardinal/purge.go +++ b/cmd/world/cardinal/purge.go @@ -5,7 +5,8 @@ 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" ) ///////////////// @@ -19,8 +20,25 @@ 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 + } + + // Create a new Docker client + docker, err := docker.NewClient(cfg) + if err != nil { + return err + } + defer func() { + err = docker.Close() + if err != nil { + fmt.Println("Failed to close docker client") + } + }() + + err = docker.Purge(cfg) if err != nil { return err } diff --git a/cmd/world/cardinal/restart.go b/cmd/world/cardinal/restart.go index 84488c5..e02373d 100644 --- a/cmd/world/cardinal/restart.go +++ b/cmd/world/cardinal/restart.go @@ -4,7 +4,9 @@ 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" + "pkg.world.dev/world-cli/common/logger" ) // restartCmd restarts your Cardinal game shard stack @@ -31,18 +33,19 @@ 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 + docker, err := docker.NewClient(cfg) + if err != nil { + return err } - + defer func() { + err := docker.Close() + if err != nil { + logger.Error("Failed to close docker client", err) + } + }() + + err = docker.Restart(cmd.Context(), cfg, service.Cardinal, service.Nakama) if err != nil { return err } diff --git a/cmd/world/cardinal/start.go b/cmd/world/cardinal/start.go index 8ed5085..c8ea171 100644 --- a/cmd/world/cardinal/start.go +++ b/cmd/world/cardinal/start.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/google/uuid" "github.com/rotisserie/eris" "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -11,7 +12,9 @@ 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" + "pkg.world.dev/world-cli/common/logger" ) ///////////////// @@ -108,11 +111,27 @@ This will start the following Docker services and its dependencies: fmt.Println("This may take a few minutes to rebuild the Docker images.") fmt.Println("Use `world cardinal dev` to run Cardinal faster/easier in development mode.") + // Print CockroachDB password + cfg.DockerEnv["DB_PASSWORD"] = uuid.New().String() + printServiceAddress("DB Password", cfg.DockerEnv["DB_PASSWORD"]) + group, ctx := errgroup.WithContext(cmd.Context()) + // Create docker client + docker, err := docker.NewClient(cfg) + if err != nil { + return err + } + defer func() { + err := docker.Close() + if err != nil { + logger.Error("Failed to close docker client", err) + } + }() + // Start the World Engine stack group.Go(func() error { - if err := teacmd.DockerStartAll(cfg); err != nil { + if err := docker.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") diff --git a/cmd/world/cardinal/stop.go b/cmd/world/cardinal/stop.go index c10a96d..503e395 100644 --- a/cmd/world/cardinal/stop.go +++ b/cmd/world/cardinal/stop.go @@ -5,7 +5,10 @@ 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" + "pkg.world.dev/world-cli/common/logger" ) ///////////////// @@ -23,8 +26,25 @@ 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 + } + + // Create docker client + docker, err := docker.NewClient(cfg) + if err != nil { + return err + } + defer func() { + err := docker.Close() + if err != nil { + logger.Error("Failed to close docker client", err) + } + }() + + err = docker.Stop(cfg, service.Nakama, service.Cardinal, service.NakamaDB, service.Redis) if err != nil { return err } diff --git a/cmd/world/root/login.go b/cmd/world/root/login.go index 2125080..63809f9 100644 --- a/cmd/world/root/login.go +++ b/cmd/world/root/login.go @@ -37,7 +37,7 @@ var ( func getLoginCmd() *cobra.Command { loginCmd := &cobra.Command{ Use: "login", - Short: "Authenticate using an access token", + Short: "Authenticate using an access token (under construction)", RunE: func(cmd *cobra.Command, _ []string) error { err := loginOnBrowser(cmd.Context()) if err != nil { diff --git a/cmd/world/root/root_test.go b/cmd/world/root/root_test.go index 958f9f9..a0e9a0b 100644 --- a/cmd/world/root/root_test.go +++ b/cmd/world/root/root_test.go @@ -132,7 +132,7 @@ func TestCreateStartStopRestartPurge(t *testing.T) { assert.NilError(t, err) // Start cardinal - rootCmd.SetArgs([]string{"cardinal", "start", "--build", "--detach", "--editor=false"}) + rootCmd.SetArgs([]string{"cardinal", "start", "--detach", "--editor=false"}) err = rootCmd.Execute() assert.NilError(t, err) diff --git a/common/config/config.go b/common/config/config.go index c3dfb4a..66ca129 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -28,13 +28,14 @@ var ( ) type Config struct { - RootDir string - GameDir string - Detach bool - Build bool - Debug bool - Timeout int - DockerEnv map[string]string + RootDir string + GameDir string + Detach bool + Build bool + Debug bool + BuildkitSupport bool + Timeout int + DockerEnv map[string]string } func AddConfigFlag(cmd *cobra.Command) { diff --git a/common/docker/client.go b/common/docker/client.go new file mode 100644 index 0000000..274f02f --- /dev/null +++ b/common/docker/client.go @@ -0,0 +1,138 @@ +package docker + +import ( + "context" + + "github.com/docker/docker/client" + "github.com/rotisserie/eris" + + "pkg.world.dev/world-cli/common/config" + "pkg.world.dev/world-cli/common/docker/service" + "pkg.world.dev/world-cli/common/logger" +) + +type Client struct { + cli *client.Client + cfg *config.Config +} + +func NewClient(cfg *config.Config) (*Client, error) { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, eris.Wrap(err, "Failed to create docker client") + } + + cfg.BuildkitSupport = checkBuildKitSupport(cli) + + return &Client{ + cli: cli, + cfg: cfg, + }, nil +} + +func (c *Client) Close() error { + return c.cli.Close() +} + +func (c *Client) Start(ctx context.Context, cfg *config.Config, //nolint:gocognit + services ...service.GetDockerService) error { + defer func() { + if !cfg.Detach { + err := c.Stop(cfg, services...) + if err != nil { + logger.Error("Failed to stop containers", err) + } + } + }() + + namespace := cfg.DockerEnv["CARDINAL_NAMESPACE"] + err := c.createNetworkIfNotExists(namespace) + if err != nil { + return eris.Wrap(err, "Failed to create network") + } + + err = c.createVolumeIfNotExists(namespace) + if err != nil { + return eris.Wrap(err, "Failed to create volume") + } + + // var for storing container names + containers := make([]string, 0) + + // iterate over configurations and create containers + for _, getService := range services { + service := getService(cfg) + if cfg.Build && service.Dockerfile.Script != "" { + for _, preImage := range service.Dockerfile.PrerequisiteImages { + if err := c.pullImageIfNotExists(ctx, preImage); err != nil { + return eris.Wrap(err, "Failed to pull prerequisite image") + } + } + + if err := c.buildImage(ctx, service.Dockerfile, service.Image); err != nil { + return eris.Wrap(err, "Failed to build image") + } + } else { + if err := c.pullImageIfNotExists(ctx, service); err != nil { + return eris.Wrap(err, "Failed to pull image") + } + } + + if err := c.createContainer(ctx, service); err != nil { + return eris.Wrap(err, "Failed to create container") + } + + containers = append(containers, service.Name) + } + + // log containers if not detached + if !cfg.Detach { + c.logContainers(ctx, containers) + } + + return nil +} + +func (c *Client) Stop(cfg *config.Config, services ...service.GetDockerService) error { + ctx := context.Background() + for _, getService := range services { + configuration := getService(cfg) + if err := c.stopAndRemoveContainer(ctx, configuration.Name); err != nil { + return eris.Wrap(err, "Failed to stop container") + } + } + + return nil +} + +func (c *Client) Purge(cfg *config.Config) error { + ctx := context.Background() + err := c.Stop(cfg, service.Nakama, service.NakamaDB, + service.Cardinal, service.Redis, service.CelestiaDevNet, service.EVM) + if err != nil { + return err + } + + err = c.removeVolume(ctx, cfg.DockerEnv["CARDINAL_NAMESPACE"]) + if err != nil { + return err + } + + err = c.removeNetwork(ctx, cfg.DockerEnv["CARDINAL_NAMESPACE"]) + if err != nil { + return err + } + + return nil +} + +func (c *Client) Restart(ctx context.Context, cfg *config.Config, + services ...service.GetDockerService) error { + // stop containers + err := c.Stop(cfg, services...) + if err != nil { + return err + } + + return c.Start(ctx, cfg, services...) +} diff --git a/common/docker/client_container.go b/common/docker/client_container.go new file mode 100644 index 0000000..06b976d --- /dev/null +++ b/common/docker/client_container.go @@ -0,0 +1,150 @@ +package docker + +import ( + "archive/tar" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/rotisserie/eris" + + "pkg.world.dev/world-cli/common/docker/service" + "pkg.world.dev/world-cli/common/logger" + "pkg.world.dev/world-cli/tea/style" +) + +func (c *Client) createContainer(ctx context.Context, configuration service.Service) error { + resp, err := c.cli.ContainerCreate(ctx, &configuration.Config, &configuration.HostConfig, + &configuration.NetworkingConfig, &configuration.Platform, configuration.Name) + if err != nil { + return err + } + + if err := c.cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + return err + } + + return nil +} + +func (c *Client) stopAndRemoveContainer(ctx context.Context, containerName string) error { + text := contextPrint("Removing", "1", "container", "4", containerName) + fmt.Print(text) + + // Check if the container exists + _, err := c.cli.ContainerInspect(ctx, containerName) + if err != nil { + if client.IsErrNotFound(err) { + fmt.Printf("\r%s %s\n", text, style.TickIcon.Render()) + return nil // or return an error if you prefer + } + return eris.Wrapf(err, "Failed to inspect container %s", containerName) + } + + // Stop the container + err = c.cli.ContainerStop(ctx, containerName, container.StopOptions{}) + if err != nil { + logger.Println("Failed to stop container", err) + return eris.Wrapf(err, "Failed to stop container %s", containerName) + } + + // Remove the container + err = c.cli.ContainerRemove(ctx, containerName, container.RemoveOptions{}) + if err != nil { + return eris.Wrapf(err, "Failed to remove container %s", containerName) + } + + fmt.Printf("\r%s %s\n", text, style.TickIcon.Render()) + return nil +} + +func (c *Client) logContainers(ctx context.Context, containerNames []string) { + logs := make(map[string]io.ReadCloser) + for _, containerName := range containerNames { + out, err := c.cli.ContainerLogs(context.Background(), containerName, container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + }) + if err != nil { + panic(err) + } + logs[containerName] = out + defer out.Close() + } + + nameStyle := 1 + for name, log := range logs { + nameStyle++ + go func(name string, log io.ReadCloser, nameStyle int) { + buf := make([]byte, 4096) //nolint:gomnd + for { + n, err := log.Read(buf) + if n > 0 { + fmt.Printf("[%s] %s", foregroundPrint(name, strconv.Itoa(nameStyle)), buf[:n]) + } + if err != nil { + break + } + } + }(name, log, nameStyle) + } + + <-ctx.Done() +} + +// AddFileToTarWriter adds a file or directory to the tar writer +func (c *Client) addFileToTarWriter(baseDir string, tw *tar.Writer) error { + return filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Check if the file is world.toml or inside the cardinal directory + relPath, err := filepath.Rel(baseDir, path) + if err != nil { + return err + } + if !(info.Name() == "world.toml" || strings.HasPrefix(filepath.ToSlash(relPath), "cardinal/")) { + return nil + } + + // Create a tar header for the file or directory + header, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + + // Adjust the header name to be relative to the baseDir + header.Name = filepath.ToSlash(relPath) + + // Write the header to the tar writer + if err := tw.WriteHeader(header); err != nil { + return err + } + + // If it's a directory, there's no need to write file content + if info.IsDir() { + return nil + } + + // Write the file content to the tar writer + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + if _, err := io.Copy(tw, file); err != nil { + return err + } + + return nil + }) +} diff --git a/common/docker/client_image.go b/common/docker/client_image.go new file mode 100644 index 0000000..5f0fa08 --- /dev/null +++ b/common/docker/client_image.go @@ -0,0 +1,174 @@ +package docker + +import ( + "archive/tar" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + + "pkg.world.dev/world-cli/common/docker/service" + "pkg.world.dev/world-cli/common/logger" +) + +func (c *Client) pullImageIfNotExists(ctx context.Context, configuration service.Service) error { + fmt.Println(contextPrint("Pulling", "2", "image", "4", configuration.Image)) + + _, _, err := c.cli.ImageInspectWithRaw(ctx, configuration.Image) + + // If image exists, return + if err == nil { + logger.Println("Image already exists", configuration.Image) + return nil + } + + // If image does not exist, pull it + if client.IsErrNotFound(err) { + pullOptions := image.PullOptions{} + + // Set platform if specified + if configuration.Platform.Architecture != "" && configuration.Platform.OS != "" { + pullOptions.Platform = fmt.Sprintf("%s/%s", configuration.Platform.OS, configuration.Platform.Architecture) + } + + out, err := c.cli.ImagePull(ctx, configuration.Image, pullOptions) + if err != nil { + return err + } + defer out.Close() + + return c.filterDockerPullOutput(out) + } + + return err +} + +func (c *Client) buildImage(ctx context.Context, dockerfile service.Dockerfile, imageName string) error { + fmt.Println(contextPrint("Building", "2", "image", "4", imageName)) + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + defer tw.Close() + + // Add the Dockerfile to the tar archive + header := &tar.Header{ + Name: "Dockerfile", + Size: int64(len(dockerfile.Script)), + } + if err := tw.WriteHeader(header); err != nil { + return err + } + if _, err := tw.Write([]byte(dockerfile.Script)); err != nil { + return err + } + + // Add source code to the tar archive + if err := c.addFileToTarWriter(".", tw); err != nil { + return err + } + + // Read the tar archive + tarReader := bytes.NewReader(buf.Bytes()) + + buildOptions := types.ImageBuildOptions{ + Dockerfile: "Dockerfile", + Tags: []string{imageName}, + Target: dockerfile.Target, + } + + if c.cfg.BuildkitSupport { + buildOptions.Version = types.BuilderBuildKit + } + + // Build the image + buildResponse, err := c.cli.ImageBuild(ctx, tarReader, buildOptions) + if err != nil { + return err + } + defer buildResponse.Body.Close() + + // Print the build logs + err = c.filterDockerBuildOutput(buildResponse.Body) + if err != nil { + return err + } + + return nil +} + +func (c *Client) filterDockerBuildOutput(reader io.Reader) error { + decoder := json.NewDecoder(reader) + for { + var event map[string]interface{} + if err := decoder.Decode(&event); errors.Is(err, io.EOF) { + break + } else if err != nil { + return err + } + + // Filter the content + // Only print the stream and error fields + if val, ok := event["Stream"]; ok && val != "" { + fmt.Println(val) + } else if val, ok = event["error"]; ok && val != "" { + fmt.Println(contextPrint("Error:", "1", "", "1", val.(string))) + } + // else if val, ok = event["aux"]; ok { + // // Do nothing if build are using buildkit + // } + } + return nil +} + +func (c *Client) filterDockerPullOutput(reader io.Reader) error { //nolint:gocognit + decoder := json.NewDecoder(reader) + var statusTemp interface{} + for { + var event map[string]interface{} + if err := decoder.Decode(&event); errors.Is(err, io.EOF) { + fmt.Println() + break + } else if err != nil { + return err + } + + // Check for errorDetail and error fields + if errorDetail, ok := event["errorDetail"]; ok { + if errorMessage, ok := errorDetail.(map[string]interface{})["message"]; ok { + fmt.Printf("\r%s %s\n", foregroundPrint("Error:", "1"), errorMessage) + break + } + } else if errorMsg, ok := event["error"]; ok { + fmt.Printf("\r%s %s\n", foregroundPrint("Error:", "1"), errorMsg) + break + } + + // Filter and print relevant information + if status, ok := event["status"]; ok { + output, ok := status.(string) + if !ok { + logger.Errorf("Failed to cast status to string %v", status) + continue + } + output = foregroundPrint(output, "4") + if progress, ok := event["progress"]; ok { + output += " " + progress.(string) + } + if statusTemp == status { + fmt.Printf("\r%s", output) + } else { + fmt.Printf("\n%s", output) + } + statusTemp = status + + os.Stdout.Sync() + } + } + return nil +} diff --git a/common/docker/client_network.go b/common/docker/client_network.go new file mode 100644 index 0000000..f90c061 --- /dev/null +++ b/common/docker/client_network.go @@ -0,0 +1,64 @@ +package docker + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types/network" + + "pkg.world.dev/world-cli/common/logger" + "pkg.world.dev/world-cli/tea/style" +) + +func (c *Client) createNetworkIfNotExists(networkName string) error { + ctx := context.Background() + networks, err := c.cli.NetworkList(ctx, network.ListOptions{}) + if err != nil { + return err + } + + for _, network := range networks { + if network.Name == networkName { + logger.Infof("Network %s already exists", networkName) + return nil + } + } + + _, err = c.cli.NetworkCreate(ctx, networkName, network.CreateOptions{ + Driver: "bridge", + }) + if err != nil { + return err + } + + return nil +} + +func (c *Client) removeNetwork(ctx context.Context, networkName string) error { + text := contextPrint("Removing", "1", "network", "4", networkName) + fmt.Print(text) + + networks, err := c.cli.NetworkList(ctx, network.ListOptions{}) + if err != nil { + return err + } + + networkExist := false + for _, network := range networks { + if network.Name == networkName { + networkExist = true + break + } + } + + if networkExist { + err = c.cli.NetworkRemove(ctx, networkName) + if err != nil { + return err + } + } + + fmt.Printf("\r%s %s\n", text, style.TickIcon.Render()) + + return nil +} diff --git a/common/docker/client_test.go b/common/docker/client_test.go new file mode 100644 index 0000000..e9a029e --- /dev/null +++ b/common/docker/client_test.go @@ -0,0 +1,233 @@ +package docker + +import ( + "context" + "net" + "os" + "testing" + "time" + + "gotest.tools/v3/assert" + + "pkg.world.dev/world-cli/common/config" + "pkg.world.dev/world-cli/common/docker/service" + "pkg.world.dev/world-cli/common/logger" + "pkg.world.dev/world-cli/common/teacmd" +) + +const ( + redisPort = "56379" + redisPassword = "password" + cardinalNamespace = "test" +) + +var ( + dockerClient *Client +) + +func TestMain(m *testing.M) { + // Purge any existing containers + cfg := &config.Config{ + DockerEnv: map[string]string{ + "CARDINAL_NAMESPACE": cardinalNamespace, + }, + } + + c, err := NewClient(cfg) + if err != nil { + logger.Errorf("Failed to create docker client: %v", err) + os.Exit(1) + } + + dockerClient = c + + err = dockerClient.Purge(cfg) + if err != nil { + logger.Errorf("Failed to purge containers: %v", err) + os.Exit(1) + } + + // Run the tests + code := m.Run() + + err = dockerClient.Close() + if err != nil { + logger.Errorf("Failed to close docker client: %v", err) + os.Exit(1) + } + + os.Exit(code) +} + +func TestStart(t *testing.T) { + cfg := &config.Config{ + DockerEnv: map[string]string{ + "CARDINAL_NAMESPACE": cardinalNamespace, + "REDIS_PASSWORD": redisPassword, + "REDIS_PORT": redisPort, + }, + Detach: true, + } + + ctx := context.Background() + assert.NilError(t, dockerClient.Start(ctx, cfg, service.Redis), "failed to start container") + cleanUp(t, cfg) + + // Test if the container is running + assert.Assert(t, redislIsUp(t)) +} + +func TestStop(t *testing.T) { + cfg := &config.Config{ + DockerEnv: map[string]string{ + "CARDINAL_NAMESPACE": cardinalNamespace, + "REDIS_PASSWORD": redisPassword, + "REDIS_PORT": redisPort, + }, + Detach: true, + } + + ctx := context.Background() + assert.NilError(t, dockerClient.Start(ctx, cfg, service.Redis), "failed to start container") + cleanUp(t, cfg) + + assert.NilError(t, dockerClient.Stop(cfg, service.Redis), "failed to stop container") + + // Test if the container is stopped + assert.Assert(t, redisIsDown(t)) +} + +func TestRestart(t *testing.T) { + cfg := &config.Config{ + DockerEnv: map[string]string{ + "CARDINAL_NAMESPACE": cardinalNamespace, + "REDIS_PASSWORD": redisPassword, + "REDIS_PORT": redisPort, + }, + Detach: true, + } + + ctx := context.Background() + assert.NilError(t, dockerClient.Start(ctx, cfg, service.Redis), "failed to start container") + cleanUp(t, cfg) + + assert.NilError(t, dockerClient.Restart(ctx, cfg, service.Redis), "failed to restart container") + + // Test if the container is running + assert.Assert(t, redislIsUp(t)) +} + +func TestPurge(t *testing.T) { + cfg := &config.Config{ + DockerEnv: map[string]string{ + "CARDINAL_NAMESPACE": cardinalNamespace, + "REDIS_PASSWORD": redisPassword, + "REDIS_PORT": redisPort, + }, + Detach: true, + } + + ctx := context.Background() + assert.NilError(t, dockerClient.Start(ctx, cfg, service.Redis), "failed to start container") + assert.NilError(t, dockerClient.Purge(cfg), "failed to purge container") + + // Test if the container is stopped + assert.Assert(t, redisIsDown(t)) +} + +func TestStartUndetach(t *testing.T) { + cfg := &config.Config{ + DockerEnv: map[string]string{ + "CARDINAL_NAMESPACE": cardinalNamespace, + "REDIS_PASSWORD": redisPassword, + "REDIS_PORT": redisPort, + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + assert.NilError(t, dockerClient.Start(ctx, cfg, service.Redis), "failed to start container") + cleanUp(t, cfg) + }() + assert.Assert(t, redislIsUp(t)) + + cancel() + assert.Assert(t, redisIsDown(t)) +} + +func TestBuild(t *testing.T) { + // Create a temporary directory + dir, err := os.MkdirTemp("", "sgt") + assert.NilError(t, err) + + // Remove dir + defer func() { + err = os.RemoveAll(dir) + assert.NilError(t, err) + }() + + // Change to the temporary directory + err = os.Chdir(dir) + assert.NilError(t, err) + + // Pull the repository + templateGitURL := "https://github.com/Argus-Labs/starter-game-template.git" + err = teacmd.GitCloneCmd(templateGitURL, dir, "Initial commit from World CLI") + assert.NilError(t, err) + + // Preparation + cfg := &config.Config{ + DockerEnv: map[string]string{ + "CARDINAL_NAMESPACE": cardinalNamespace, + }, + } + cardinalService := service.Cardinal(cfg) + ctx := context.Background() + + // Pull prerequisite images + for _, image := range cardinalService.Dockerfile.PrerequisiteImages { + assert.NilError(t, dockerClient.pullImageIfNotExists(ctx, image)) + } + + // Build the image + err = dockerClient.buildImage(ctx, cardinalService.Dockerfile, cardinalService.Image) + assert.NilError(t, err, "Failed to build Docker image") +} + +func redislIsUp(t *testing.T) bool { + up := false + for i := 0; i < 60; i++ { + conn, err := net.DialTimeout("tcp", "localhost:"+redisPort, time.Second) + if err != nil { + time.Sleep(time.Second) + t.Logf("Failed to connect to Redis at localhost:%s, retrying...", redisPort) + continue + } + _ = conn.Close() + up = true + break + } + return up +} + +func redisIsDown(t *testing.T) bool { + down := false + for i := 0; i < 60; i++ { + conn, err := net.DialTimeout("tcp", "localhost:"+redisPort, time.Second) + if err != nil { + down = true + break + } + _ = conn.Close() + time.Sleep(time.Second) + t.Logf("Redis is still running at localhost:%s, retrying...", redisPort) + continue + } + return down +} + +func cleanUp(t *testing.T, cfg *config.Config) { + t.Cleanup(func() { + assert.NilError(t, dockerClient.Purge(cfg), "Failed to purge container during cleanup") + }) +} diff --git a/common/docker/client_util.go b/common/docker/client_util.go new file mode 100644 index 0000000..a3841d6 --- /dev/null +++ b/common/docker/client_util.go @@ -0,0 +1,53 @@ +package docker + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/docker/docker/client" + + "pkg.world.dev/world-cli/common/logger" +) + +func contextPrint(title string, titleColor string, subject string, subjectColor string, object string) string { + titleStr := foregroundPrint(title, titleColor) + arrowStr := foregroundPrint("→", "241") + subjectStr := foregroundPrint(subject, subjectColor) + + return fmt.Sprintf("%s %s %s %s", titleStr, arrowStr, subjectStr, object) +} + +func foregroundPrint(text string, color string) string { + return lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render(text) +} + +func checkBuildKitSupport(cli *client.Client) bool { + ctx := context.Background() + defer func() { + err := cli.Close() + if err != nil { + logger.Error("Failed to close docker client", err) + } + }() + + // Get Docker server version + version, err := cli.ServerVersion(ctx) + if err != nil { + logger.Warnf("Failed to get Docker server version: %v", err) + return false + } + + // Check if the Docker version supports BuildKit + supportsBuildKit := strings.HasPrefix(version.Version, "18.09") || version.Version > "18.09" + + if !supportsBuildKit { + return false + } + + // Check if DOCKER_BUILDKIT environment variable is set to 1 + buildKitEnv := os.Getenv("DOCKER_BUILDKIT") + return buildKitEnv == "1" +} diff --git a/common/docker/client_volume.go b/common/docker/client_volume.go new file mode 100644 index 0000000..f31d018 --- /dev/null +++ b/common/docker/client_volume.go @@ -0,0 +1,66 @@ +package docker + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types/volume" + "github.com/rotisserie/eris" + + "pkg.world.dev/world-cli/common/logger" + "pkg.world.dev/world-cli/tea/style" +) + +func (c *Client) createVolumeIfNotExists(volumeName string) error { + ctx := context.Background() + volumes, err := c.cli.VolumeList(ctx, volume.ListOptions{}) + if err != nil { + return err + } + + for _, volume := range volumes.Volumes { + if volume.Name == volumeName { + logger.Debugf("Volume %s already exists\n", volumeName) + return nil + } + } + + _, err = c.cli.VolumeCreate(ctx, volume.CreateOptions{Name: volumeName}) + if err != nil { + return err + } + + fmt.Printf("Created volume %s\n", volumeName) + return nil +} + +func (c *Client) removeVolume(ctx context.Context, volumeName string) error { + volumes, err := c.cli.VolumeList(ctx, volume.ListOptions{}) + if err != nil { + return eris.Wrap(err, "Failed to list volumes") + } + + isExist := false + for _, v := range volumes.Volumes { + if v.Name == volumeName { + isExist = true + break + } + } + + // Return if volume does not exist + if !isExist { + return nil + } + + text := contextPrint("Removing", "1", "volume", "4", volumeName) + fmt.Print(text) + + err = c.cli.VolumeRemove(ctx, volumeName, true) + if err != nil { + return eris.Wrapf(err, "Failed to remove volume %s", volumeName) + } + + fmt.Printf("\r%s %s\n", text, style.TickIcon.Render()) + return nil +} diff --git a/common/docker/service/cardinal.Dockerfile b/common/docker/service/cardinal.Dockerfile new file mode 100644 index 0000000..407fc0d --- /dev/null +++ b/common/docker/service/cardinal.Dockerfile @@ -0,0 +1,61 @@ +################################ +# Build Image - Normal +################################ +FROM golang:1.22-bookworm AS build + +WORKDIR /go/src/app + +# Copy the go module files and download the dependencies +# We do this before copying the rest of the source code to avoid +# having to re-download the dependencies every time we build the image +COPY /cardinal/go.mod /cardinal/go.sum ./ +RUN go mod download + +# Set the GOCACHE environment variable to /root/.cache/go-build to speed up build +ENV GOCACHE=/root/.cache/go-build + +# Copy the rest of the source code and build the binary +COPY /cardinal ./ +RUN --mount=type=cache,target="/root/.cache/go-build" go build -v -o /go/bin/app + +################################ +# Runtime Image - Normal +################################ +FROM gcr.io/distroless/base-debian12 AS runtime + +# Copy world.toml to the image +COPY world.toml world.toml + +# Copy the binary from the build image +COPY --from=build /go/bin/app /usr/bin + +# Run the binary +CMD ["app"] + +################################ +# Runtime Image - Debug +################################ +FROM golang:1.22-bookworm AS runtime-debug + +WORKDIR /go/src/app + +# Install delve +RUN go install github.com/go-delve/delve/cmd/dlv@latest + +# Copy the go module files and download the dependencies +# We do this before copying the rest of the source code to avoid +# having to re-download the dependencies every time we build the image +COPY /cardinal/go.mod /cardinal/go.sum ./ +RUN go mod download + +# Set the GOCACHE environment variable to /root/.cache/go-build to speed up build +ENV GOCACHE=/root/.cache/go-build + +# Copy the rest of the source code and build the binary with debugging symbols +COPY /cardinal ./ +RUN --mount=type=cache,target="/root/.cache/go-build" go build -gcflags="all=-N -l" -v -o /usr/bin/app + +# Copy world.toml to the image +COPY world.toml world.toml + +CMD ["dlv", "--listen=:40000", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/usr/bin/app"] \ No newline at end of file diff --git a/common/docker/service/cardinal.go b/common/docker/service/cardinal.go new file mode 100644 index 0000000..749efdb --- /dev/null +++ b/common/docker/service/cardinal.go @@ -0,0 +1,94 @@ +package service + +import ( + "fmt" + "strings" + + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + + "pkg.world.dev/world-cli/common/config" + + _ "embed" +) + +const ( + // mountCache is the Docker mount command to cache the go build cache + mountCacheScript = `--mount=type=cache,target="/root/.cache/go-build"` +) + +//go:embed cardinal.Dockerfile +var dockerfileContent string + +func getCardinalContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-cardinal", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func Cardinal(cfg *config.Config) Service { + containerConfig := container.Config{ + Image: cfg.DockerEnv["CARDINAL_NAMESPACE"], + Env: []string{ + fmt.Sprintf("REDIS_ADDRESS=%s:6379", getRedisContainerName(cfg)), + fmt.Sprintf("BASE_SHARD_SEQUENCER_ADDRESS=%s:9601", getEVMContainerName(cfg)), + }, + ExposedPorts: nat.PortSet{ + "4040/tcp": struct{}{}, + }, + } + + hostConfig := container.HostConfig{ + PortBindings: nat.PortMap{ + "4040/tcp": []nat.PortBinding{{HostPort: "4040"}}, + }, + RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, + NetworkMode: container.NetworkMode(cfg.DockerEnv["CARDINAL_NAMESPACE"]), + } + + // Add debug options + debug := cfg.Debug + if debug { + containerConfig.ExposedPorts["40000/tcp"] = struct{}{} + hostConfig.PortBindings["40000/tcp"] = []nat.PortBinding{{HostPort: "40000"}} + hostConfig.CapAdd = []string{"SYS_PTRACE"} + hostConfig.SecurityOpt = []string{"seccomp:unconfined"} + } + + return Service{ + Name: getCardinalContainerName(cfg), + Config: containerConfig, + HostConfig: hostConfig, + Dockerfile: prepareCardinalDockerfile(cfg), + } +} + +func prepareCardinalDockerfile(cfg *config.Config) Dockerfile { + runtime := "runtime" + if cfg.Debug { + runtime = "runtime-debug" + } + + dockerfile := dockerfileContent + if !cfg.BuildkitSupport { + dockerfile = strings.ReplaceAll(dockerfile, mountCacheScript, "") + } + + return Dockerfile{ + Script: dockerfile, + Target: runtime, + // Images for the build + PrerequisiteImages: []Service{ + { + Name: "golang:1.22-bookworm", + Config: container.Config{ + Image: "golang:1.22-bookworm", + }, + }, + { + Name: "gcr.io/distroless/base-debian12", + Config: container.Config{ + Image: "gcr.io/distroless/base-debian12", + }, + }, + }, + } +} diff --git a/common/docker/service/celestia.go b/common/docker/service/celestia.go new file mode 100644 index 0000000..0e7075b --- /dev/null +++ b/common/docker/service/celestia.go @@ -0,0 +1,46 @@ +package service + +import ( + "fmt" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + + "pkg.world.dev/world-cli/common/config" +) + +func getCelestiaDevNetContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-celestia-devnet", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func CelestiaDevNet(cfg *config.Config) Service { + return Service{ + Name: getCelestiaDevNetContainerName(cfg), + Config: container.Config{ + Image: "ghcr.io/rollkit/local-celestia-devnet:latest", + ExposedPorts: nat.PortSet{ + "26657/tcp": struct{}{}, + "26658/tcp": struct{}{}, + "26659/tcp": struct{}{}, + "9090/tcp": struct{}{}, + }, + Healthcheck: &container.HealthConfig{ + Test: []string{"CMD", "curl", "-f", "http://127.0.0.1:26659/head"}, + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + Retries: 20, //nolint:gomnd + }, + }, + HostConfig: container.HostConfig{ + PortBindings: nat.PortMap{ + "26657/tcp": []nat.PortBinding{{HostPort: "26657"}}, + "26658/tcp": []nat.PortBinding{{HostPort: "26658"}}, + "26659/tcp": []nat.PortBinding{{HostPort: "26659"}}, + "9090/tcp": []nat.PortBinding{{HostPort: "9090"}}, + }, + RestartPolicy: container.RestartPolicy{Name: "on-failure"}, + NetworkMode: container.NetworkMode(cfg.DockerEnv["CARDINAL_NAMESPACE"]), + }, + } +} diff --git a/common/docker/service/evm.go b/common/docker/service/evm.go new file mode 100644 index 0000000..6490836 --- /dev/null +++ b/common/docker/service/evm.go @@ -0,0 +1,79 @@ +package service + +import ( + "fmt" + + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + + "pkg.world.dev/world-cli/common/config" +) + +func getEVMContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-evm", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func EVM(cfg *config.Config) Service { + daBaseURL := cfg.DockerEnv["DA_BASE_URL"] + if daBaseURL == "" { + daBaseURL = getCelestiaDevNetContainerName(cfg) + } + + daAuthToken := cfg.DockerEnv["DA_AUTH_TOKEN"] + if daAuthToken == "" { + daAuthToken = "" + } + + faucetEnabled := cfg.DockerEnv["FAUCET_ENABLED"] + if faucetEnabled == "" { + faucetEnabled = "false" + } + + faucetAddress := cfg.DockerEnv["FAUCET_ADDRESS"] + if faucetAddress == "" { + faucetAddress = "aa9288F88233Eb887d194fF2215Cf1776a6FEE41" + } + + faucetAmount := cfg.DockerEnv["FAUCET_AMOUNT"] + if faucetAmount == "" { + faucetAmount = "0x56BC75E2D63100000" + } + + baseShardRouterKey := cfg.DockerEnv["BASE_SHARD_ROUTER_KEY"] + if baseShardRouterKey == "" { + baseShardRouterKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ01" + } + + return Service{ + Name: getEVMContainerName(cfg), + Config: container.Config{ + Image: "ghcr.io/argus-labs/world-engine-evm:1.4.1", + Env: []string{ + fmt.Sprintf("DA_BASE_URL=%s", daBaseURL), + fmt.Sprintf("DA_AUTH_TOKEN=%s", daAuthToken), + fmt.Sprintf("FAUCET_ENABLED=%s", faucetEnabled), + fmt.Sprintf("FAUCET_ADDRESS=%s", faucetAddress), + fmt.Sprintf("FAUCET_AMOUNT=%s", faucetAmount), + fmt.Sprintf("BASE_SHARD_ROUTER_KEY=%s", baseShardRouterKey), + }, + ExposedPorts: nat.PortSet{ + "1317/tcp": struct{}{}, + "26657/tcp": struct{}{}, + "9090/tcp": struct{}{}, + "9601/tcp": struct{}{}, + "8545/tcp": struct{}{}, + }, + }, + HostConfig: container.HostConfig{ + PortBindings: nat.PortMap{ + "1317/tcp": []nat.PortBinding{{HostPort: "1317"}}, + "26657/tcp": []nat.PortBinding{{HostPort: "26657"}}, + "9090/tcp": []nat.PortBinding{{HostPort: "9090"}}, + "9601/tcp": []nat.PortBinding{{HostPort: "9601"}}, + "8545/tcp": []nat.PortBinding{{HostPort: "8545"}}, + }, + RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, + NetworkMode: container.NetworkMode(cfg.DockerEnv["CARDINAL_NAMESPACE"]), + }, + } +} diff --git a/common/docker/service/nakama.go b/common/docker/service/nakama.go new file mode 100644 index 0000000..9d62d1a --- /dev/null +++ b/common/docker/service/nakama.go @@ -0,0 +1,76 @@ +package service + +import ( + "fmt" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "pkg.world.dev/world-cli/common/config" +) + +func getNakamaContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-nakama", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func Nakama(cfg *config.Config) Service { + enableAllowList := cfg.DockerEnv["ENABLE_ALLOWLIST"] + if enableAllowList == "" { + enableAllowList = "false" + } + + outgoingQueueSize := cfg.DockerEnv["OUTGOING_QUEUE_SIZE"] + if outgoingQueueSize == "" { + outgoingQueueSize = "64" + } + + return Service{ + Name: getNakamaContainerName(cfg), + Config: container.Config{ + Image: "ghcr.io/argus-labs/world-engine-nakama:1.2.7", + Env: []string{ + fmt.Sprintf("CARDINAL_CONTAINER=%s", getCardinalContainerName(cfg)), + fmt.Sprintf("CARDINAL_ADDR=%s:4040", getCardinalContainerName(cfg)), + fmt.Sprintf("CARDINAL_NAMESPACE=%s", cfg.DockerEnv["CARDINAL_NAMESPACE"]), + fmt.Sprintf("DB_PASSWORD=%s", cfg.DockerEnv["DB_PASSWORD"]), + fmt.Sprintf("ENABLE_ALLOWLIST=%s", enableAllowList), + fmt.Sprintf("OUTGOING_QUEUE_SIZE=%s", outgoingQueueSize), + }, + Entrypoint: []string{ + "/bin/sh", + "-ec", + fmt.Sprintf("/nakama/nakama migrate up --database.address root:%s@%s:26257/nakama && /nakama/nakama --config /nakama/data/local.yml --database.address root:%s@%s:26257/nakama --socket.outgoing_queue_size=64 --logger.level INFO", //nolint:lll + cfg.DockerEnv["DB_PASSWORD"], + getNakamaDBContainerName(cfg), + cfg.DockerEnv["DB_PASSWORD"], + getNakamaDBContainerName(cfg)), + }, + ExposedPorts: nat.PortSet{ + "7349/tcp": struct{}{}, + "7350/tcp": struct{}{}, + "7351/tcp": struct{}{}, + }, + Healthcheck: &container.HealthConfig{ + Test: []string{"CMD", "/nakama/nakama", "healthcheck"}, + Interval: 1 * time.Second, + Timeout: 1 * time.Second, + Retries: 20, //nolint:gomnd + }, + }, + HostConfig: container.HostConfig{ + PortBindings: nat.PortMap{ + "7349/tcp": []nat.PortBinding{{HostPort: "7349"}}, + "7350/tcp": []nat.PortBinding{{HostPort: "7350"}}, + "7351/tcp": []nat.PortBinding{{HostPort: "7351"}}, + }, + RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, + NetworkMode: container.NetworkMode(cfg.DockerEnv["CARDINAL_NAMESPACE"]), + }, + Platform: ocispec.Platform{ + Architecture: "amd64", + OS: "linux", + }, + } +} diff --git a/common/docker/service/nakamadb.go b/common/docker/service/nakamadb.go new file mode 100644 index 0000000..a83fc05 --- /dev/null +++ b/common/docker/service/nakamadb.go @@ -0,0 +1,51 @@ +package service + +import ( + "fmt" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/go-connections/nat" + + "pkg.world.dev/world-cli/common/config" +) + +func getNakamaDBContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-nakama-db", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func NakamaDB(cfg *config.Config) Service { + return Service{ + Name: getNakamaDBContainerName(cfg), + Config: container.Config{ + Image: "cockroachdb/cockroach:latest-v23.1", + Cmd: []string{"start-single-node", "--insecure", "--store=attrs=ssd,path=/var/lib/cockroach/,size=20%"}, + Env: []string{ + "COCKROACH_DATABASE=nakama", + "COCKROACH_USER=root", + fmt.Sprintf("COCKROACH_PASSWORD=%s", cfg.DockerEnv["DB_PASSWORD"]), + }, + ExposedPorts: nat.PortSet{ + "26257/tcp": struct{}{}, + "8080/tcp": struct{}{}, + }, + Healthcheck: &container.HealthConfig{ + Test: []string{"CMD", "curl", "-f", "http://localhost:8080/health?ready=1"}, + Interval: 3 * time.Second, //nolint:gomnd + Timeout: 3 * time.Second, //nolint:gomnd + Retries: 5, //nolint:gomnd + }, + }, + HostConfig: container.HostConfig{ + PortBindings: nat.PortMap{ + "26257/tcp": []nat.PortBinding{{HostPort: "26257"}}, + "8080/tcp": []nat.PortBinding{{HostPort: "8080"}}, + }, + RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, + Mounts: []mount.Mount{{Type: mount.TypeVolume, Source: cfg.DockerEnv["CARDINAL_NAMESPACE"], + Target: "/var/lib/cockroach"}}, + NetworkMode: container.NetworkMode(cfg.DockerEnv["CARDINAL_NAMESPACE"]), + }, + } +} diff --git a/common/docker/service/redis.go b/common/docker/service/redis.go new file mode 100644 index 0000000..1020d5f --- /dev/null +++ b/common/docker/service/redis.go @@ -0,0 +1,43 @@ +package service + +import ( + "fmt" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/go-connections/nat" + + "pkg.world.dev/world-cli/common/config" +) + +func getRedisContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-redis", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func Redis(cfg *config.Config) Service { + redisPort := cfg.DockerEnv["REDIS_PORT"] + if redisPort == "" { + redisPort = "6379" + } + + return Service{ + Name: getRedisContainerName(cfg), + Config: container.Config{ + Image: "redis:latest", + Env: []string{ + fmt.Sprintf("REDIS_PASSWORD=%s", cfg.DockerEnv["REDIS_PASSWORD"]), + }, + ExposedPorts: nat.PortSet{ + "6379/tcp": struct{}{}, + }, + }, + HostConfig: container.HostConfig{ + PortBindings: nat.PortMap{ + "6379/tcp": []nat.PortBinding{{HostPort: redisPort}}, + }, + RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, + Mounts: []mount.Mount{{Type: mount.TypeVolume, Source: "data", Target: "/redis"}}, + NetworkMode: container.NetworkMode(cfg.DockerEnv["CARDINAL_NAMESPACE"]), + }, + } +} diff --git a/common/docker/service/service.go b/common/docker/service/service.go new file mode 100644 index 0000000..67dfe74 --- /dev/null +++ b/common/docker/service/service.go @@ -0,0 +1,28 @@ +package service + +import ( + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "pkg.world.dev/world-cli/common/config" +) + +type GetDockerService func(cfg *config.Config) Service + +// DockerConfig is a configuration for a docker container +// It contains the name of the container and a function to get the container and host config +type Service struct { + Name string + container.Config + container.HostConfig + network.NetworkingConfig + ocispec.Platform + Dockerfile +} + +type Dockerfile struct { + PrerequisiteImages []Service + Script string + Target string +} diff --git a/common/logger/init.go b/common/logger/init.go index 80da3d4..ff1da3a 100644 --- a/common/logger/init.go +++ b/common/logger/init.go @@ -21,7 +21,7 @@ var ( logBuffer bytes.Buffer // VerboseMode flag for determining verbose logging - verboseMode = false + VerboseMode = false ) func init() { @@ -51,7 +51,7 @@ func init() { // PrintLogs print all stacked log func PrintLogs() { - if verboseMode { + if VerboseMode { // Extract the logs from the buffer and print them logs := logBuffer.String() if len(logs) > 0 { @@ -64,6 +64,6 @@ func PrintLogs() { // AddVerboseFlag set flag --log-debug func AddVerboseFlag(cmd ...*cobra.Command) { for _, c := range cmd { - c.PersistentFlags().BoolVarP(&verboseMode, "verbose", "v", false, "Enable World CLI debug logs") + c.PersistentFlags().BoolVarP(&VerboseMode, "verbose", "v", false, "Enable World CLI debug logs") } } diff --git a/common/logger/logger.go b/common/logger/logger.go index f0425ca..47fe155 100644 --- a/common/logger/logger.go +++ b/common/logger/logger.go @@ -68,6 +68,7 @@ func WarnWithFields(msg string, kv map[string]interface{}) { // Error function func Error(args ...interface{}) { + fmt.Print(args...) log.Error().Timestamp().Msg(fmt.Sprint(args...)) } @@ -118,21 +119,21 @@ func FatalWithFields(msg string, kv map[string]interface{}) { // Printf standard printf with debug mode validation func Printf(format string, v ...interface{}) { - if verboseMode { + if VerboseMode { fmt.Printf(format, v...) } } // Println standard println with debug mode validation func Println(v ...interface{}) { - if verboseMode { + if VerboseMode { fmt.Println(v...) } } // Print standard print with debug mode validation func Print(v ...interface{}) { - if verboseMode { + if VerboseMode { fmt.Print(v...) } } diff --git a/go.mod b/go.mod index d66e100..abd3b83 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,14 @@ require ( github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.26.2 github.com/denisbrodbeck/machineid v1.0.1 + github.com/docker/docker v27.1.1+incompatible + github.com/docker/go-connections v0.5.0 github.com/getsentry/sentry-go v0.27.0 github.com/google/uuid v1.6.0 github.com/guumaster/logsymbols v0.3.1 github.com/hashicorp/go-version v1.6.0 github.com/magefile/mage v1.15.0 + github.com/opencontainers/image-spec v1.1.0 github.com/pelletier/go-toml v1.9.5 github.com/pelletier/go-toml/v2 v2.2.2 github.com/pkg/errors v0.9.1 @@ -26,14 +29,32 @@ require ( ) require ( + github.com/Microsoft/go-winio v0.4.14 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-errors/errors v1.5.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -52,8 +73,8 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sync v0.7.0 - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.20.0 - golang.org/x/text v0.15.0 // indirect + golang.org/x/text v0.16.0 // indirect gopkg.in/gookit/color.v1 v1.1.6 // indirect ) diff --git a/go.sum b/go.sum index 361ab17..dcb7557 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,14 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v0.26.2 h1:Eeb+n75Om9gQ+I6YpbCXQRKHt5Pn4vMwusQpwLiEgJQ= @@ -11,6 +17,8 @@ github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -20,24 +28,46 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/guumaster/logsymbols v0.3.1 h1:bnCE484dAQFvMWt2EfZAzF1oCgu8yo/Vp1QGQ0EmaAA= github.com/guumaster/logsymbols v0.3.1/go.mod h1:1M5/1js2Z7Yo8DRB3QrPURwqsXeOfgsJv1Utjookknw= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= @@ -54,6 +84,12 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -62,12 +98,17 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -87,14 +128,19 @@ github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -102,21 +148,78 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/gookit/color.v1 v1.1.6 h1:5fB10p6AUFjhd2ayq9JgmJWr9WlTrguFdw3qlYtKNHk= diff --git a/makefiles/lint.mk b/makefiles/lint.mk index b33716d..1cda4b0 100644 --- a/makefiles/lint.mk +++ b/makefiles/lint.mk @@ -2,12 +2,14 @@ lint_version=v1.56.2 lint-install: @echo "--> Checking if golangci-lint $(lint_version) is installed" - @if [ $$(golangci-lint --version 2> /dev/null | awk '{print $$4}') != "$(lint_version)" ]; then \ + @installed_version=$$(golangci-lint --version 2> /dev/null | awk '{print $$4}') || true; \ + if [ "$$installed_version" != "$(lint_version)" ]; then \ echo "--> Installing golangci-lint $(lint_version)"; \ go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(lint_version); \ else \ echo "--> golangci-lint $(lint_version) is already installed"; \ fi + lint: @$(MAKE) lint-install @echo "--> Running linter"