Skip to content

Commit dc79b5c

Browse files
committed
feat: implement backend refactor for improved stack management and configuration handling
This commit introduces a new backend structure for managing both the Lab and Xatu stacks, aiming to enhance code maintainability and extend functionality. The refactor separates concerns related to each stack type and includes capabilities for editable configurations, service management, and environment variable overrides. Key changes include: - Creation of `labBackend` and `xatuBackend` to encapsulate operations specific to their respective stacks. - Introduction of standardized capability checks, allowing the frontend to adaptively display features based on stack capabilities. - Streamlined API responses, including stack capabilities in the stack information payloads. - Improved structure for configuration editing, allowing for async updates and management of profiles and environment variables through dedicated components. These changes are aimed at enhancing the user experience by providing better feedback on stack status and improving the flexibility of the configuration management interface. feat(stack): create StackBackend interface to unify stack management and add backend logic for managing services feat(stack): implement StackBackend methods for service control, logging, and config management feat(logs): improve logging stream management with context cancellation and liveness detection feat(compose): add Pull method to pull the latest service images fix(stack_context): enhance service health management and initialization logic for existing services fix(tui): handle potential panics when stopping health monitors and streaming logs
1 parent 67aa180 commit dc79b5c

33 files changed

+2005
-1027
lines changed

pkg/cc/api.go

