diff --git a/cmd/world/cardinal/dev.go b/cmd/world/cardinal/dev.go index 1e04da6..7785668 100644 --- a/cmd/world/cardinal/dev.go +++ b/cmd/world/cardinal/dev.go @@ -18,8 +18,8 @@ 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/logger" - "pkg.world.dev/world-cli/common/teacmd" "pkg.world.dev/world-cli/tea/style" ) @@ -231,7 +231,7 @@ func startRedis(ctx context.Context, cfg *config.Config) error { // 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(cfg, docker.Redis); err != nil { return eris.Wrap(err, "Encountered an error with Redis") } return nil @@ -243,7 +243,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, docker.Redis); err != nil { return err } return nil diff --git a/cmd/world/cardinal/purge.go b/cmd/world/cardinal/purge.go index 46e2daf..5a0d77d 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,13 @@ 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 + } + + 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..efe79a9 100644 --- a/cmd/world/cardinal/restart.go +++ b/cmd/world/cardinal/restart.go @@ -4,7 +4,7 @@ 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" ) // restartCmd restarts your Cardinal game shard stack @@ -31,18 +31,7 @@ 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, - }) - } - + err = docker.Restart(cfg, docker.Cardinal, docker.Nakama) if err != nil { return err } diff --git a/cmd/world/cardinal/start.go b/cmd/world/cardinal/start.go index 8ed5085..6d06c3a 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,7 @@ 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" ) ///////////////// @@ -108,11 +109,15 @@ 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()) // Start the World Engine stack group.Go(func() error { - if err := teacmd.DockerStartAll(cfg); err != nil { + if err := docker.Start(cfg, docker.NakamaDB, docker.Redis, docker.Cardinal, docker.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..b89baac 100644 --- a/cmd/world/cardinal/stop.go +++ b/cmd/world/cardinal/stop.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" ) ///////////////// @@ -23,8 +24,13 @@ 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 + } + + err = docker.Stop(cfg, docker.Nakama, docker.Cardinal, docker.NakamaDB, docker.Redis) if err != nil { return err } diff --git a/cmd/world/root/root_test.go b/cmd/world/root/root_test.go index 958f9f9..18e472e 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", "-v"}) err = rootCmd.Execute() assert.NilError(t, err) diff --git a/common/docker/configuration.go b/common/docker/configuration.go new file mode 100644 index 0000000..b36f56b --- /dev/null +++ b/common/docker/configuration.go @@ -0,0 +1,271 @@ +package docker + +import ( + "fmt" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/go-connections/nat" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "pkg.world.dev/world-cli/common/config" +) + +type GetDockerConfig func(cfg *config.Config) Config + +// 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 Config struct { + Name string + *container.Config + *container.HostConfig + *network.NetworkingConfig + *ocispec.Platform + *Dockerfile +} + +type Dockerfile struct { + Script string + Target string +} + +func getRedisContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-redis", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func getCardinalContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-cardinal", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func getNakamaContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-nakama", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func getNakamaDBContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-nakama-db", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func getCelestiaDevNetContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-celestia-devnet", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func getEVMContainerName(cfg *config.Config) string { + return fmt.Sprintf("%s-evm", cfg.DockerEnv["CARDINAL_NAMESPACE"]) +} + +func Redis(cfg *config.Config) Config { + return Config{ + 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: "6379"}}, + }, + RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, + Mounts: []mount.Mount{{Type: mount.TypeVolume, Source: "data", Target: "/redis"}}, + NetworkMode: container.NetworkMode(cfg.DockerEnv["CARDINAL_NAMESPACE"]), + }, + } +} + +func Cardinal(cfg *config.Config) Config { + imageName := cfg.DockerEnv["CARDINAL_NAMESPACE"] + containerConfig := container.Config{ + Image: imageName, + 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"]), + } + + dockerfile := Dockerfile{Script: cardinalDockerfile, Target: "runtime"} + + // 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"} + dockerfile.Target = "runtime-debug" + } + + return Config{ + Name: getCardinalContainerName(cfg), + Config: &containerConfig, + HostConfig: &hostConfig, + Dockerfile: &dockerfile, + } +} + +func Nakama(cfg *config.Config) Config { + return Config{ + 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"]), + "ENABLE_ALLOWLIST=false", + "OUTGOING_QUEUE_SIZE=64", + }, + 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", + }, + } +} + +func NakamaDB(cfg *config.Config) Config { + return Config{ + 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"]), + }, + } +} + +func CelestiaDevNet(cfg *config.Config) Config { + return Config{ + 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"]), + }, + } +} + +func EVM(cfg *config.Config) Config { + return Config{ + Name: getEVMContainerName(cfg), + Config: &container.Config{ + Image: "ghcr.io/argus-labs/world-engine-evm:1.4.1", + Env: []string{ + "DA_BASE_URL=http://celestia-devnet", + "DA_AUTH_TOKEN=", + "FAUCET_ENABLED=false", + "FAUCET_ADDRESS=aa9288F88233Eb887d194fF2215Cf1776a6FEE41", + "FAUCET_AMOUNT=0x56BC75E2D63100000", + "BASE_SHARD_ROUTER_KEY=abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ01", + }, + 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/docker.go b/common/docker/docker.go new file mode 100644 index 0000000..1f880ba --- /dev/null +++ b/common/docker/docker.go @@ -0,0 +1,490 @@ +package docker + +import ( + "archive/tar" + "bytes" + "context" + "fmt" + "io" + "os" + "os/signal" + "path/filepath" + "syscall" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/client" + "github.com/rotisserie/eris" + + "pkg.world.dev/world-cli/common/config" + "pkg.world.dev/world-cli/common/logger" +) + +func Start(cfg *config.Config, configurations ...GetDockerConfig) error { //nolint:gocognit + ctx := context.Background() + + cli, err := getCli() + if err != nil { + return eris.Wrap(err, "Failed to create docker client") + } + defer func() { + if !cfg.Detach { + err := Stop(cfg, configurations...) + if err != nil { + logger.Error("Failed to stop containers", err) + } + } + + err := cli.Close() + if err != nil { + logger.Error("Failed to close docker client", err) + } + }() + + namespace := cfg.DockerEnv["CARDINAL_NAMESPACE"] + err = createNetworkIfNotExists(cli, namespace) + if err != nil { + return eris.Wrap(err, "Failed to create network") + } + + err = createVolumeIfNotExists(cli, 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 _, c := range configurations { + configuration := c(cfg) + if configuration.Dockerfile == nil { + if err := pullImageIfNotExists(ctx, cli, configuration.Image); err != nil { + return eris.Wrap(err, "Failed to pull image") + } + } else if cfg.Build { + if err := buildImage(ctx, cli, *configuration.Dockerfile, configuration.Image); err != nil { + return eris.Wrap(err, "Failed to build image") + } + } + + if err := createContainer(ctx, cli, configuration); err != nil { + return eris.Wrap(err, "Failed to create container") + } + + containers = append(containers, configuration.Name) + } + + // log containers if not detached + if !cfg.Detach { + logContainers(cli, containers) + } + + return nil +} + +func Stop(cfg *config.Config, configurations ...GetDockerConfig) error { + cli, err := getCli() + if err != nil { + return eris.Wrap(err, "Failed to create docker client") + } + defer func() { + err := cli.Close() + if err != nil { + logger.Error("Failed to close docker client", err) + } + }() + + ctx := context.Background() + for _, c := range configurations { + configuration := c(cfg) + if err := stopAndRemoveContainer(ctx, cli, configuration.Name); err != nil { + return eris.Wrap(err, "Failed to stop container") + } + } + + err = removeNetwork(ctx, cli, cfg.DockerEnv["CARDINAL_NAMESPACE"]) + if err != nil { + return eris.Wrapf(err, "Failed to remove network %s", cfg.DockerEnv["CARDINAL_NAMESPACE"]) + } + + return nil +} + +func Purge(cfg *config.Config) error { + cli, err := getCli() + if err != nil { + return eris.Wrap(err, "Failed to create docker client") + } + defer func() { + err := cli.Close() + if err != nil { + logger.Error("Failed to close docker client", err) + } + }() + + err = Stop(cfg, Nakama, NakamaDB, Cardinal, Redis, CelestiaDevNet, EVM) + if err != nil { + return err + } + + err = removeVolume(context.Background(), cli, cfg.DockerEnv["CARDINAL_NAMESPACE"]) + if err != nil { + return err + } + + return nil +} + +func Restart(cfg *config.Config, configurations ...GetDockerConfig) error { //nolint:gocognit + cli, err := getCli() + if err != nil { + return eris.Wrap(err, "Failed to create docker client") + } + defer func() { + if !cfg.Detach { + err := Stop(cfg, configurations...) + if err != nil { + logger.Error("Failed to stop containers", err) + } + } + + err := cli.Close() + if err != nil { + logger.Error("Failed to close docker client", err) + } + }() + + ctx := context.Background() + for _, c := range configurations { + configuration := c(cfg) + if err := stopAndRemoveContainer(ctx, cli, configuration.Name); err != nil { + return eris.Wrap(err, "Failed to stop container") + } + } + + // var for storing container names + containers := make([]string, 0) + + // iterate over configurations and create containers + for _, c := range configurations { + configuration := c(cfg) + if configuration.Dockerfile == nil { + if err := pullImageIfNotExists(ctx, cli, configuration.Image); err != nil { + return eris.Wrap(err, "Failed to pull image") + } + } else if cfg.Build { + if err := buildImage(ctx, cli, *configuration.Dockerfile, configuration.Image); err != nil { + return eris.Wrap(err, "Failed to build image") + } + } + + if err := createContainer(ctx, cli, configuration); err != nil { + return eris.Wrap(err, "Failed to create container") + } + + containers = append(containers, configuration.Name) + } + + // log containers if not detached + if !cfg.Detach { + logContainers(cli, containers) + } + + return nil +} + +func getCli() (*client.Client, error) { + return client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) +} + +func createNetworkIfNotExists(cli *client.Client, networkName string) error { + ctx := context.Background() + networks, err := 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 = cli.NetworkCreate(ctx, networkName, network.CreateOptions{ + Driver: "bridge", + }) + if err != nil { + return err + } + + return nil +} + +func createVolumeIfNotExists(cli *client.Client, volumeName string) error { + ctx := context.Background() + volumes, err := 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 = cli.VolumeCreate(ctx, volume.CreateOptions{Name: volumeName}) + if err != nil { + return err + } + + fmt.Printf("Created volume %s\n", volumeName) + return nil +} + +func createContainer(ctx context.Context, cli *client.Client, configuration Config) error { + resp, err := cli.ContainerCreate(ctx, configuration.Config, configuration.HostConfig, + configuration.NetworkingConfig, configuration.Platform, configuration.Name) + if err != nil { + return err + } + + if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + return err + } + + return nil +} + +func pullImageIfNotExists(ctx context.Context, cli *client.Client, imageName string) error { + _, _, err := cli.ImageInspectWithRaw(ctx, imageName) + + // If image exists, return + if err == nil { + logger.Println("Image already exists", imageName) + return nil + } + + // If image does not exist, pull it + if client.IsErrNotFound(err) { + out, err := cli.ImagePull(ctx, imageName, image.PullOptions{}) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(os.Stdout, out) + if err != nil { + return err + } + } else { + return err + } + + return nil +} + +func buildImage(ctx context.Context, cli *client.Client, dockerfile Dockerfile, imageName string) error { + fmt.Println("Building image ", 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 := 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, + Version: types.BuilderBuildKit, + } + + // Build the image + buildResponse, err := cli.ImageBuild(ctx, tarReader, buildOptions) + if err != nil { + return err + } + defer buildResponse.Body.Close() + + // Print the build logs + if logger.VerboseMode { + _, err = io.Copy(os.Stdout, buildResponse.Body) + } + + fmt.Println("Image built successfully") + return err +} + +func stopAndRemoveContainer(ctx context.Context, cli *client.Client, containerName string) error { + fmt.Printf("Removing %s...", containerName) + + // Check if the container exists + _, err := cli.ContainerInspect(ctx, containerName) + if err != nil { + if client.IsErrNotFound(err) { + fmt.Println(" Done") + return nil // or return an error if you prefer + } + return eris.Wrapf(err, "Failed to inspect container %s", containerName) + } + + // Stop the container + err = 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 = cli.ContainerRemove(ctx, containerName, container.RemoveOptions{}) + if err != nil { + return eris.Wrapf(err, "Failed to remove container %s", containerName) + } + + fmt.Println(" Done") + + return nil +} + +func removeNetwork(ctx context.Context, cli *client.Client, networkName string) error { + fmt.Printf("Removing network %s...", networkName) + + networks, err := 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 = cli.NetworkRemove(ctx, networkName) + if err != nil { + return err + } + } + + fmt.Println(" Done") + return nil +} + +func removeVolume(ctx context.Context, cli *client.Client, volumeName string) error { + fmt.Printf("Removing volume %s...", volumeName) + + err := cli.VolumeRemove(ctx, volumeName, true) + if err != nil { + return eris.Wrapf(err, "Failed to remove volume %s", volumeName) + } + + fmt.Println(" Done") + return nil +} + +func logContainers(cli *client.Client, containers []string) { + logs := make(map[string]io.ReadCloser) + for _, c := range containers { + out, err := cli.ContainerLogs(context.Background(), c, container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + }) + if err != nil { + panic(err) + } + logs[c] = out + defer out.Close() + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + + for name, log := range logs { + go func(name string, log io.ReadCloser) { + buf := make([]byte, 4096) //nolint:gomnd + for { + n, err := log.Read(buf) + if n > 0 { + fmt.Printf("[%s] %s", name, buf[:n]) + } + if err != nil { + break + } + } + }(name, log) + } + + <-stop +} + +// AddFileToTarWriter adds a file or directory to the tar writer +func 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 + } + + // 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 + relPath, err := filepath.Rel(baseDir, path) + if err != nil { + return err + } + 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/script.go b/common/docker/script.go new file mode 100644 index 0000000..54e0c77 --- /dev/null +++ b/common/docker/script.go @@ -0,0 +1,66 @@ +package docker + +const ( + cardinalDockerfile = ` + ################################ + # 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"]` +) 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"