Skip to content

Commit 1176e48

Browse files
authored
Merge pull request #115 from ethpandaops/feat/xcli-xatu
2 parents cecb768 + 99cc652 commit 1176e48

19 files changed

+1395
-7
lines changed

cmd/xcli/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func main() {
103103
// Add stack commands
104104
rootCmd.AddCommand(commands.NewLabCommand(log, configPath))
105105
rootCmd.AddCommand(commands.NewCCCommand(log, configPath))
106+
rootCmd.AddCommand(commands.NewXatuCommand(log, configPath))
106107

107108
// Execute
108109
if err := rootCmd.ExecuteContext(ctx); err != nil {

pkg/commands/completion_helpers.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package commands
33
import (
44
"io"
55

6+
"github.com/ethpandaops/xcli/pkg/compose"
67
"github.com/ethpandaops/xcli/pkg/config"
78
"github.com/ethpandaops/xcli/pkg/constants"
89
"github.com/ethpandaops/xcli/pkg/orchestrator"
@@ -90,3 +91,33 @@ func completeReleasableProjects() func(*cobra.Command, []string, string) ([]stri
9091
return completions, cobra.ShellCompDirectiveNoFileComp
9192
}
9293
}
94+
95+
// completeXatuServices returns a ValidArgsFunction that completes xatu service names.
96+
// It loads the xatu config, creates a compose runner, and lists available services.
97+
func completeXatuServices(configPath string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
98+
return func(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
99+
if len(args) > 0 {
100+
return nil, cobra.ShellCompDirectiveNoFileComp
101+
}
102+
103+
log := logrus.New()
104+
log.SetOutput(io.Discard)
105+
106+
xatuCfg, _, err := config.LoadXatuConfig(configPath)
107+
if err != nil {
108+
return nil, cobra.ShellCompDirectiveNoFileComp
109+
}
110+
111+
runner, err := compose.NewRunner(log, xatuCfg.Repos.Xatu, xatuCfg.Profiles, xatuCfg.EnvOverrides)
112+
if err != nil {
113+
return nil, cobra.ShellCompDirectiveNoFileComp
114+
}
115+
116+
services, err := runner.ListServices(cmd.Context())
117+
if err != nil {
118+
return nil, cobra.ShellCompDirectiveNoFileComp
119+
}
120+
121+
return services, cobra.ShellCompDirectiveNoFileComp
122+
}
123+
}

pkg/commands/init.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ func NewInitCommand(log logrus.FieldLogger, configPath string) *cobra.Command {
2121
This command creates an empty configuration file that you can then populate
2222
by running stack-specific init commands:
2323
- xcli lab init (initialize lab stack configuration)
24+
- xcli xatu init (initialize xatu stack configuration)
2425
- xcli <stack> init (initialize other stack configurations)
2526
2627
If the configuration file already exists, this command will exit without changes.`,
@@ -42,6 +43,7 @@ func runRootInit(log logrus.FieldLogger, configPath string) error {
4243
ui.Blank()
4344
ui.Info("To initialize specific stacks, run:")
4445
fmt.Println(" xcli lab init - Initialize lab stack")
46+
fmt.Println(" xcli xatu init - Initialize xatu stack")
4547
fmt.Println(" xcli <stack> init - Initialize other stacks")
4648

4749
return nil
@@ -64,7 +66,8 @@ func runRootInit(log logrus.FieldLogger, configPath string) error {
6466
ui.Blank()
6567
ui.Header("Next steps:")
6668
fmt.Println(" 1. Run 'xcli lab init' to discover and configure lab repositories")
67-
fmt.Println(" 2. Run 'xcli <stack> init' for other stacks as needed")
69+
fmt.Println(" 2. Run 'xcli xatu init' to configure the xatu stack")
70+
fmt.Println(" 3. Run 'xcli <stack> init' for other stacks as needed")
6871
fmt.Printf(" 3. Edit %s to customize settings\n", configPath)
6972

7073
return nil

pkg/commands/xatu.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package commands
2+
3+
import (
4+
"github.com/sirupsen/logrus"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
// NewXatuCommand creates the xatu command namespace.
9+
func NewXatuCommand(log logrus.FieldLogger, configPath string) *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "xatu",
12+
Short: "Manage the xatu docker-compose stack",
13+
Long: `Manage the xatu docker-compose stack for local development.
14+
15+
The xatu stack is entirely docker-compose based, running ~25 services
16+
including ClickHouse, Kafka, Grafana, and the various xatu components.
17+
18+
Common workflows:
19+
1. Initial setup:
20+
xcli xatu init # Discover xatu repo, verify Docker
21+
xcli xatu check # Verify environment is ready
22+
xcli xatu up # Start the stack
23+
24+
2. Development:
25+
(make code changes)
26+
xcli xatu rebuild xatu-server # Rebuild and restart a service
27+
xcli xatu status # Check service status
28+
xcli xatu logs xatu-server -f # Stream logs
29+
30+
3. Service control:
31+
xcli xatu stop <service> # Stop a specific service
32+
xcli xatu start <service> # Start a specific service
33+
xcli xatu restart <service> # Restart a specific service
34+
35+
4. Teardown:
36+
xcli xatu down # Stop all containers
37+
xcli xatu clean # Remove everything including volumes and images
38+
39+
Use 'xcli xatu [command] --help' for more information about a command.`,
40+
}
41+
42+
// Add xatu subcommands
43+
cmd.AddCommand(NewXatuInitCommand(log, configPath))
44+
cmd.AddCommand(NewXatuCheckCommand(log, configPath))
45+
cmd.AddCommand(NewXatuUpCommand(log, configPath))
46+
cmd.AddCommand(NewXatuDownCommand(log, configPath))
47+
cmd.AddCommand(NewXatuCleanCommand(log, configPath))
48+
cmd.AddCommand(NewXatuStatusCommand(log, configPath))
49+
cmd.AddCommand(NewXatuStartCommand(log, configPath))
50+
cmd.AddCommand(NewXatuStopCommand(log, configPath))
51+
cmd.AddCommand(NewXatuRestartCommand(log, configPath))
52+
cmd.AddCommand(NewXatuLogsCommand(log, configPath))
53+
cmd.AddCommand(NewXatuBuildCommand(log, configPath))
54+
cmd.AddCommand(NewXatuRebuildCommand(log, configPath))
55+
56+
return cmd
57+
}

pkg/commands/xatu_build.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ethpandaops/xcli/pkg/config"
7+
"github.com/ethpandaops/xcli/pkg/ui"
8+
"github.com/sirupsen/logrus"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
// NewXatuBuildCommand creates the xatu build command.
13+
func NewXatuBuildCommand(log logrus.FieldLogger, configPath string) *cobra.Command {
14+
return &cobra.Command{
15+
Use: "build [service...]",
16+
Short: "Build xatu docker images",
17+
Long: `Build docker images for xatu services without starting them.
18+
19+
If no service is specified, all services with build configurations are built.
20+
21+
Examples:
22+
xcli xatu build # Build all images
23+
xcli xatu build xatu-server # Build just xatu-server image`,
24+
ValidArgsFunction: completeXatuServices(configPath),
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
xatuCfg, _, err := config.LoadXatuConfig(configPath)
27+
if err != nil {
28+
return fmt.Errorf("failed to load config: %w", err)
29+
}
30+
31+
runner, err := newXatuRunner(log, xatuCfg)
32+
if err != nil {
33+
return fmt.Errorf("failed to create compose runner: %w", err)
34+
}
35+
36+
ui.Header("Building xatu images...")
37+
ui.Blank()
38+
39+
if err := runner.Build(cmd.Context(), args...); err != nil {
40+
ui.Blank()
41+
ui.Error("Build failed")
42+
43+
return err
44+
}
45+
46+
ui.Blank()
47+
ui.Success("Build complete")
48+
49+
return nil
50+
},
51+
}
52+
}