Lines changed: 23 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ import (
1414
"time"
1515

1616
"github.com/ethpandaops/xcli/pkg/ai"
17-
"github.com/ethpandaops/xcli/pkg/config"
1817
"github.com/ethpandaops/xcli/pkg/git"
19-
"github.com/ethpandaops/xcli/pkg/orchestrator"
2018
"github.com/ethpandaops/xcli/pkg/tui"
2119
"github.com/sirupsen/logrus"
2220
)
@@ -39,7 +37,7 @@ type stackProgressEvent struct {
3937

4038
// stackState tracks background stack operations to prevent concurrent boots/stops.
4139
type stackState struct {
42-
status string // "idle", "starting", "stopping"
40+
status string // "idle", "starting", "running", "stopping"
4341
lastError string // last boot/stop error, cleared on next operation
4442
cancelBoot context.CancelFunc // cancels the in-progress boot context; nil when not booting
4543
progressEvents []stackProgressEvent // accumulated progress events for the current operation
@@ -49,12 +47,8 @@ type stackState struct {
4947
// apiHandler holds dependencies for REST API handlers.
5048
type apiHandler struct {
5149
log logrus.FieldLogger
52-
wrapper *tui.OrchestratorWrapper
53-
health *tui.HealthMonitor
54-
orch *orchestrator.Orchestrator
50+
backend StackBackend
5551
redis *RedisAdmin
56-
labCfg *config.LabConfig
57-
cfgPath string
5852
gitChk *git.Checker
5953
aiDefaultProvider ai.ProviderID
6054
diagnoseSessions map[string]*diagnoseSession
@@ -68,10 +62,9 @@ type apiHandler struct {
6862

6963
// statusResponse is the full dashboard snapshot.
7064
type statusResponse struct {
71-
Services []serviceResponse `json:"services"`
72-
Infrastructure []infraResponse `json:"infrastructure"`
73-
Config configResponse `json:"config"`
74-
Timestamp time.Time `json:"timestamp"`
65+
Services []serviceResponse `json:"services"`
66+
Config any `json:"config"`
67+
Timestamp time.Time `json:"timestamp"`
7568
}
7669

7770
// serviceResponse represents a service with merged health info.
@@ -86,13 +79,6 @@ type serviceResponse struct {
8679
LogFile string `json:"logFile"`
8780
}
8881

89-
// infraResponse represents infrastructure status.
90-
type infraResponse struct {
91-
Name string `json:"name"`
92-
Status string `json:"status"`
93-
Type string `json:"type"`
94-
}
95-
9682
// configResponse is a sanitized view of the lab configuration.
9783
type configResponse struct {
9884
Mode string `json:"mode"`
@@ -139,41 +125,22 @@ type repoInfo struct {
139125
Error string `json:"error,omitempty"`
140126
}
141127

142-
// recreateOrchestrator rebuilds the orchestrator with the current config.
143-
// This is needed after config changes (e.g. mode switch) so the orchestrator
144-
// picks up the new mode, ports, etc. Must be called with a.mu held for writing.
145-
func (a *apiHandler) recreateOrchestrator() error {
146-
newOrch, err := orchestrator.NewOrchestrator(a.log, a.labCfg, a.cfgPath)
147-
if err != nil {
148-
return fmt.Errorf("failed to recreate orchestrator: %w", err)
149-
}
150-
151-
a.orch = newOrch
152-
a.wrapper.SetOrchestrator(newOrch)
153-
154-
return nil
155-
}
156-
157128
// handleGetStatus returns the full dashboard snapshot.
158-
func (a *apiHandler) handleGetStatus(w http.ResponseWriter, _ *http.Request) {
129+
func (a *apiHandler) handleGetStatus(w http.ResponseWriter, r *http.Request) {
130+
ctx := r.Context()
131+
159132
resp := statusResponse{
160-
Services: a.getServicesData(),
161-
Infrastructure: a.getInfraData(),
162-
Config: a.getConfigData(),
163-
Timestamp: time.Now(),
133+
Services: a.backend.GetServices(ctx),
134+
Config: a.backend.GetConfigSummary(),
135+
Timestamp: time.Now(),
164136
}
165137

166138
writeJSON(w, http.StatusOK, resp)
167139
}
168140

169141
// handleGetServices returns all services with health info.
170-
func (a *apiHandler) handleGetServices(w http.ResponseWriter, _ *http.Request) {
171-
writeJSON(w, http.StatusOK, a.getServicesData())
172-
}
173-
174-
// handleGetInfrastructure returns infrastructure status.
175-
func (a *apiHandler) handleGetInfrastructure(w http.ResponseWriter, _ *http.Request) {
176-
writeJSON(w, http.StatusOK, a.getInfraData())
142+
func (a *apiHandler) handleGetServices(w http.ResponseWriter, r *http.Request) {
143+
writeJSON(w, http.StatusOK, a.backend.GetServices(r.Context()))
177144
}
178145

179146
// handleGetGit returns git status for all repos, caching successful
@@ -192,12 +159,11 @@ func (a *apiHandler) handleGetGit(w http.ResponseWriter, r *http.Request) {
192159

193160
a.gitCache.mu.RUnlock()
194161

195-
repos := map[string]string{
196-
"cbt": a.labCfg.Repos.CBT,
197-
"xatu-cbt": a.labCfg.Repos.XatuCBT,
198-
"cbt-api": a.labCfg.Repos.CBTAPI,
199-
"lab-backend": a.labCfg.Repos.LabBackend,
200-
"lab": a.labCfg.Repos.Lab,
162+
repos := a.backend.GitRepos()
163+
if len(repos) == 0 {
164+
writeJSON(w, http.StatusOK, gitResponse{Repos: []repoInfo{}})
165+
166+
return
201167
}
202168

203169
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
@@ -277,13 +243,13 @@ func (a *apiHandler) handlePostServiceAction(
277243

278244
switch action {
279245
case "start":
280-
err = a.wrapper.StartService(ctx, name)
246+
err = a.backend.StartService(ctx, name)
281247
case "stop":
282-
err = a.wrapper.StopService(ctx, name)
248+
err = a.backend.StopService(ctx, name)
283249
case "restart":
284-
err = a.wrapper.RestartService(ctx, name)
250+
err = a.backend.RestartService(ctx, name)
285251
case "rebuild":
286-
err = a.wrapper.RebuildService(ctx, name)
252+
err = a.backend.RebuildService(ctx, name)
287253
default:
288254
writeJSON(w, http.StatusBadRequest, map[string]string{
289255
"error": "unknown action: " + action,
@@ -326,7 +292,7 @@ func (a *apiHandler) handleGetServiceLogs(
326292
return
327293
}
328294

329-
logPath := filepath.Clean(a.orch.LogFilePath(name))
295+
logPath := filepath.Clean(a.backend.LogFilePath(name))
330296

331297
f, err := os.Open(logPath) //nolint:gosec // path is constructed by LogFilePath from internal config
332298
if err != nil {
@@ -355,78 +321,6 @@ func (a *apiHandler) handleGetServiceLogs(
355321
writeJSON(w, http.StatusOK, lines)
356322
}
357323

358-
// getServicesData builds the services response slice.
359-
func (a *apiHandler) getServicesData() []serviceResponse {
360-
services := a.wrapper.GetServices()
361-
result := make([]serviceResponse, 0, len(services))
362-
363-
for _, svc := range services {
364-
resp := serviceResponse{
365-
Name: svc.Name,
366-
Status: svc.Status,
367-
PID: svc.PID,
368-
URL: svc.URL,
369-
Ports: svc.Ports,
370-
Health: svc.Health,
371-
LogFile: svc.LogFile,
372-
}
373-
374-
if svc.Uptime > 0 {
375-
resp.Uptime = formatDuration(svc.Uptime)
376-
}
377-
378-
result = append(result, resp)
379-
}
380-
381-
return result
382-
}
383-
384-
// getInfraData builds the infrastructure response slice.
385-
func (a *apiHandler) getInfraData() []infraResponse {
386-
infra := a.wrapper.GetInfrastructure()
387-
result := make([]infraResponse, 0, len(infra))
388-
389-
for _, i := range infra {
390-
result = append(result, infraResponse{
391-
Name: i.Name,
392-
Status: i.Status,
393-
Type: i.Type,
394-
})
395-
}
396-
397-
return result
398-
}
399-
400-
// getConfigData builds the sanitized config response.
401-
func (a *apiHandler) getConfigData() configResponse {
402-
networks := make([]networkInfo, 0, len(a.labCfg.Networks))
403-
for _, n := range a.labCfg.Networks {
404-
networks = append(networks, networkInfo{
405-
Name: n.Name,
406-
Enabled: n.Enabled,
407-
PortOffset: n.PortOffset,
408-
})
409-
}
410-
411-
return configResponse{
412-
Mode: a.labCfg.Mode,
413-
Networks: networks,
414-
Ports: portsInfo{
415-
LabBackend: a.labCfg.Ports.LabBackend,
416-
LabFrontend: a.labCfg.Ports.LabFrontend,
417-
CBTBase: a.labCfg.Ports.CBTBase,
418-
CBTAPIBase: a.labCfg.Ports.CBTAPIBase,
419-
CBTFrontendBase: a.labCfg.Ports.CBTFrontendBase,
420-
ClickHouseCBT: a.labCfg.Infrastructure.ClickHouseCBTPort,
421-
ClickHouseXatu: a.labCfg.Infrastructure.ClickHouseXatuPort,
422-
Redis: a.labCfg.Infrastructure.RedisPort,
423-
Prometheus: a.labCfg.Infrastructure.Observability.PrometheusPort,
424-
Grafana: a.labCfg.Infrastructure.Observability.GrafanaPort,
425-
},
426-
CfgPath: a.cfgPath,
427-
}
428-
}
429-
430324
// formatDuration formats a duration into a human-readable string.
431325
func formatDuration(d time.Duration) string {
432326
d = d.Truncate(time.Second)

0 commit comments

Comments
 (0)