pkg/commands/xatu_check.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os/exec"
7+
"path/filepath"
8+
9+
"github.com/ethpandaops/xcli/pkg/config"
10+
"github.com/ethpandaops/xcli/pkg/ui"
11+
"github.com/sirupsen/logrus"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
// NewXatuCheckCommand creates the xatu check command.
16+
func NewXatuCheckCommand(log logrus.FieldLogger, configPath string) *cobra.Command {
17+
return &cobra.Command{
18+
Use: "check",
19+
Short: "Verify xatu environment is ready",
20+
Long: `Perform health checks on the xatu environment without starting services.
21+
22+
Verifies:
23+
- Configuration file exists and is valid
24+
- Xatu repo exists with docker-compose.yml
25+
- Docker daemon is running and accessible
26+
- Docker Compose is available
27+
- Warns about potential port conflicts with lab stack
28+
29+
Exit codes:
30+
0 - All checks passed
31+
1 - One or more checks failed
32+
33+
Example:
34+
xcli xatu check`,
35+
RunE: func(cmd *cobra.Command, args []string) error {
36+
return runXatuCheck(cmd.Context(), log, configPath)
37+
},
38+
}
39+
}
40+
41+
func runXatuCheck(ctx context.Context, log logrus.FieldLogger, configPath string) error {
42+
ui.Header("Running xatu environment health checks...")
43+
44+
allPassed := true
45+
46+
// Check 1: Configuration file
47+
spinner := ui.NewSpinner("Checking configuration file")
48+
49+
xatuCfg, _, err := config.LoadXatuConfig(configPath)
50+
if err != nil {
51+
spinner.Fail(fmt.Sprintf("Configuration file error: %v", err))
52+
53+
allPassed = false
54+
} else {
55+
spinner.Success("Configuration file valid")
56+
}
57+
58+
// Check 2: Configuration validity
59+
if xatuCfg != nil {
60+
spinner = ui.NewSpinner("Validating configuration")
61+
62+
if err := xatuCfg.Validate(); err != nil {
63+
spinner.Fail(fmt.Sprintf("Configuration validation failed: %v", err))
64+
65+
allPassed = false
66+
} else {
67+
spinner.Success("Configuration valid")
68+
}
69+
70+
// Check 3: docker-compose.yml
71+
spinner = ui.NewSpinner("Checking docker-compose.yml")
72+
73+
absRepo, _ := filepath.Abs(xatuCfg.Repos.Xatu)
74+
composePath := filepath.Join(absRepo, "docker-compose.yml")
75+
76+
if _, statErr := statFile(composePath); statErr != nil {
77+
spinner.Fail(fmt.Sprintf("docker-compose.yml not found: %s", composePath))
78+
79+
allPassed = false
80+
} else {
81+
spinner.Success("docker-compose.yml found")
82+
}
83+
}
84+
85+
// Check 4: Docker
86+
spinner = ui.NewSpinner("Checking Docker daemon")
87+
88+
cmd := exec.CommandContext(ctx, "docker", "info")
89+
if err := cmd.Run(); err != nil {
90+
spinner.Fail("Docker daemon not accessible - Ensure Docker Desktop is running")
91+
92+
allPassed = false
93+
} else {
94+
spinner.Success("Docker daemon accessible")
95+
}
96+
97+
// Check 5: Docker Compose
98+
spinner = ui.NewSpinner("Checking Docker Compose")
99+
100+
cmd = exec.CommandContext(ctx, "docker", "compose", "version")
101+
if err := cmd.Run(); err != nil {
102+
spinner.Fail("Docker Compose not available")
103+
104+
allPassed = false
105+
} else {
106+
spinner.Success("Docker Compose available")
107+
}
108+
109+
// Check 6: Port conflict warning with lab stack
110+
checkPortConflicts(log, configPath)
111+
112+
// Summary
113+
ui.Blank()
114+
115+
if allPassed {
116+
ui.Success("All checks passed! Environment is ready.")
117+
118+
ui.Header("Next steps:")
119+
fmt.Println(" xcli xatu up # Start the xatu stack")
120+
fmt.Println(" xcli xatu up --build # Start with image rebuild")
121+
122+
return nil
123+
}
124+
125+
ui.Error("Some checks failed. Please resolve the issues above.")
126+
127+
return fmt.Errorf("environment checks failed")
128+
}
129+
130+
// checkPortConflicts warns about potential port conflicts between xatu and lab stacks.
131+
func checkPortConflicts(log logrus.FieldLogger, configPath string) {
132+
result, err := config.Load(configPath)
133+
if err != nil || result.Config.Lab == nil || result.Config.Xatu == nil {
134+
return
135+
}
136+
137+
// Both stacks are configured - warn about common port conflicts
138+
ui.Blank()
139+
ui.Warning("Both lab and xatu stacks are configured. Common port conflicts:")
140+
fmt.Println(" - Grafana (3000), Prometheus (9090), ClickHouse (8123)")
141+
ui.Info("Use xatu.envOverrides in .xcli.yaml to remap xatu ports, e.g.:")
142+
fmt.Println(" xatu:")
143+
fmt.Println(" envOverrides:")
144+
fmt.Println(" GRAFANA_PORT: \"3001\"")
145+
}

0 commit comments

Comments
 (0)